Pixel Shader Effects in WPF using .Net3.5sp1 Beta

Technorati Tags: ,,

Well, I finally managed to get round to whipping up a pixel shader today, and a reasonable workflow with skeleton projects to enable quick(ish) turnaround during development.

Since Pixel Shaders were introduced on Greg Shechters blog I’d taken a quick look and one thing was immediately apparent, that using just VS2008 to develop shaders could be a bit of a drag – we lose the visual feedback we normally have when developing normal applications, and we’re back to the good old days of build-run-suck-it-and-see-modify-build again loop. Not ideal, and not quick.

I’ll adapt the process a little using other tools but hopefully this development workflow wont change too much post beta.

Anyway, before we start pixel shading, we’ll need a software few pre-requisites and a bit of prior knowledge.

Software pre-requisites

First of all, if you haven’t installed it, you’ll need the DirectX SDK. This contains the all important compiler tools for compiling your HLSL files into the byte code that the runtime requires. We’ll be linking this tool into the devstudio environment, but not with a custom build step. If you read Greg’s comments, the MS guys are busy fixing up a nice MSBuild target just for this purpose, so I wont spend too much time shoe-horning this into the environment.

Next, we’ll need a tool to help visualise our shaders as we develop them. For this we need to go back to the video card manufacturers developer’s pages.

Since I’m running on an nVidia 8800GTX, I use nVidia’s FX Composer 2.5. FX Composert 2.5 is the latest version of their shader development tool, (also currently in beta – heh, can you tell I’m a sucker for this beta software?! lol!) and is a free download from their developer site.

I’ve been using nVidia hardware for quite a while, hence I’ve always used FX Composer to explore their hardware, but if you’re an ATI fan they also have a tool to perform similar functions – Render Monkey.

Next you’ll need to download the .Net3.5sp1 and Visual Studio Service Pack 1 Beta.

Beware, this is BETA software and as happened to me may completely hose your setup. However, I managed to solve my problem, but don’t install this lot on a production machine! YOU HAVE BEEN WARNED!

Ok, that about covers it for the software.

Knowledge pre-requisites.

What do you need to know? Well, obviously you’ll need a good understanding of C#, and WPF.

If you really want to grasp how to write shaders from scratch you’ll need to have a reasonable understanding of HLSL, and a good grounding in how the video hardware operates wouldn’t go a miss. Since we (probably) won’t be dealing with vertex shaders understanding much of the 3d stuff associated with rendering isn’t really needed, however, as always, it’s useful to grasp what’s going on under the hood.

Here are a few good links to get you started.

Writing Pixel Shaders on MSDN.

Reference for HLSL

Greg Shechters blog

Implementing Lighting Models with HLSL on Gamasutra

Remember, since many of the graphics techniques we’re now using are getting into the domain of games development, you could do worse than hit up the games dev sites and boards. nVidia has a reasonably active HLSL/shader development forum for example.

Finally if you really want to know about shaders there are some great books on amazon :

Shader X6: Advanced Rendering (Shaderx)
by Wolfgang Engel

Read more about this book…

Shaders for Game Programmers and Artists (Premier Press Game Development)
by Sebastien St-Laurent

Read more about this book…

and finally, older, but still immensely usefull

The COMPLETE Effect and HLSL Guide
by Sebastien St-Laurent

Read more about this book…

Its well worth searching out articles by these authors on line too.

Anyway, talk is cheap, show me the code!

Pixel Shaders 101

For this article, we’ll write a simple desaturation shader. This will allow you to show any WPF element as disabled, rather than having to fiddle with animating colours, or producing different controls (or worse still bitmaps!) to show in disabled states. I’d previously written a software version of this shader, but being in software (managed code at that!) was quite slow, so hopefully these hardware assisted versions will boost performance and make them usable across larger areas.

Fist, we’ll take a look at getting our environment set up.

For this we’ll need to configure one of devstudios external tools to call Microsoft’s HLSL compiler fxc.exe, installed with the DirectX SDK.

ExternalToolSetup

