Shaders with HLSL #03: Passing Additional Parameters

In previous parts of this series, we briefly discussed the need for Shaders and created our first Shader Program. In this subsequent part, we would be exploring on passing additional parameters to our Custom Shader.

In previous example, we replaced entire color of applied Control with a single color. What if we need to replace only certain colors, among different colors in the control. We would then need to pass the ‘color to replace’ and ‘new color’ to the Shader.

Step 01 : The Shader

Let’s write our Shader, again with HLSL. We will delve into details soon.

sampler2D input: register(s0);
float4 oldColor : register(c0);
float4 newColor : register(c1);

float4 main(float2 uv:TEXCOORD) : COLOR
{
float4 color = tex2D(input, uv);
if (color.r == oldColor.r && color.b == oldColor.b && color.g == oldColor.g)
{
return newColor;
}
return color;
}

We now have two additional parameters defined in the Line 2 & 3. While it follows the earlier parameter declaration, there is an important difference. For additional (non-image) parameters, we are using the C series registers, which are, as discussed in last post, floating point registers. We have two parameters, both of type float4, which would hold details of ‘old color‘ and ‘new color‘.

float4 oldColor : register(c0);
float4 newColor : register(c1);

In the first line of our main function, we see a new function, which we haven’t seen in previous example.

float4 color = tex2D(input, uv);

tex2D is a texture sampling intrinsic function, which generates a vector from its a sampler and a float2, which holds the two-dimensional texture cordinates at which the sampler is to be sampled. The cordinates (u and v), ranges from (0,0) to (1,1) as one moves from top-left to bottom-right.

In the subsequent code, which is self-explanatory, we compare the RGB values to decide whether we need to replace the color.

Step 02 : The ShaderEffect Class

Having written our Shader, the next obvious step is create our custom class derieved from ShaderEffect. Like in Shader code, we will write the code first and then explore the details.

public class ReplaceColorEffect : ShaderEffect
{
public static readonly DependencyProperty InputProperty = ShaderEffect.RegisterPixelShaderSamplerProperty("Input", typeof(ReplaceColorEffect), 0);
public Brush Input
{

get => ((Brush)(GetValue(InputProperty)));
set => SetValue(InputProperty, value);
}

public Color OldColor
{
get { return (Color)GetValue(OldColorProperty); }
set { SetValue(OldColorProperty, value); }
}

public static readonly DependencyProperty OldColorProperty =
DependencyProperty.Register("OldColor", typeof(Color), typeof(ReplaceColorEffect), new PropertyMetadata(Colors.Black,PixelShaderConstantCallback(0)));
public Color NewColor
{
get { return (Color)GetValue(NewColorProperty); }
set { SetValue(NewColorProperty, value); }
}

public static readonly DependencyProperty NewColorProperty =
DependencyProperty.Register("NewColor", typeof(Color), typeof(ReplaceColorEffect), new PropertyMetadata(Colors.Black, PixelShaderConstantCallback(1)));

public ReplaceColorEffect()
{
PixelShader pixelShader = new PixelShader();

pixelShader.UriSource = new Uri(@"E:\App Store\GitHub\anuviswan\LearningPoint\Shaders\ShaderExample001\Shader\ReplaceColor.ps", UriKind.Absolute);

PixelShader = pixelShader;
UpdateShaderValue(InputProperty);
UpdateShaderValue(OldColorProperty);
UpdateShaderValue(NewColorProperty);

}
}

While most of the code is pretty similiar to code in our previous example, ones that needs more attension is discussed below. The most significant difference is the two additional parameters, which are mapped to floating point registers (c registers) and how it is defined in C# code.

public Color OldColor
{
get { return (Color)GetValue(OldColorProperty); }
set { SetValue(OldColorProperty, value); }
}

public static readonly DependencyProperty OldColorProperty =
DependencyProperty.Register("OldColor", typeof(Color), typeof(ReplaceColorEffect), new PropertyMetadata(Colors.Black,PixelShaderConstantCallback(0)));

As observed in the code above, the declaration of dependency properties for c-registers are in the more familiar & conventional syntax, using the DependencyProperty class. What makes it different from normal dependency properties is the PixelShaderConstantCallback parameter of PropertyMetadata class. This tells the compiler that this is a special Dependency property which needs to be mapped to c-registers when communicating with the Shader application.

Rest of the code is pretty self-explanatory and XAML is no different than ones you might have seen countless times, using the dependency properties.

<shadereffects:ReplaceColorEffect OldColor="Red" NewColor="Green"/>

Hit F5 and you can now see your Shader in action. For complete code discussed in this series, please refer to my Github.

Complete Shader with HLSL Series

Advertisements

Shaders with HLSL #02 : Applying Shader to WPF Application

In the first part of series, we wrote our first Shader Program. Now it is time to compile it so that we could use it in our WPF Application.

HLSL Compiler

To compile your HLSL code to a format that could be used with .Net WPF application, you would require the fxc compiler. For Windows 7 and earlier editions, you would need to install DirectX SDK in order to get fxc.exe. For Windows 8 and above, this comes as part of the Windows SDK.

