Writing WPF pixel shaders in C#!

This week I saw a great project, Brahma, on expressing shader-based GPU computation in C#/LINQ. Using Brahma, one can write a simple LINQ query to transform a matrix without ever touching HLSL code or compiler, neat stuff! The advantage of this approach is that you don’t have to leave C# and the accompany tool support, and you don't have to setup your project to support pixel shader development.

So I was thinking: can we do this to write WPF pixel shaders? Sure! I found a nice project that allows WPF pixel shaders to be compiled on the fly by NyaRuRu (in Japanese only), and the author gave me permission to use his compiler in my WPF signals project. I then wrote a lifted-style meta-programming library to generate HLSL code from lifted C# code—this is the same way I wrapped WPF databinding using signals. Basically, the body of a pixel shader uses special lifted types for floats, doubles, and the other data types that can go into a pixel shader. Example:

     public static ColorShader Emboss(ShaderCompiler<NoArgLiftedShader> txt, PointShader uv) {
      var input = txt.ImplicitInput;
      var color = input[uv];
      var cA = txt.Cache(input[uv - 0.001] * 2).Lft();
      var cB = txt.Cache(input[uv + 0.001] * 2).Lft();
      var cC = txt.Cache(ColorShader.New(0.5f, 1) - cA + cB).Lft();
      return ColorShader.New(txt.Cache((cC.R + cC.G + cC.B) / 3f).Lft(), color.A);
    }

This pixel shader method implements an emboss effect (I found the algorithm here). “uv” is the current point, implicit input is obtained from the shader compiler, use standard bracket syntax to access elements of the array. A special “Cache” method in the shader compiler is needed to create new local variables, where otherwise expressions will be repeated if they are used more than once. All standard arithmetic operations are available along with various data type constructors; e.g., ColorShader.New. To apply this shader, simply call LiftedShaderEffect.Apply and assign the result to the Effect property of your widget:

       canvas.Effect = LiftedShaderEffect.Apply(ShaderFunctions.Emboss);

Now, isn’t that easy? A more complicated lifted shader can have one or more “registers” that are represented as dependency properties in WPF. Here is an example of a wave shader that I copied from a random XNA sample project:

   public class WaveShader : LiftedShaderEffect {
    private static readonly LiftedShaderConfig Config0 = CreateConfig<WaveShader>();
    protected override LiftedShaderConfig Config { get { return Config0; } }
    private static readonly DependencyProperty WaveProperty = Config0.AddParameter<double>("Wave", Math.PI / 0.75);
    private static readonly DependencyProperty DistortionProperty = Config0.AddParameter<double>("Distortion", 1d);
    private static readonly DependencyProperty CenterProperty = Config0.AddParameter<Point>("Center", new Point(0.5,0.5));
    public Bling.Signals.DoubleSg Wave() { return this.Signal<double>(WaveProperty).Lft(); }
    public Bling.Signals.DoubleSg Distortion() { return this.Signal<double>(DistortionProperty).Lft(); }
    public Bling.Signals.PointSg Center() { return this.Signal<Point>(CenterProperty).Lft(); }
    static WaveShader() { Config0.ShaderFunction = ShaderFunction0; }
    private static ColorSh ShaderFunction0(ShaderCompiler txt, PointSh uv) {
      var wave = txt.Parameter<double>(WaveProperty).LftSh();
      var distortion = txt.Parameter<double>(DistortionProperty).LftSh();
      var center = txt.Parameter<Point>(CenterProperty).LftSh();
      var distance = (uv - center).Abs();
      var scalar = distance.Length;
      // invert the scale so 1 is centerpoint
      scalar = (1 - scalar).Abs();
      // calculate how far to distort for this pixel    
      var offset = (wave / scalar).Sin();
      offset = offset.Clamp(0, 1);
      // calculate which direction to distort
      var sign = (wave / scalar).Cos();
      // reduce the distortion effect
      offset = offset * distortion / 32;
      var input = txt.ImplicitInput;
      return input[uv + (offset * sign)];
    }
  }

A custom lifted shader effect must extend LiftedShaderEffect with itself as the type parameter (a bit of a cludge, I did some type hacking). It must also statically create a LiftedShaderConfig that manages transparently the registers and inputs of the pixel shader effect. Then, again statically, you can create your registers using the AddParameter of this config object by specifying the type of the parameter (passed in as a type parameter) along with its name and default value. Finally, you have to override the abstract Config property of LiftedShaderEffect with the static config object that you created. I’ve then wrapped all these dependency properties as signals for convenience. The pixel shader function is defined in WaveShader’s static initializer by assigning ShaderFunctin0 to ShaderFunction of the config object. Inside the shader, each shader parameter from the shader compiler using the Parameter method with same property type that you used in AddParameter to create the dependency property (which only remember their type as a value). Finally, the LftSh() method has to be called on these parameters so that the appropriate operators are available. The rest of the code looks pretty standard, you can access methods like Abs (absolute value), Length (of a vector), Sin, Clamp, and Cos in the C# code as you would in HLSL code.

The code and a release is available at https://www.codeplex.com/wpfsignals. Download the latest release and build (should work ok if you have 3.5 SP1, although I’m not sure if the latest DirectX SDK is required or not…). Run the SignalTest project and you should see this picture:

image

The blue thumb in the middle as well as the two sliders are data bound to the wave shader parameters (via signals of course!). You can move these around to cause the pixel shader to refresh the screen. If you want to use this in your own code, just include Bling.Shaders in your project and add “using Bling.Shaders” to the set of used namespaces (add “using Bling.Signals” if you want to also use signals).