WPF Reflection Effect

This article shows a tutorial on how to create a WPF pixel shader effect that reflects a user interface element.

Introduction

Recently, I was trying to make some controls in my user interface more attractive by creating a reflection of the controls. I’ve searched over the Internet and found some useful sites. This article by Gill Cleeren shows how to reflect an image by binding it to the Visual property of a VisualBrush object, applying a ScaleTransform to flip it and applying the brush as the background of a Border object. The image can also be skewed to give a 3D impression. This is a nice start but I want something that can be easily reused. Also, this method creates another control, which takes up some space. If you use it in a panel, this could ruin your layout if you assume that the “effect” is rendered in a different layer.

I also saw this article by Steve Li that shows a reflection shader effect for use in Silverlight. I believed that with a few tweaks, this can also be used in WPF. It’s straight to the point and gets the job done. However, I also wanted to add some things like the reflection having some angle. Moreover, I want to write an article that can be easily understood by beginners, such as me.

Getting Started

WPF pixel shader effects became available with .NET 3.5 SP1, so you need to install this to run the shader effect. To create one, we need Visual Studio and a compiler for our High Level Shading Language (HLSL) code, which is found in the DirectX SDK. These are basically all we need. However, Greg Schechter and Gerhard Schneider wrote a BuildTask to compile the shaders directly in Visual Studio and a project template specifically for creating WPF shader effects. This also removes the need to install the DirectX SDK, since they also included a compiler. You can download the Shader Effects BuildTask and Templates.zip from the Codeplex site. Just follow the instructions on how to install. On my machine, I have both DirectX SDK and the BuildTask. Once you installed the BuildTask and the project template, let’s create a project of type WPF Shader Effect Library. When prompted, choose the option to load the project normally.



Figure 1. Shader Effects Project

As you can see, three files have been automatically created. Effect1.cs contains the managed effect class. Effect1.fx contains the HLSL code. EffectLibrary.cs contains a helper class. Let’s rename Effect1 to ReflectionEffect. Let’s first check the HLSL code in ReflectionEffect.fx.

The HLSL Code

First of all, I’m no game programmer so HLSL is quite new to me, although the syntax is quite similar to the C programming language. I have to check the DirectX documentation every now and then to figure out some things. To better understand how pixel shaders work, let’s check the HLSL code template provided in the generated FX file.

float4 colorFilter : register(C0);
sampler2D implicitInputSampler : register(S0);

float4 main(float2 uv : TEXCOORD) : COLOR
{

float4 color = tex2D(implicitInputSampler, uv);
return color * colorFilter;
}


Listing 1. Sample HLSL Code

The first variable defined here is colorFilter which is of type float4. The float4 type is a vector type. A vector can contain one to four scalar components. In the case of float4, it means that the variable is a vector that contains 4 floating-point values. The value in float constant register 0 will be assigned to the colorFilter variable. This register may contain up to 4 floating-point values. A value is put in the register by explicitly assigning a dependency property (in the ShaderEffect class) to a specific register number. We can see this later.

The next variable, implicitInputSampler, is of type sampler2D. To make it simple, we can say that the sampler contains a reference to the bitmap representation of the UI element where the effect is applied. Normally, the bitmap is the texture to be applied to an input image. However, in this case, we are using the texture as the input image to our pixel shader. We will get the value of the sampler from the sampler register 0.

Next, we have the main function. This function accepts a parameter of type float2. This parameter contains the current texture coordinate (u, v). The TEXCOORD keyword is a semantic that indicates that the input parameter is a texture coordinate. A semantic is used for input and output parameters that are passed between shaders. This indicates what the intended use of a parameter is. The function returns the output color for the particular coordinate. Inside the function, we get a color value from the texture at the coordinate (u, v) using the function tex2D. The reason why (u, v) is used instead of (x, y) is that that default range of the coordinates is from 0.0 to 1.0. Some mapping is done behind the scenes.

With this bit of information, let’s try to create the reflection pixel shader. By default, we can reflect the upper half of the element where the effect will be applied. We can easily get a texture pixel (texel) as illustrated in Listing 1. What we are interested in now is how to get a texel from a different location.