Here we have a new external tool set to call FXC.EXE from the Tools menu inside Devstudio. This means that when we have our Shader highlighted in the Solution explorer, we can execute the tool, and have it compiled into a shader without leaving the environment. Although not as integrated as a nice build step, at least we dont have to shell out, and hopefully Greg’s boys will help us out there.

For the Command, locate the FXC.EXE executable in the DirectX SDK install folder. Since I used the default install options for my DirectX SDK, my FXC is in the following path :

C:Program Files (x86)Microsoft DirectX SDK (March 2008)UtilitiesBinx64fxc.exe

Next we need to add the arguments to make FXC compile our shader. My arguments look like :

/DWPF /Od /T ps_2_0 /E $(ItemFileName)PS /Fo$(ProjectDir)$(ItemFileName).ps $(ItemPath)

You can see the parameters that FXC takes by going to the command line and typing fxc. For my arguments we have

  • /DWPF
    Defines the manifest constant "WPF" inside the shader file. We can use this to determine which bits of code in the shader file we should compile for inclusion in a WPF project as opposed to use in the FXComposer file. We’ll get back to this shortly.
  • /T ps_2_0
    causes the compiler to compile with the shader profile 2.0
  • /E $(ItemFileName)PS
    This sets the entry point for our shader. Here I concatenate the .Fx’s filename with PS, so inside our shader code, the entry point for WPF would look like

    DesaturatePS

  • /Fo$(ProjectDir)$(ItemFileName).ps
    Causes the output of FXC to be places into the projects folder, alongside the classes which will load up the compiled bytecode.

That about covers the command line arguments for when we execute the tool from within devstudio.

Next we’ll fire up FX Composer and start to develop the shader.

My Fx Composer looks like this :

fxComposer

Here we can see the finished shader, desaturating the standard Utah Teapot provided inside fx composer.

I wont go into an exhaustive breakdown of how to use FX Composer, its quite a complex tool, and we only need to use a bare minimum of its tool set to get what we want.

If you’ve downloaded the solution file accompanying this article, go along and open up the fx composer project located in the Shaders sub folder.

This should load up the code for the shader, and some pre set materials that i’d applied to the teapot so we can test our work.

Here are a few pointers :

  • Use the mouse in the Render window to orient the objects using ALT-Left Click to rotate, and the mouse wheel to zoom in and out.
  • The render windows tool bar contains reset buttons if you get in a mess! Click the object, and select the "Zoom Selected Object Extents" button.
  • On the main tool bar, click the Compile button to recompile and immediately visualise any changes you’ve made to the shader code.
  • In the materials window, click on a material (effect) to select, and then modify its parameters in the properties window (top right). For the Desaturate effect, there is a Saturation silder which desaturates the teapot in real time.
  • You can right click in the materials window to create new shaders from a few default ones, to play with an modify. After adding a new shader, simply click and drag it over to the render window to apply to an object or the whole scene. Be careful with post-process shaders that don’t correctly transform the object. You’ll end up with a big blotch of messed up teapot. Press Ctrl-Z to undo the shader application, or select the shader in the materials window and press delete.

Our shader is written to take advantage of FX Composer, and because we can pass in manifest constants we can use exactly the same shader code to pass to the FXC compiler. This means we get the best of both worlds, with a minor inconvenience of passing in a few command line parameters!

Without delving into the bowels of the other parts of the shader code in FX Composer there are a couple of things worth pointing out.

   1: #ifndef WPF
   2: float Saturation <
   3:     string UIWidget = "slider";
   4:     float UIMin = 0.0f;
   5:     float UIMax = 1.0f;
   6:     float UIStep = 0.01f;
   7:     string UIName = "Saturation";
   8: > = 0.5f;
   9: #else
  10: float Saturation : register(c0);
  11: #endif

here we see what we use the /DWPF for in the arguments to the command line compiler. We perform conditional compilation of one of the shaders parameters.

For FX Composer, this is simply defined as a float (with a UI element that shows up in the property window), however, when we compile inside devstudio, we provide a HLSL semantic that ensures our saturation comes from a specific register , which happens to be the one we set up in the C# wrapper class’s dependency property.

