Я учусь создавать эффекты для wpf с помощью hlsl. В настоящее время я пытаюсь создать простой эффект, который отмечает края изображения. Я также хочу использовать для этого оператор Собеля, поэтому я установил общедоступный float2x3 в своем коде hlsl, но я не могу получить доступ к элементам в этой матрице.
Я пробовал вручную вводить правильные значения, и это работает, но не тогда, когда я использую цикл.
sampler2D imageSampler : register(s0);
float imageWidth : register(c0);
float imageHeight : register(c1);
float threshold : register(c2);
float2x3 op =
{
1.0f, 2.0f, 1.0f,
-1.0f, -2.0f, -1.0f
};
float grayScale(float3 color)
{
return (color.r + color.g + color.b) / 3;
}
float4 GetEdgeLoop(float2 coord, float2 pixelSize)
{
float2 current;
float avrg = 0;
float holder;
float gsHolder;
current.x = coord.x - pixelSize.x;
for (int x = 0; x < 2; x++)
{
current.y = coord.y - pixelSize.y;
for (int y = 0; y < 3; y++)
{
holder = op[x][y];
gsHolder = grayScale(tex2D(imageSampler, current).rgb);
avrg += gsHolder * holder;
current.y += pixelSize.y;
}
current.x += pixelSize.x * 2;
}
avrg = abs(avrg / 8);
if (avrg > threshold)
return float4(1, 0, 0, 1);
return tex2D(imageSampler, coord);
}
float4 main(float2 uv : TEXCOORD) : COLOR
{
float2 pixelSize = (1 / imageWidth, 1 / imageHeight);
return GetEdgeLoop(uv, pixelSize);
}
Этот метод должен возвращать красный цвет для достаточно сильных краев. Иногда это возвращает красный цвет, но явно не для краев.
У меня есть другой метод обнаружения краев, который действительно работает, но он выбирает нужные пиксели вручную:
float4 GetEdge(float2 coord, float2 pixelSize)
{
float avrg = 0;
avrg += grayScale(tex2D(imageSampler, float2(coord.x - pixelSize.x, coord.y - pixelSize.y)).rgb) * 1;
avrg += grayScale(tex2D(imageSampler, float2(coord.x - pixelSize.x, coord.y)).rgb) * 2;
avrg += grayScale(tex2D(imageSampler, float2(coord.x - pixelSize.x, coord.y + pixelSize.y)).rgb) * 1;
avrg += grayScale(tex2D(imageSampler, float2(coord.x + pixelSize.x, coord.y - pixelSize.y)).rgb) * (-1);
avrg += grayScale(tex2D(imageSampler, float2(coord.x + pixelSize.x, coord.y)).rgb) * (-2);
avrg += grayScale(tex2D(imageSampler, float2(coord.x + pixelSize.x, coord.y + pixelSize.y)).rgb) * (-1);
avrg = abs(avrg / 8);
if (avrg > threshold)
return float4(1, 0, 0, 1);
return tex2D(imageSampler, coord);
}
Этот метод не очень элегантный, и я хочу его заменить.
Ответ 1 · George H.
Я не эксперт по шейдерам в WPF, но в предоставленном вами коде вы не указываете никаких регистров для ввода, поэтому он недоступен из кода пиксельного шейдера. Я ожидаю, что что-то вроде этого будет частью вашего кода:
sampler2D implicitInputSampler : register(S0);
float opacity : register(C0);
(Первое определение - сэмплер текстуры, второе - регистр с плавающей точкой.)
Вам нужно "объявить" объекты, которые будут переданы в функцию шейдера. В руководстве Microsoft по написанию шейдерных эффектов есть хороший пример по адресу https://docs.microsoft.com/en-us/dotnet/api/system.windows.media.effects.shadereffect?view=netframework-4.8. Вот код для потомков:
// Threshold shader
// Object Declarations
sampler2D implicitInput : register(s0);
float threshold : register(c0);
float4 blankColor : register(c1);
//------------------------------------------------------------------------------------
// Pixel Shader
//------------------------------------------------------------------------------------
float4 main(float2 uv : TEXCOORD) : COLOR
{
float4 color = tex2D(implicitInput, uv);
float intensity = (color.r + color.g + color.b) / 3;
float4 result;
if (intensity > threshold)
{
result = color;
}
else
{
result = blankColor;
}
return result;
}
Вы можете видеть, что код объявляет сэмплер текстуры, число с плавающей запятой и число с плавающей запятой4 (вектор с 4 числами с плавающей запятой) в качестве входных данных. Затем вы можете привязаться к ним через DependencyProperties следующим образом:
#region Input dependency property
public Brush Input
{
get { return (Brush)GetValue(InputProperty); }
set { SetValue(InputProperty, value); }
}
public static readonly DependencyProperty InputProperty =
ShaderEffect.RegisterPixelShaderSamplerProperty("Input", typeof(ThresholdEffect), 0);
#endregion
///////////////////////////////////////////////////////////////////////
#region Threshold dependency property
public double Threshold
{
get { return (double)GetValue(ThresholdProperty); }
set { SetValue(ThresholdProperty, value); }
}
public static readonly DependencyProperty ThresholdProperty =
DependencyProperty.Register("Threshold", typeof(double), typeof(ThresholdEffect),
new UIPropertyMetadata(0.5, PixelShaderConstantCallback(0)));
#endregion
///////////////////////////////////////////////////////////////////////
#region BlankColor dependency property
public Color BlankColor
{
get { return (Color)GetValue(BlankColorProperty); }
set { SetValue(BlankColorProperty, value); }
}
public static readonly DependencyProperty BlankColorProperty =
DependencyProperty.Register("BlankColor", typeof(Color), typeof(ThresholdEffect),
new UIPropertyMetadata(Colors.Transparent, PixelShaderConstantCallback(1)));
#endregion
Обратите внимание, что PixelShaderConstantCallback имеет параметр int, который позволяет вам указать индекс регистра, к которому вы хотите привязать значение.
Редактировать: Хорошо, я прочитал еще кое-что по этой теме, поиграл с вашим кодом и заставил его работать. Вот изменения, которые я сделал в произвольном порядке:
- Я читал, что вы должны объявить свой массив как static const. Он не меняется во время выполнения шейдера и, по-видимому, "может" позволить некоторые незначительные оптимизации компилятора.
- Я изменил float2x3 на float3x3, чтобы соответствовать ядру Лапласа, поскольку мы хотим, чтобы края работали со всех сторон изображения.
- Я изменил функцию оттенков серого, чтобы использовать веса для подхода яркости.
Вот соответствующий код:
static const float3x3 laplace =
{
-1.0f, -1.0f, -1.0f,
-1.0f, 8.0f, -1.0f,
-1.0f, -1.0f, -1.0f,
};
float grayScaleByLumino(float3 color)
{
return (0.299 * color.r + 0.587 * color.g + 0.114 * color.b);
}
float4 GetEdgeGeorge(float2 coord, float2 pixelSize)
{
float2 current = coord;
float avrg = 0;
float kernelValue;
float4 currentColor;
float grayScale;
float4 result;
current.x = coord.x - pixelSize.x;
for (int x = 0; x < 3; x++)
{
current.y = coord.y - pixelSize.y;
for (int y = 0; y < 3; y++)
{
kernelValue = laplace[x][y];
grayScale = grayScaleByLumino(tex2D(imageSampler, current).rgb);
avrg += grayScale * kernelValue;
current.y += pixelSize.y;
}
current.x += pixelSize.x;
}
avrg = abs(avrg / 8);
if (avrg > threshold)
{
result = float4(1, 0, 0, 1);
}
else
{
result = tex2D(imageSampler, coord);
}
return result;
}
Все решение, которое я создал, доступно здесь: https://github.com/georgethejournalist/WPFShaders . Надеюсь, это поможет.