The compiler can found under the location ‘Program Files (x86)\Windows Kits‘. If we had saved our previous written code (from previous part of this series) as RedTint.hlsl, we would need to use following command to compile it.

fxc /T ps_2_0 /E main /Fo RedTint.ps RedTint.hlsl

This would give the necessary ps file, or Pixel Shader file which we could now refer in our WPF Application. Now that we have our Pixel Shader file, we will move on to our WPF application.

Creating our Test Application

We will keep our text application simple. The XAML code is shown below.

<Grid Margin="10,10,10,10">
<Grid.RowDefinitions>
<RowDefinition Height="*"/>
<RowDefinition Height="4*"/>
</Grid.RowDefinitions>
<Grid.ColumnDefinitions>
<ColumnDefinition/>
<ColumnDefinition/>
</Grid.ColumnDefinitions>
<Label Grid.Column="0" Grid.Row="0" Content="Normal Button" HorizontalAlignment="Center"/>
<Button Grid.Column="0" Grid.Row="1" Content="Click Me"/>

<Label Grid.Column="1" Grid.Row="0" Content="Button with Shader" HorizontalAlignment="Center"/>
<Button Grid.Column="1" Grid.Row="1" Content="Click Me"/>
</Grid>

Also enclosed is a screenshot of the application. The purpose of our application would be apply our shader to one of the buttons, while leaving the other button untouched, so that we could easily demonstrate the difference (or rather impact of shader). We will also add our RentTint.ps file to our project as a resource.

Defining Custom Effect

To use the compiled shader application, we need to load it to the rendering engine input stream. This is accomplished with the help of ShaderEffect class in .Net. The ShaderEffect base class makes it possible to wrap the HLSL pixel shader and apply them our applications XAML elements.

public class RedTintEffect: ShaderEffect
{
public static readonly DependencyProperty InputProperty = ShaderEffect.RegisterPixelShaderSamplerProperty("Input", typeof(RedTintEffect), 0);
public Brush Input
{

get => ((Brush)(GetValue(InputProperty)));
set => SetValue(InputProperty, value);
}

public RedTintEffect()
{
PixelShader pixelShader = new PixelShader();

pixelShader.UriSource = new Uri(@"path_to_ps_resource", UriKind.Absolute);

PixelShader = pixelShader;
UpdateShaderValue(InputProperty);

}
}

We have defined our custom effect, namely RedTintEffect, derieved from ShaderEffect. Let’s examine the code closely beginging with the constructor.

public RedTintEffect()
{
PixelShader pixelShader = new PixelShader();

pixelShader.UriSource = new Uri(@"path_to_ps_resource", UriKind.Absolute);

PixelShader = pixelShader;
UpdateShaderValue(InputProperty);
}

The first step we need to is to initialize an object of PixelShader (which is a protected member of ShaderEffect class) and refer our ps file using the UriSource property. This would enable the ShaderEffect to communicate with the GPU.

The next step is to create a Dependency Property of Type Brush, calling it “Input”. This is a special property and would contain the input image. There lies a difference in the way we register the Dependency Property as well. Instead of the usual DependencyProperty.Register(), we would be using ShaderEffect.RegisterPixelShaderSamplerProperty() method.

public static readonly DependencyProperty InputProperty = ShaderEffect.RegisterPixelShaderSamplerProperty("Input", typeof(RedTintEffect), 0);
public Brush Input
{

get => ((Brush)(GetValue(InputProperty)));
set => SetValue(InputProperty, value);
}

One of the important points to note in the above code is the final parameter of ShaderEffect.RegisterPixelShaderSamplerProperty() method. The Integer value 0 corresponds to the S0 pixel shader register in our HLSL code. Here is the refered code from HLSL source.

sampler2D input: register(s0);

That’s all we need. We can now go back to our XAML and apply the effect.

Applying Shader

The update code is as follows.

<Grid Margin="10,10,10,10">
<Grid.RowDefinitions>
<RowDefinition Height="*"/>
<RowDefinition Height="4*"/>
</Grid.RowDefinitions>
<Grid.ColumnDefinitions>
<ColumnDefinition/>
<ColumnDefinition/>
</Grid.ColumnDefinitions>
<Label Grid.Column="0" Grid.Row="0" Content="Normal Button" HorizontalAlignment="Center"/>
<Button Grid.Column="0" Grid.Row="1" Content="Click Me"/>

<Label Grid.Column="1" Grid.Row="0" Content="Button with Shader" HorizontalAlignment="Center"/>
<Button Grid.Column="1" Grid.Row="1" Content="Click Me">
<Button.Effect>
<shadereffects:RedTintEffect/>
</Button.Effect>
</Button>
</Grid>

We have added our newly added custom Effect to one of the button. Let’s hit F5 and verify our changes.

As observed in the screenshot, the Button to which the Effect was applied has turned Red, while the other Controls remain the same. Do observe that even the Forecolor of Text within the button has turned Red. This is true to our definition of our Shader, since it returns Red irrespective of the input. We will look into more complex scenarios in later posts where we will introduce conditional color changes.

You can access the entire source code described in Part 1 and Part 2 of this series from my Github