we also do the same with the incoming texture data sampler :

   1: #ifndef WPF
   2: sampler2D implicitSampler = sampler_state { 
   3:     texture = implicitTexture; 
   4:     AddressU = Clamp; 
   5:     AddressV = Clamp; 
   6:     MagFilter = Linear; 
   7:     MipFilter = POINT; 
   8:     MinFilter = LINEAR; 
   9:     MagFilter = LINEAR; 
  10: };
  11: #else
  12: sampler2D implicitSampler : register(s0);
  13: #endif

inside the C# wrapper class, the implicit texture described on Gregs blog is passed in the s0 register. Here, if we’re not inside the C# wrapper class, we use a texture set up inside FX Composer to cover the render window, so we can see the results of our effect immediately.

Finally we get onto the actual pixel shader code

   1: float4 DesaturatePS(float2 uv : TEXCOORD0) : COLOR {
   2:     float3  LuminanceWeights = float3(0.299,0.587,0.114);
   3:     float4    srcPixel = tex2D(implicitSampler, uv);
   4:     float    luminance = dot(srcPixel,LuminanceWeights);
   5:     float4    dstPixel = lerp(luminance,srcPixel,Saturation);
   6:     //retain the incoming alpha
   7:     dstPixel.a = srcPixel.a;
   8:     return dstPixel;
   9: }

First, look how tiny the code is! Add to this the fact that its executed in massively parallel hardware, this means we’ll easily be able to execute these shaders at the screen level with little or no visible performance hit.

Here’s a quick breakdown of what’s happening line by line.

  1. Shader declaration – takes a TEXCOORD (another HLSL semantic) and returns a float4 : COLOR.
  2. declare a float3 that represents the conversion factors to greyscale. These factors are widely recognised as the standard greyscale factors and are used both by NTSC and JPEG for grey conversions.
  3. extract a source pixel from the incoming texture sampler, using the pixels u,v coordinates. If you don’t know what u,v coordinates are, you’ll need to look up a bit of 3d math concerning texture coordinates, but basically they amount to the X,Y coordinate of a pixel in a texture.
  4. calculate the luminance by using the dot product of the source pixel, and the luminence factors.
  5. calculate the linear interpolation between the grey values (luminance) and the source pixel data (srcPixel) by the amount specified in the Saturation float. (wow, look at that all in one go!)
  6. comment!
  7. because we dont want to grey scale the alpha channel , we just take the incoming pixels alpha , and put it into the destination pixels alpha channel untouched.
  8. return the new pixel.

Simple!

Now we have a shader in FX Composer, we can edit the code, and recompile, immediately seeing the results of our compilation. Something that’s harder to do, and much less interactive in devstudio, especially if you’ve got your shaders in a large program, or one that takes a while to run.

That about covers the shader code inside FX Composer. Next into devstudio.

Shaders coding in Devstudio – Touring the code.

After loading up the shaders solution file, open up the FX file we’ve just been editing inside FX Composer. Note that the Pixel shaders name is named as same as the source file name with "PS" appended. This is so the command line in devstudio picks this up as the entry point to the code.

Next take a look at the DesaturateEffect.cs file. This is the wrapper file that loads up our pixel shader, and allows the C# code to interface and pass parameters to the shaders. This is the part that will most likely change post-beta, as some of the registers used to communicate are fixed, and we’re currently limited to passing in a single sampler which means that we cant do any multi-texturing effects YET 🙂

I wont cover this in great depth as there’s plenty already on Gregs blog. However the two main points are the dependency properties.

   1: public static readonly DependencyProperty SaturationProperty = 
   2:             DependencyProperty.Register("Saturation", typeof(double), typeof    
   3:     (DesaturateEffect),new UIPropertyMetadata(1.0, PixelShaderConstantCallback(0)));

The important part is PixelShaderConstantCallback(0) which maps directly to the code in the shader which links the saturation to the register :

  10: float Saturation : register(c0);

The same dependency property pattern is also used for the implicitSampler passed in register s0.

Those dependency property callbacks are the missing link which allows the C# to pass values into the shader code. Reading through gregs blog, this mechanism will probably change post-beta, so watch out!

