Click here to Skip to main content
65,938 articles
CodeProject is changing. Read more.
Articles
(untagged)

WPF Bitmap Effects using Pixel Shaders .Net3.5sp1 Beta

0.00/5 (No votes)
27 May 2008 1  
How to write bitmap effects using the latest classes available in the .Net3.5 Sp1 beta.

Introduction

This article will introduce an alternative development method for hardware assisted Bitmap Effects using the graphics cards hardware pixel shaders. This article and all the associated code, and many other articles on C# and WPF are available on my blog here

Hopefully you'll enjoy my first code project article! Be Gentle! ;)

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. People have come to recognise UI elements that are grey as disabled, and deviating from this standard is sure to confuse. Using a pixel shader will allow you to show any WPF element as disabled, rather than having to fiddle with animating colours towards grey, 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. Grey-ing out your entire applications window (think of the windows XP shutdown screen that greys out your desktop) will now be possible using pixel shader effect such as these.

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

Environment Setup

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.

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)\Utilities\Bin\x64\fxc.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 :

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.

Firstly, the conditional compilation of the Shaders only input variable.

#ifndef WPF
float Saturation <
    string UIWidget = "slider";
    float UIMin = 0.0f;
    float UIMax = 1.0f;
    float UIStep = 0.01f;
    string UIName = "Saturation";
> = 0.5f;
#else
float Saturation : register(c0);
#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 :

#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 

Then, inside the C# wrapper class, the implicitBrush described on Gregs blog is passed in the sampler register s0. 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

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

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.

Shader coding in Devstudio - the C# support code

After loading up the shaders solution file, open up the FXComposer project file located in the Shaders Sub folder 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 won't cover this in great depth as there's plenty of information about the C# support code on Gregs blog. However I will outline the two main points - the dependency properties used to connect the Shader registers with the C# properties.

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

The important part is PixelShaderConstantCallback(0) which maps directly to the code in the shader file , which links the Saturation property to the register C0 -

float Saturation : register(c0); 


The same dependency property pattern is also used for the sampler (the implicitBrush) 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!

Building the code

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

Using our custom tool we set up earlier , 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 selected 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:\ActiveProjects\ShaderEffects\DesaturateEffect\Desaturate.ps

FXC.exe 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 assembly.

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.

Note that you should now be able to stretch the window to full screen whilst the pixel shader bitmap effect is in operation, with little or no performance hit.

Pixel shaders ROCK!.

If you have any comments, corrections or suggestions this article and more are available from my blog at

http://rob.runtothehills.org

Enjoy!

Rob

License

This article has no explicit license attached to it but may contain usage terms in the article text or the download files themselves. If in doubt please contact the author via the discussion board below.

A list of licenses authors might use can be found here