Writing custom Effects - adding parameters to Effects

A couple of posts ago, I wrote about writing custom Effects.  The example that I dove into was ColorComplementEffect, an Effect that has no parameters other than the incoming "sampler" that it inverts the color on.

This post is going to go into what it takes to add parameters to your Effects, which allows for much, much more powerful Effects.

Shader Constants and Dependency Properties

The first thing to understand is that HLSL shaders expose "shader constants" bound to "shader registers".  We saw a shader constant in the form of a "sampler2D" called "implicitInput" in the previous post on writing Effects:

 sampler2D implicitInput : register(s0);

The parameters we discuss here will be shader constants of type float, float2, float3, or float4 in HLSL.  These shader constants maintain their value for an entire "frame" of the pixel shader executing across every pixel in the frame.  This is why they're called "shader constants", since they're constant per-frame, though they can and often do change between frames.

From an Effects point of view, they can be thought of as a property or parameter of the shader/effect.  And we already have a great way of dealing with properties in WPF -- we use DependencyProperties, which provides for change notification, databinding, animation, etc.

So... the next step is kind of obvious...  we expose HLSL shader constants through custom Dependency Properties on the corresponding Effect.

An example - ThresholdEffect

Let's move to a simple example... the ThresholdEffect used here:

         <eff:ThresholdEffect Threshold="0.25" BlankColor="Orange" />

image

This Effect just turns pixels that are below the specified Threshold intensity (0.25 in the case above) into the BlankColor (orange, in the case above).

Here's the HLSL for the ThresholdEffect:

 sampler2D implicitInput : register(s0);
float threshold : register(c0);
float4 blankColor : register(c1);

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;
}

It's a very straightforward effect.  It samples the texture, figures out the intensity by averaging RGB, and if above the threshold returns the sampled color, otherwise the blankColor.

I'll list the entire C# for the ThresholdEffect class below, but the most important part is to understand how these properties are defined.  Here's one of them, Threshold:

 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)));

The CLR getter/setter Threshold is identical to all CLR getter/setters for DPs... it just does a GetValue/SetValue.  Then the definition for ThresholdProperty is also the same as all DPs...  the one difference is that the PropertyChangedCallback is created via "PixelShaderConstantCallback(registerNumber)", which generates a callback to be invoked when the property changes.  In this case, we pass 0 as the parameter to PixelShaderConstantCallback, since that matches the "threshold" shader constant in the HLSL that's assigned register "c0".

Once we've set this up, ThresholdProperty is just like any other DP in the system.  Bind to it, bind from it, animate it, etc.

The only other thing we need to do is call "UpdateShaderValue(ThresholdProperty)" in the constructor of the Effect.  This is necessary to inform the system about this value the first time, since the PropertyChangedCallback doesn't execute when the default value is set.  Don't forget to call UpdateShaderValue() on each of the properties you define, including InputProperty!!

What Types are supported?

The DependencyProperties that are bound to floating point shader constant registers can be any of the following types:

  • Double
  • Single ('float' in C#)
  • Color
  • Size
  • Point
  • Vector
  • Point3D
  • Vector3D
  • Point4D

They each will go into their shader register filling up whatever number of components of that register are appropriate.  For instance, Double and Single go into one component, Color into 4, Size, Point and Vector into 2, etc.  Unfilled components are set to '1'.

Some minutiae

Register Limit : There is a limit of 32 floating point registers that can be used in PS 2.0.  In the unlikely event that you have more values than that that you want to pack in, you might consider tricks like packing, for instance, two Points into a single Point4D, etc.

What about int and bool registers?:  PS 2.0 doesn't deal particularly well with int and bool registers.  We decided to support only float registers.  If for some reason, you really need int or bool in your HLSL, you can cast a float register as appropriate.

 

Complete ThresholdEffect listing

Finally, here's the entire listing for the ThresholdEffect class, which includes the Input sampler property, Threshold that we saw above, and the BlankColor property, that's managed in the exact same way that we did Threshold:

 public class ThresholdEffect : ShaderEffect
{
    public ThresholdEffect()
    {
        PixelShader = _pixelShader;

        UpdateShaderValue(InputProperty);
        UpdateShaderValue(ThresholdProperty);
        UpdateShaderValue(BlankColorProperty);
    }

    public Brush Input
    {
        get { return (Brush)GetValue(InputProperty); }
        set { SetValue(InputProperty, value); }
    }

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


    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)));


    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)));


    private static PixelShader _pixelShader =
        new PixelShader() { UriSource = Global.MakePackUri("ThresholdEffect.ps") };
}