To build the solution we must first compile the pixel shader.

Go to the solution explorer, and select (left click) the .fx file, Desaturate.fx. This tells the external tool which file it will be operating on.

Next, go to the tools menu and click on the tool we set up earlier to run FXC.exe on our select file.

If you don’t have the output window visible, show it (ctrl-alt-O) and you should see something like the following :

 

Microsoft (R) D3D10 Shader Compiler 9.19.949.2185
Copyright (C) Microsoft Corporation 2002-2007. All rights reserved.

compilation succeeded; see F:ActiveProjectsShaderEffectsDesaturateEffectDesaturate.ps

FXC has compiled our .fx source code, and put the resulting byte code in the project directory. This is set in the C# project file as a resource so it gets compiled into the class library.

Since this is an example, I’ve shamelessly stolen Gregs example code to get the packURI syntax, (which would have been the same anyway, so why re-write it?!). Obviously this would be much more robust if we were writing a production app.

Next, build the whole solution which should now compile both the shader library, and the test application.

Once that is complete we’re good to go. Run the test application (it should be set as the startup application) , and if all has gone well, you should see the window below with its colours fading in and out as the effect desaturates the window.

shader

Note that you should be able to scale this up to full screen with no loss of performance.

Enjoy.

Download the solution files

This entry was posted in General. Bookmark the permalink.

6 Responses to Pixel Shader Effects in WPF using .Net3.5sp1 Beta

  1. Robert says:

    Also here are the images broken 🙁

    Forbidden
    You don’t have permission to access /rob/wp-content/uploads/2008/05/shader.png on this server.

    Additionally, a 404 Not Found error was encountered while trying to use an ErrorDocument to handle the request.

  2. rob says:

    Hi Robert!
    That’s kind of odd, there are no restrictions on the images, and the page access count is quite high, i would have thought there’d have been more comments about broken images if that were the case. Maybe its something on your route that’s restricting access?

  3. Eric Rodewald says:

    Really liked this article. Touched on a few things the others haven’t been.. like up the command as an External Tool is very helpful.

  4. rob says:

    Thanks Eric,
    I’m completely snowed under on the current WPF project at work, so i haven’t been around on line much, but when i do get back , one of the first things i intend playing with is the final version of pixel shader API. I’ve had a quick look and there still seems to be some limitations but I’ll be better placed to play with it when im not working 18 hour days!
    Thanks for reading!
    Rob

  5. SeveQ says:

    Hi there,

    can you tell me why I get the following error when I try to compile the shader with Composer 2.5 (not beta)?

    Error Error: Unrecognized RHS value in assignment: ‘implicitTexture’ XXXDesaturate.fx 25

    Error Error: ID3DXEffectCompiler: Error in type checking XXXDesaturate.fx -1

    Error Error: ID3DXEffectCompiler: There was an error initializing the compiler XXXDesaturate.fx -1

    Here is my shader code:

    /*

    % Description of my shader.
    % Second line of description for my shader.

    keywords: material classic

    date: YYMMDD

    */
    #ifndef WPF
    float Saturation = 0.5f;
    #else
    float Saturation : register(c0);
    #endif

    #ifndef WPF
    sampler2D implicitSampler = sampler_state {
    texture = implicitTexture;
    AddressU = Clamp;
    AddressV = Clamp;
    MagFilter = Linear;
    MipFilter = POINT;
    MinFilter = LINEAR;
    MagFilter = LINEAR;

    };
    #else
    sampler2D implicitSampler : register(s0);
    #endif

    float4 DesaturatePS(float2 uv : TEXCOORD0) : COLOR {
    float3 LuminanceWeight = float3(0.299, 0.578, 0.114);
    float4 srcPixel = tex2D(implicitSampler, uv);
    float luminance = dot(srcPixel, LuminanceWeight);
    float4 dstPixel = lerp(luminance, srcPixel, Saturation);

    dstPixel.a = srcPixel.a;

    return dstPixel;
    }

    Thanks a lot!

  6. SeveQ says:

    Okay, I have found the problem… code was incomplete.

Leave a Reply