Saturday, July 25, 2009

Using GPU-Accelerated shader effects in WPF

by Svetoslav Savov

With Service Pack 1 of .NET 3.5 the use of effects in WPF was significantly improved. The old so called “bitmap effects” were using the CPU to render the visual elements. As a result, when a BitmapEffect is used, the whole visual tree of the element on which it’s applied becomes software rendered. As you can imagine, this dramatically decreases the performance of the user interface, especially when you add animations or 3D scenes. The new effects are using the DirectX shader system and they are rendered using the graphical processor which is a lot faster when it comes to calculating pixels, and also it relieves the CPU of the time-consuming effect operations so it is free to perform other tasks. With Effects you can combine the programmability of the GPU and the flexible environment of WPF to create rich user interfaces with animations, data binding and 3D scenes.

What do you need?

For developing a WPF application with shader effects you need several things:

  • .NET 3.5 SP1 (of course :) )

http://www.microsoft.com/downloads/details.aspx?FamilyID=ab99342f-5d1a-413d-8319-81da479ab0d7&displaylang=en

  • DirectX SDK for the development stage

http://www.microsoft.com/downloads/details.aspx?FamilyID=ea4894b5-e98d-44f6-842d-e32147237638&displaylang=en

  • A tool to create the effects

The Shazzam Tool is a great tool for this - http://shazzam-tool.com

Getting started with Shazzam

First of all we can take a look at Shazzam. It comes with some sample shaders we can use for learning.

Start Shazzam and go to Shader Loader -> Sample Shaders.

You can see the shader’s FX file, and a sample image which will be used to review and test the effect. Click on Tools->Compile Shader or press F7. Then click on Tools->Apply Shader (F5). When the shader is compiled you can see that Shazzam generated a C# class and a Visual Basic class that wrap the effect’s properties.

Also the Change Shader Settings page appeared. There you can see all the changeable properties of the effect and see in real time what happens when you change a property. To test the effect you can use one of the built-in sample images or you can select another.

Let’s try something out. Select the Pixelate.fx shader from the samples. Choose the first sample image (Sample1). Compile (F7) and Apply (F5) the effect. What happened? The image seems to have disappeared. That’s because the properties of the shader have default values of zero, and that just doesn’t work for our effect.


In the settings page, you can see some text boxes and sliders. They are used for changing the properties of the effect. First of all, you can see that there are a Min and a Max boxes with default values of 0 and 1. We have to change them to actually see the effect working. That’s because the system cannot know what the property is being used for, it doesn’t make any sense, it’s just a value. So the developers of Shazzam decided that the default values can be from 0 to 1. But in the case of Pixelate effect, the parameter values are in pixels. So we need to change them to 0-600 for both Horizontal pixel counts and Vertical pixel counts. And now when we move the sliders, we change the values from 0 to 600 instead of from 0 to 1. Now it makes sense.

So let’s change the values to about 100.


As we move the slider we can actually see what’s happening in the image above.


You can try some other effects from the samples, or browse the internet for third-party shaders. There are some nice video tutorials on Shazzam website, http://shazzam-tool.com.

Using effects in a WPF application

Adding the effect to a WPF project

The integration of a shader effect in WPF application is a relatively simple task.

First of all, it’s probably a good idea to add the compiled shader file to our WPF project. In Shazzam, go to the Tools menu and click on View Compiled Shaders. There are all the effects that we have compiled using Shazzam. In Visual Studio, select the project and right-click on it, choose Add New -> Existing Item and select the compiled effect file (the files are with .ps extension). Right click on the file in Solution Explorer and select Properties. Set the Build Action parameter to Resource.


Create a new code file, go to Shazzam, copy the auto-generated class code, and paste it in the new code file. You will see some dependency properties, and a constructor. So let’s take a look at the constructor.

public AutoGenShaderEffect(PixelShader shader) {

// Note: for your project you must decide how to use the generated ShaderEffect class (Choose A or B below).

// A: Comment out the following line if you are not passing in the shader and remove the shader parameter from the constructor

PixelShader = shader;

// B: Uncomment the following two lines - which load the *.ps file

// Uri u = new Uri(@"pack://application:,,,/bandedswirl.ps");

// PixelShader = new PixelShader() { UriSource = u };

// Must initialize each DependencyProperty that's affliated with a shader register

// Ensures the shader initializes to the proper default value.

this.UpdateShaderValue(InputProperty);

this.UpdateShaderValue(HorizontalpixelcountsProperty);

this.UpdateShaderValue(VerticalpixelcountsProperty);

}

It’s very well commented and it practically tells us what to do. As we can see, there are two cases for using the effect class. First one is to pass the pixel shader as a parameter to the constructor. The second one is to create the pixel shader directly there. In this example, we choose option B – create the PixelShader object directly in the constructor. So we remove the line

PixelShader = shader;

and uncomment the lines for creating the object. Also It’s a good idea to rename the class so that we can recognize it later. The default name is AutoGenShaderEffect which is not very intuitive. But when you change the name of the class, don’t forget to change the owner type of the dependency properties.

So with all the changes our class looks something like this:

public PixelateShaderEffect()

{

Uri u = new Uri("/PixelShaders;component/EffectPS/Pixelate.ps", UriKind.Relative);

PixelShader = new PixelShader() { UriSource = u };

this.UpdateShaderValue(InputProperty);

this.UpdateShaderValue(HorizontalpixelcountsProperty);

this.UpdateShaderValue(VerticalpixelcountsProperty);

}