sampler2D implicitInputSampler : register(S0);

float4 main(float2 uv : TEXCOORD) : COLOR
{
if (uv.y > 0.5)
{
uv.y = 1.0 - uv.y;
return tex2D(implicitInputSampler, uv);
}

return tex2D(implicitInputSampler, uv);
}

Listing 2. Reflection Pixel Shader Code

The preceding code snippet shows a modified pixel shader that reflects the top half of an element. As you can see here, we just subtracted the y-component from 1.0 if the y-component is greater than 0.5. To test our pixel shader even without creating the managed effect class, we can use Shazzam, a shader effect tester made by Walt Ritscher. You can download it on this link. Here is a screenshot of Shazzam showing our pixel shader before it is applied. Using Shazzam, you can compile and apply the shader on a sample image, assuming you already installed DirectX. Another nice feature of this application is that if automatically generates the corresponding managed effect class for both C# and Visual Basic.



Figure 1. The Shazzam Shader Effect Tester

You can easily see the menu items for compiling and applying the shader on the sample image. The following shows the image after application of the effect.



Figure 2. Reflection Shader Applied to an Image

Ok, half of the image was replaced. Most of the time, we would not want this behavior. What we want is that the whole image be reflected. However, this can’t be done using pixel shader alone. We have to create the WPF managed effect class.

WPF Shader Effect

To use the pixel shader in a WPF application, we need to create a custom ShaderEffect class. The following code snippet shows the ReflectionEffect class. The codes here are automatically generated when the project is created. I just removed the ColorFilter property since this is not needed in our pixel shader.

public class ReflectionEffect : ShaderEffect
{
#region Constructors

static ReflectionEffect()
{
_pixelShader.
UriSource = Global.MakePackUri("ReflectionEffect.ps");
}

public ReflectionEffect()
{
this.PixelShader = _pixelShader;

// Update each DependencyProperty that's registered with a shader register. This
// is needed to ensure the shader gets sent the proper default value.
UpdateShaderValue(InputProperty);
}

#endregion

#region Dependency Properties

public Brush Input
{
get {
return (Brush)GetValue(InputProperty); }
set {
SetValue(InputProperty, value); }
}

// Brush-valued properties turn into sampler-property in the shader.
// This helper sets "ImplicitInput" as the default, meaning the default
// sampler is whatever the rendering of the element it's being applied to is.
public static readonly DependencyProperty InputProperty =
ShaderEffect.RegisterPixelShaderSamplerProperty("Input", typeof(ReflectionEffect), 0);

#endregion

#region Member Data

private static PixelShader _pixelShader = new PixelShader();

#endregion
}

Listing 3. Reflection Shader Effect Class

In the constructor, the PixelShader property is set to an instance of a PixelShader object. In the static constructor, the UriSource of the PixelShader object is set to the PS file generated when the FX file is compiled. The static method MakePackUri of the Global helper class returns the URI of the PS file. You can see that we have an Input dependency property. This is mapped to the implicitInputSampler variable in our pixel shader. Since this property is accessible, we can actually set the input to another element instead of the element where the effect is set.

Going back to our problem, we need the whole element to be reflected. The ShaderEffect class contains the following protected properties: PaddingLeft, PaddingTop, PaddingRight, and PaddingBottom. By setting the padding, we can increase the output area of the pixel shader. If we set the PaddingBottom property to the height of the element, then we’ll have the whole element reflected. Our problem now is how to get the height of the element where the effect is applied. Unfortunately, we don’t have a way to get the height of the element implicitly. We have to specify the height ourselves so we need to create a dependency property.

public double InputHeight
{
get {
return (double)GetValue(InputHeightProperty); }
set {
SetValue(InputHeightProperty, value); }
}

public static readonly DependencyProperty InputHeightProperty =
DependencyProperty.
Register("InputHeight", typeof(double), typeof(ReflectionEffect),
new UIPropertyMetadata(
new PropertyChangedCallback(
(DependencyObject o, DependencyPropertyChangedEventArgs e) =>
{
((ReflectionEffect)o).PaddingBottom = (
double)e.NewValue;
}
)
)
);


