• Writing HLSL Pixel Shaders for WPF

    by  • October 18, 2012 • .Net, Journal, Programming • 0 Comments

    This article is part of a series of articles I have written covering WPF; improving its performance, its quirks and associated workarounds, as well as porting it to WUA Applications:

     

    One of the limiting factors of WPF used to be the availability of hardware accelerated effects for graphics and video. This has all changed with the inclusion of Pixel Shaders in WPF 3.5 onwards.

    A Pixel Shader is a routine that gets applied as you may guess, to the individual pixels, however it is performed directly on the graphics card for huge performance boosts. This means you can offload complex operations onto the GPU and leave your CPU to handle animations and timings.

    Recently I’ve had the chance to play around with Pixel Shaders and learn some of the High Level Shader Language (HLSL) originally developed for DirectX, and thought I’d write a tutorial. HLSL, which syntactically looks a bit like see, is itself is pretty limited with only a handful of functions, but it is designed to be as compact and fast as possible, you aren’t meant to write entire apps in it.

    Graphics cards in general have been designed to handle floating point calculations at very high speed, thus integers or booleans are not present in HLSL. Below is a rough equivalency guide for WPF types to their HLSL equivalents.

    The following table shows all allowed input types (as defined in the ShaderEffect Class) and the corresponding HLSL types (as defined in the pixel shader). Only floating-point values are currently allowed.

    .NET type HLSL type
    System.Boolean (C# keyword bool) Not Available
    System.Int32 (C# keyword int) Not Available
    System.Double (C# keyword double) float
    System.Single (C# keyword float) float
    System.Windows.Size float2
    System.Windows.Point float2
    System.Windows.Vector float2
    System.Windows.Media.Media3D.Point3D float3
    System.Windows.Media.Media3D.Vector3D float3
    System.Windows.Media.Media3D.Point4D float4
    System.Windows.Media.Color float4

    Before we start, you will need to download and install the latest DirectX SDK, this is required because it contains the Pixel Shader Compiler tool, fxc.exe.

    fxc.exe compiles the human readable HLSL into a bytecode format which can be loaded by DirectX & the GPU.

    After you install the SDK, you will find fxc.exe in the following location:

    (SDK root)\Utilities\Bin\x86\

    Creating the Pixel Shader Wrapper

    You can’t directly access a Pixel Shader from WPF too easily, to access it you need create a class which extends the ShadderEffect class and inherits from the underlying Effect class.

    The ShadderEffect class has a property called PixelShader which needs to be set with an instance of a PixelShader object. The PixelShader object has to be told where its effect is located via its UriSource.

    public class Transparency : ShaderEffect {
      static Transparency() {
        // Associate _pixelShader with our compiled pixel shader
        _pixelShader.UriSource = Global.MakePackUri("Transparency.ps");
      }
    
      private static PixelShader _pixelShader = new PixelShader();
    
      public Transparency() {
        this.PixelShader = _pixelShader;
        UpdateShaderValue(InputProperty);
        UpdateShaderValue(OpacityProperty);
      }
    
      public Brush Input {
        get { return (Brush)GetValue(InputProperty); }
        set { SetValue(InputProperty, value); }
      }
    
      public static readonly DependencyProperty InputProperty =
          ShaderEffect.RegisterPixelShaderSamplerProperty("Input", typeof(Transparency), 0);
    
      public double Opacity {
        get { return (double)GetValue(OpacityProperty); }
        set { SetValue(OpacityProperty, value); }
      }
    
      public static readonly DependencyProperty OpacityProperty =
          DependencyProperty.Register("Opacity", typeof(double), typeof(Transparency),
            new UIPropertyMetadata(1.0d, PixelShaderConstantCallback(0)));
    }
    

    Above is the source code example for the more complex of the following HLSL examples.
    Notice how when you register the Pixel Shader itself you use ShaderEffect.RegisterPixelShaderSamplerProperty with the S register index but when you register an argument travelling into the Pixel Shader you use PixelShaderConstantCallback with the C register index (both which I discuss later).
    Please not that arguments travelling into the Pixel Shader via PixelShaderConstantCallback need to be a double, so as above, you need to pass (for example) 1.0d, and not 1

    Programming HLSL

    For our first HLSL attempt, lets try something simple. In the following code we set the Red(R) channel of each pixel to 255, so red is always illuminated.

    sampler2D texSampler : register(S0);
    
    float4 main(float2 uv : TEXCOORD) : COLOR {
      float4 color = tex2D(texSampler, uv);
      color.r = 255;
      return color;
    }
    

    Now lets break this down a bit and explain line by line:

    sampler2D texSampler : register(S0);
    

    This tells the pixel shader that the 2D texture that is getting sampled is coming from the S0 register, this register value is set from managed code and can allow you to pipe multiple textures into a pixel shader for alpha blending effects etc. We name the texture sampler texSampler.

    float4 main(float2 uv : TEXCOORD) : COLOR {
    

    Like most C programs, we need a main function. This is the main entry point into the pixel shader. Here we specify the texture coordinate uv which is where the pixel resides (0,0 -> 1,1). The function returns a float4, which is a structure that comprises of 4 floats. a, r, g, b

    float4 color = tex2D(texSampler, uv);
    

    Here we declare a locally defined variable called color which is used to hold the pixel colour information from a specific point on the texture surface. The coordinates being those passed to the pixel shader function.

      color.r = 255;
      return color;
    }
    

    This is where we set the red channel, and then we end the function by returning the modified colour structure.

    You can go even further if you want by passing additional variables into a pixel shader, but setting additional registers, below is an example of getting an opacity value from the C0 register. I will talk more later about how you programmatically pass these from WPF.

    sampler2D texSampler : register(S0);
    float opacity : register(C0);
    
    float4 main(float2 uv : TEXCOORD) : COLOR {
      float4 color = tex2D(texSampler, uv);
      return color * opacity;
    }
    

    About

    Software engineer. Tea drinker

    http://MrPfister.com

    Leave a Reply

    Your email address will not be published. Required fields are marked *