And that’s it. We now have a class that represents the pixel shader effect.

Using the effect in user interface

Now that we have the effect created in our project, we finally can see it in action in our user interface. Let’s create some demo interface, nothing special, just to see the effect.

I created something like this, just for fun:


And here’s the XAML code:

<Grid>

<Grid.ColumnDefinitions>

<ColumnDefinition Width="100" />

<ColumnDefinition />

Grid.ColumnDefinitions>

<StackPanel>

<Button x:Name="btnTest1" Content="Button One" Margin="2"/>

<Button x:Name="btnTest2" Content="Hack NASA" Margin="2"/>

<Button x:Name="btnTest3" Content="Go to the moon" Margin="2"/>

<Button x:Name="btnTest4" Content="Crash app" Margin="2"/>

StackPanel>

<TextBlock Grid.Column="1" Text="Some programming languages are so good that a program written with them can go through an infinite loop for about ten minutes on really fast computers."

TextWrapping="Wrap" Margin="10" Foreground="Blue"/>

<Canvas Grid.Column="1" Width="100" Height="100">

<Ellipse Width="100" Height="100" Fill="Yellow" Stroke="Black"/>

<Ellipse Width="25" Height="25" Fill="Black" Canvas.Left="20" Canvas.Top="20" />

<Ellipse Width="25" Height="25" Fill="Black" Canvas.Left="55" Canvas.Top="20" />

<Ellipse Width="60" Height="40" Fill="Black" Canvas.Left="20" Canvas.Top="45" />

<Ellipse Width="60" Height="36" Fill="Yellow" Canvas.Left="20" Canvas.Top="44" />

Canvas>

Grid>

So, let’s apply our effect.

Add a namespace reference to the shader class in the XAML code, something like this:

xmlns:sh="clr-namespace:Shazzam.Shaders"

Set the effect for the root grid:

<Grid.Effect>

<sh:PixelateShaderEffect

Horizontalpixelcounts="200"

Verticalpixelcounts="200"/>

Grid.Effect>

Now, we run the application and see what happens:


Nice! The effect was applied. That’s it. Simple, right?

Things to play with

Testing effects in real time in the application interface

Just for fun, I continued working on my shader effects demo application. I added two sliders that can control the effect, just like it was in Shazzam. Only, this works on a real application interface, so it’s more interesting.


<Grid>

<Grid.ColumnDefinitions>

<ColumnDefinition Width="40" />

<ColumnDefinition />

Grid.ColumnDefinitions>

<Grid.RowDefinitions>

<RowDefinition />

<RowDefinition Height="40"/>

Grid.RowDefinitions>

<Grid Grid.Column="1">

<Grid.ColumnDefinitions>

<ColumnDefinition Width="100" />

<ColumnDefinition />

Grid.ColumnDefinitions>

<StackPanel>

<Button x:Name="btnTest1" Content="Button One" Margin="2"/>

<Button x:Name="btnTest2" Content="Hack NASA" Margin="2"/>

<Button x:Name="btnTest3" Content="Go to the moon" Margin="2"/>

<Button x:Name="btnTest4" Content="Crash app" Margin="2"/>

StackPanel>

<TextBlock Grid.Column="1" Text="Some programming languages are so good that a program written with them can go through an infinite loop for about ten minutes on really fast computers."

TextWrapping="Wrap" Margin="10" Foreground="Blue"/>

<Canvas Grid.Column="1" Width="100" Height="100">

<Ellipse Width="100" Height="100" Fill="Yellow" Stroke="Black"/>

<Ellipse Width="25" Height="25" Fill="Black" Canvas.Left="20" Canvas.Top="20" />

<Ellipse Width="25" Height="25" Fill="Black" Canvas.Left="55" Canvas.Top="20" />

<Ellipse Width="60" Height="40" Fill="Black" Canvas.Left="20" Canvas.Top="45" />

<Ellipse Width="60" Height="36" Fill="Yellow" Canvas.Left="20" Canvas.Top="44" />

Canvas>

<Grid.Effect>

<sh:PixelateShaderEffect

Horizontalpixelcounts="{Binding ElementName=slHori, Path=Value}"

Verticalpixelcounts="{Binding ElementName=slVert, Path=Value}"/>

Grid.Effect>

Grid>

<Slider x:Name="slHori" Minimum="10" Maximum="500" Value="500" Grid.Row="1" Grid.Column="1"/>

<Slider x:Name="slVert" Minimum="10" Maximum="500" Value="500" Orientation="Vertical" />

Grid>

Animating the shader effects

Animating the effects is very simple. Just like any other animation in WPF.

<Window.Resources>

<Storyboard x:Key="effectStory" >

<DoubleAnimation Storyboard.TargetName="shaderEffect"

Storyboard.TargetProperty="Horizontalpixelcounts"

From="10" To="500" Duration="0:0:10" AutoReverse="True" RepeatBehavior="Forever"/>

<DoubleAnimation Storyboard.TargetName="shaderEffect"

Storyboard.TargetProperty="Verticalpixelcounts"

From="10" To="500" Duration="0:0:10" AutoReverse="True" RepeatBehavior="Forever"/>

Storyboard>

Window.Resources>

Other shader effects

Motion Blur:

<sh:DirectionalBlurShaderEffect Bluramount="0.005" Angle="45" />


Embossed

<sh:EmbossedShaderEffect Width="0.002" Amount="0.5" />


Download Sample

You can download the sample application here: Download Sample



No comments:

Post a Comment