Listing 4. The Input Height Dependency Property

On the PropertyChangedCallback method, we will just assign the new value of the InputHeight to the PaddingBottom property of the ReflectionEffect instance. Let’s try using this in a sample WPF application. The following XAML shows how to use the effect on an image.

<Image x:Name="MyImage" Source="Box.png" Stretch="None" HorizontalAlignment="Center" VerticalAlignment="Center">
<Image.Effect>
<effects:ReflectionEffect
InputHeight="{Binding ElementName=MyImage, Path=ActualHeight}"/>
</Image.Effect>
</Image>

Listing 5. Using the WPF Shader Effect

Here, we bounded the InputHeight property to the ActualHeight property of the image. The following figure shows the image and the image’s reflection.



Figure 3. Reflection Effect on an Image

I’ve changed the pixel shader code a bit so that the opacity seems to be lower than the original image. This is obtained by multiplying the y-coordinate to the returned color. Due to this, the reflection becomes lighter as it goes deeper. As you can see, the effect is really meant for 2D elements or images. Since the box shown in the figure is usually viewed as a 3D object, we get the impression that the reflection of the box is wrong. We can change the pixel shader code so that the reflection becomes correct to some extent.

Adjusting the Reflection

What we want to do with the reflection of the box image is to adjust the sides so that it gives us the impression of a reflected 3D image. What I thought of is to add four properties to our reflection effect: CenterX, LeftAngle, RightAngle, and Height. To understand it more clearly, here’s the box image shown in Figure 3, with some labels drawn on it.



Figure 4. Reflection Effect Properties

The idea is to adjust the pixels upward to a certain height depending on the angle. The reason why we have to specify the x-coordinate of the center of the box is that it may not be always be at the center of the image. We also need to specify the height of the box so that the reflection won’t include the top of the box. Valid values for the CenterX and Height properties range from 0 to 1, to make the pixel shader coding easier. The valid values for LeftAngle and RightAngle properties range from 0 to 90 degrees. Here is the updated pixel shader code.

float centerX : register(C0);
float leftAngle : register(C1);
float rightAngle : register(C2);
float height : register(C3);

sampler2D implicitInputSampler : register(S0);

float4 main(float2 uv : TEXCOORD) : COLOR
{
float radiansMultiplier = 3.141592 / 180;
float edge = 0.5;

if (uv.x >= centerX)
{
edge -= (tan(rightAngle * radiansMultiplier) * (uv.x - centerX));
}
else if (uv.x < centerX)
{
edge -= (tan(leftAngle * radiansMultiplier) * (centerX - uv.x));
}

if (uv.y > edge && uv.y <= edge + height)
{
uv.y = edge - (uv.y - edge);
return tex2D(implicitInputSampler, uv) * uv.y;
}

return tex2D(implicitInputSampler, uv);
}

Listing 6. Updated Pixel Shader Code

The idea here is to calculate the edge of the box so that we can get the correct texel to reflect. We can get this by using a bit of trigonometry. We get the opposite side of a triangle by multiplying the tangent of an angle and the adjacent side. The pixel shader variables are mapped to the following dependency properties in the managed effect class.

public ReflectionEffect()
{
this.PixelShader = _pixelShader;

// Update each DependencyProperty that's registered with a shader register. This
// is needed to ensure the shader gets sent the proper default value.
UpdateShaderValue(InputProperty);
UpdateShaderValue(CenterXProperty);
UpdateShaderValue(LeftAngleProperty);
UpdateShaderValue(RightAngleProperty);
}

public double CenterX
{
get {
return (double)GetValue(CenterXProperty); }
set {
SetValue(CenterXProperty, value); }
}

public static readonly DependencyProperty CenterXProperty =
DependencyProperty.
Register("CenterX", typeof(double), typeof(ReflectionEffect),
new UIPropertyMetadata(0.5, PixelShaderConstantCallback(0)));

public double LeftAngle
{
get {
return (double)GetValue(LeftAngleProperty); }
set {
SetValue(LeftAngleProperty, value); }
}

