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

Comments

  • Anonymous
    May 16, 2008
    float threshold : register(c0); ... public static readonly DependencyProperty ThresholdProperty = DependencyProperty.Register("Threshold", ... Is this case-sensitive btw?

  • Anonymous
    May 17, 2008
    Very interesting ! Will it be in the futur possible to use another brush as a shader property ? For exemple, to apply some kind of bump mapping effect to a control ?

  • Anonymous
    May 20, 2008
    Yeah, how do you feed additional textures into the shader?

  • Anonymous
    May 21, 2008
    zzz asks about "threshold" compared to "Threshold".  Actually, there's no relation between these two... they can be called whatever you want them to be.  The association happens with the register index.  Note the "register(c0)" in the HLSL, and the PixelShaderConstantCallback(0) in the C#.  It's the zero in both that make the association. Roland and Kevin both ask about added brushes/textures as inputs into the shader.  Yes -- that's super important for flexibility.  We'll have that functionality available in the RTM release of .NET 3.5 SP1, but it's not available in the Beta.  

  • Anonymous
    May 21, 2008
    One more quick question. How do you control what region a shader applies to? For your threshold effect, it's pretty straightforward. However, with the drop shadow, it extends outside of the bounds of the item. Does it apply the shader for the entire surface of the window?

  • Anonymous
    May 22, 2008
    Kevin asks about controlling the region the shader applies to.   In the Beta release, the region is exactly the same region as the UIElement that the Effect is being applied to.  However, for the RTM version of 3.5 SP1, we added a set of "padding" parameters that let you extend the region outward, with individually controllable padding on the left, right, top, and bottom.  This, as you describe, is what one would use to implement something like DropShadow.

  • Anonymous
    May 23, 2008
    Hi Greg, You mentioned that support of additional textures isn't present in the beta. I was wondering why this was the case? In your code sample from a previous post of yours, you specify 0 for the texture sampler register: public static readonly DependencyProperty InputProperty =    ShaderEffect.RegisterPixelShaderSamplerProperty(            "Input",            typeof(ColorComplementEffect),            0); Would the beta runtime throw an error if I attempted to bind to another register by specifying, for example, 1 as the last parameter? Cheers, thanks for the posts!

  • Anonymous
    May 26, 2008
    OJ...  what you're seeing is the wiring up of the "implicit input" to a texture register. And for that, yes, registers other than 0 would work.  However, what doesn't work in the Beta is specifying an alternate Brush, which is what's required to get some other texture into the shader.  That's what comes in in the RTM release.

  • Anonymous
    June 02, 2008
    The comment has been removed

  • Anonymous
    June 02, 2008
    Okay, I've succeeded in doing a bump-mapping effect with the beta, using the alpha layer as the heighmap reference texture ! The result is quite impressing. I'll post the source code soon on my blog (a still-young french blog dedicated to WPF) (http://r.tomczak.free.fr/wordpress/)

  • Anonymous
    September 16, 2008
    The comment has been removed