public static readonly DependencyProperty LeftAngleProperty =
DependencyProperty.
Register("LeftAngle", typeof(double), typeof(ReflectionEffect),
new UIPropertyMetadata(0.0, PixelShaderConstantCallback(1)));

public double RightAngle
{
get {
return (double)GetValue(RightAngleProperty); }
set {
SetValue(RightAngleProperty, value); }
}

public static readonly DependencyProperty RightAngleProperty =
DependencyProperty.
Register("RightAngle", typeof(double), typeof(ReflectionEffect),
new UIPropertyMetadata(0.0, PixelShaderConstantCallback(2)));

public double Height
{
get {
return (double)GetValue(HeightProperty); }
set {
SetValue(HeightProperty, value); }
}

public static readonly DependencyProperty HeightProperty =
DependencyProperty.
Register("Height", typeof(double), typeof(ReflectionEffect),
new UIPropertyMetadata(0.0, PixelShaderConstantCallback(3)));

Listing 7. Updated Reflection Effect Class

To better understand the code, I created a simple demo application that lets us change the values of the properties. It includes four sliders, where each slider value corresponds to a dependency property of the reflection effect. The following code shows the content of the main window.

<StackPanel>

<Grid
Height="300"
Width="300">

<Image x:Name="MyImage" Source="Box.png" Stretch="None" HorizontalAlignment="Center" VerticalAlignment="Top">
<Image.Effect>
<effects:ReflectionEffect
InputHeight="{Binding ElementName=MyImage, Path=ActualHeight}"
CenterX="{Binding ElementName=CenterXSlider, Path=Value}"
LeftAngle="{Binding ElementName=LeftAngleSlider, Path=Value}"
RightAngle="{Binding ElementName=RightAngleSlider, Path=Value}"
Height="{Binding ElementName=HeightSlider, Path=Value}"/>
</Image.Effect>
</Image>

</Grid>

<Grid>
<Grid.RowDefinitions>
<RowDefinition/>
<RowDefinition/>
<RowDefinition/>
<RowDefinition/>
</Grid.RowDefinitions>
<Grid.ColumnDefinitions>
<ColumnDefinition/>
<ColumnDefinition/>
<ColumnDefinition/>
</Grid.ColumnDefinitions>

<Label Content="Center X:"/>
<TextBlock Grid.Column="1" Text="{Binding ElementName=CenterXSlider, Path=Value}"/>
<Slider x:Name="CenterXSlider" Grid.Column="2" Minimum="0" Maximum="1" Value="0.5"/>

<Label Grid.Row="1" Content="Left Angle:"/>
<TextBlock Grid.Row="1" Grid.Column="1" Text="{Binding ElementName=LeftAngleSlider, Path=Value}"/>
<Slider x:Name="LeftAngleSlider" Grid.Row="1" Grid.Column="2" Minimum="0" Maximum="90" Value="0"/>

<Label Grid.Row="2" Content="Right Angle:"/>
<TextBlock Grid.Row="2" Grid.Column="1" Text="{Binding ElementName=RightAngleSlider, Path=Value}"/>
<Slider x:Name="RightAngleSlider" Grid.Row="2" Grid.Column="2" Minimum="0" Maximum="90" Value="0"/>

<Label Grid.Row="3" Content="Height:"/>
<TextBlock Grid.Row="3" Grid.Column="1" Text="{Binding ElementName=HeightSlider, Path=Value}"/>
<Slider x:Name="HeightSlider" Grid.Row="3" Grid.Column="2" Minimum="0" Maximum="1" Value="1"/>
</Grid>

</StackPanel>

Listing 8. Main Window XAML Code

Once the value of a slider changes, it automatically updates the value of the corresponding property of the effect. The values are also displayed so that you can see what values you need to create a reflection of the image or element, when you’re about to use the reflection effect in your WPF application.



Figure 5. Demo Application

As I said before, the reflection effect can’t cover all scenarios since we are only working on 2D images. You can download the Visual Studio 2008 solution here.

By Michael Detras   Popularity  (9725 Views)
Biography - Michael Detras
.NET developer. Interested in WPF, Silverlight, and XNA.
My blog