XAML Playground
about XAML and other Amenities

Silverlight 3.0: Tile Effect with a Pixel Shader

2009-03-22T23:20:09+01:00 by Andrea Boschin

Listening what has been asked in the Silverlight forums I've found someone searching for a TileBrush. This kind of brush, in WPF can be used to create a Tile of repeated elements. Unfortunately the TileBrush exists in Silverlight but is only a placeholder ready for future development and for WPF compatibility.

In a thread I found to work around this missing thing. He suggested to create a Pixel Shader with Silverlight 3.0 to enable tiling. So I've decided to start studing Pixel Shaders to try create an Effect that may be used for this purpose. In this brief article I will get into this argument and explain how Pixel Shaders works and how they may be used in Silverlight 3.0.

What is a Pixel Shader

When the GPU (Graphic Processing Unit) are rendering a scene from a 3D model to its bidimensional representation it works in 3 steps. Starting from the 3D Model it go through Vertex Shaders, Pixel Shader and finally to the Frame Buffer. The first step called of "Vertex Shaders" involve the transformation of the 3D space and is out out the scope of this article. We now concentrate to the second step where Pixel Shaders start working.

To be most simpler, a Pixel Shader is a small program that transform an image pixel-by-pixel to another image that will be trasmitted to the Frame Buffer for the display. It is made of a function that will be called to transform every single pixel. The shader is written in an high level, c-like language called HLSL () and may take advantage of many primitive function to easily manipulate the pixels and create effects like bump mapping, brightness, distortion, etc...

Here is the code of a simple Pixel Shader:

   1: // .. omissis ..
   2:  
   3: float4 main(float2 uv : TEXCOORD) : COLOR
   4: {
   5:     return tex2D( implicitInputSampler, uv );
   6: }
   7:  
   8: // .. omissis ..

This code simply does nothing. The input value is used to return the color of the pixels so the Frame Buffer will receive the original picture. In this short chunk of code we may use subroutines, math functions and a type system optimized for working with images.

Here is another example:

   1: float4 main(float2 uv : TEXCOORD) : COLOR
   2: {
   3:    return tex2D( implicitInputSampler, 1 - uv );
   4: }

This shader do a reverse of the original image putting the top-left pixel on the bottom-right side and so-on. Using shader there are 32 registers where we can load some information coming from the software we are developing. "implicitInputSamples" in my example contains the input image to be transformed. Using this registers it is possible to parametrize a shader.

Working with shaders is not simple. Fortunately there is Shazzam, a wonderful click once WPF application you can download and that helps in developing the Pixel Shaders. This tool has been developed for WPF but it is useful also for Silverlight because it support the same version of Pixel Shaders 2.0 than WPF.

I think this is sufficent to show how powerful may be the Pixel Shaders. If you need some more samples please watch this . In the same page you may find a big number of shaders. I will stop here with the explanation because the argument is too extensive for the scope of this post. For a more detailed explanation please refer to .

Create a Tiling Pixel Shader

After understanding how a Pixel Shader works, we can now try to create our tiling shader as first step. While we get the pixels in our shader (a value from 0 to 1) there is a simple operation we can do to replicate the original image horizontally and vertically. Here is the operation:

x * tiles % 1

This simple operation will be repeated for x and y coordinates and get an argument "tiles" that define how many tiles we need to show. This value may be different for vertical or horizontal direction. We can try to write the shader now:

   1: //
   2: // Created by A.Boschin: andrea@boschin.it
   3: // site: http://www.silverlightplayground.org
   4: // blog: http://blog.boschin.it
   5: //
   6:  
   7: // vertical count of tiles
   8: float verticalTileCount : register(C1);
   9:  
  10: // horizontal count of tiles
  11: float horizontalTileCount : register(C2);
  12:  
  13: // inputBrush
  14: sampler2D implicitInputSampler : register(S0);
  15:  
  16: // Pixel Shader
  17:  
  18: float4 main(float2 uv : TEXCOORD) : COLOR
  19: {
  20:     float2 newUv = float2(uv.x * horizontalTileCount % 1, uv.y * verticalTileCount % 1);
  21:    
  22:     return tex2D( implicitInputSampler, newUv );
  23: }

As I have anticipated the shader get two arguments.

verticalTileCount : vertical number of tiles

horizontalTileCount : horizontal number of tiles

implicitInputSampler : the input image

If you are using Shazzam it will creates a class and a user interface to test the shaders. Using the sliders you will see the tiling appear. The numbers from 0 to 1 will transform the image to a biggest version, but the numbers bigger the 1 will transform the image.

Make our shader works in Silverlight

After created and tested the shader it is now time to use it in Silverlight 3.0. The first thing to do is compile the shader. Shazzam compile a shader in a .ps file and this is the file we have to copy in our solution. Next it is required to set the .ps file with a "Content" build action.

We have now to create an Effect that we may apply to every class derivated by UIElement. The effect can be applied to images or to elements like Buttons and every other element that supports the Effects. To use a Pixel Shader we have to create a class deriving from ShaderEffect. This class contains many functions and all the logic needed to incapsulate a compiled Pixel Shader and make it work.

The Effect class will have two dependency property wrapping vertical and horizontal. Using a Dependency Property will enable us to do DataBinding on this properties or to enable wonderful animated effects. Next we have to create a Sampler property. This is the property that encapsulate the input brush. Here is the code of the TileEffect:

   1: public class TileEffect : ShaderEffect
   2: {
   3:     public static DependencyProperty InputProperty =
   4:         ShaderEffect.RegisterPixelShaderSamplerProperty("Input", typeof(TileEffect), 0);
   5:  
   6:     public static DependencyProperty VerticalTileCountProperty =
   7:         DependencyProperty.Register(
   8:             "VerticalTileCount",
   9:             typeof(double),
  10:             typeof(TileEffect),
  11:             new PropertyMetadata(1.0, PixelShaderConstantCallback(1)));
  12:  
  13:     public static DependencyProperty HorizontalTileCountProperty =
  14:         DependencyProperty.Register(
  15:             "HorizontalTileCount",
  16:             typeof(double),
  17:             typeof(TileEffect),
  18:             new PropertyMetadata(1.0, PixelShaderConstantCallback(2)));
  19:  
  20:     public TileEffect()
  21:     {
  22:         // Note: for your project you must decide how to use the generated ShaderEffect class (Choose A or B below).
  23:         // B: Uncomment the following two lines - which load the *.ps file
  24:         Uri ps = new Uri("tile.ps", UriKind.RelativeOrAbsolute);
  25:         PixelShader = new PixelShader() { UriSource = ps };
  26:  
  27:         // Must initialize each DependencyProperty that's affliated with a shader register
  28:         // Ensures the shader initializes to the proper default value.
  29:         this.UpdateShaderValue(InputProperty);
  30:         this.UpdateShaderValue(VerticalTileCountProperty);
  31:         this.UpdateShaderValue(HorizontalTileCountProperty);
  32:     }
  33:  
  34:     /// <summary>
  35:     /// Gets or sets the input.
  36:     /// </summary>
  37:     /// <value>The input.</value>
  38:     public virtual System.Windows.Media.Brush Input
  39:     {
  40:         get
  41:         {
  42:             return ((System.Windows.Media.Brush)(GetValue(InputProperty)));
  43:         }
  44:         set
  45:         {
  46:             SetValue(InputProperty, value);
  47:         }
  48:     }
  49:  
  50:     /// <summary>
  51:     /// Gets or sets the vertical tile count.
  52:     /// </summary>
  53:     /// <value>The vertical tile count.</value>
  54:     public virtual double VerticalTileCount
  55:     {
  56:         get
  57:         {
  58:             return ((double)(GetValue(VerticalTileCountProperty)));
  59:         }
  60:         set
  61:         {
  62:             SetValue(VerticalTileCountProperty, value);
  63:         }
  64:     }
  65:  
  66:     /// <summary>
  67:     /// Gets or sets the horizontal tile count.
  68:     /// </summary>
  69:     /// <value>The horizontal tile count.</value>
  70:     public virtual double HorizontalTileCount
  71:     {
  72:         get
  73:         {
  74:             return ((double)(GetValue(HorizontalTileCountProperty)));
  75:         }
  76:         set
  77:         {
  78:             SetValue(HorizontalTileCountProperty, value);
  79:         }
  80:     }
  81: }

All the work is done in the constructor where we create a reference to the .ps file in the project and an istance of the PixelShader class. Then we have to initialize the properties to let the runtime read the values. This is all the work we need. The TileEffect now is ready to be embedded in Silverlight.

Using the TileEffect

If you had a try on how to use the DropShadowEffect you know exactly how to use the TileEffect. The only thing we have to do is to declase the namespace "local" before get access to the class. Then you add the effect in the markup code and it works:

   1: <UserControl x:Class="Elite.Silverlight3.PixelShaders.Silverlight.MainPage"
   2:     xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation" 
   3:     xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml" 
   4:     xmlns:local="clr-namespace:Elite.Silverlight3.PixelShaders.Silverlight"
   5:     Width="Auto" Height="Auto" Background="#FF000000">
   6:     <Grid x:Name="LayoutRoot" Background="#FF000000" HorizontalAlignment="Center" VerticalAlignment="Center">
   7:         <Image Grid.Column="0" Grid.Row="0" Source="parachute.jpg" Width="640" Height="426">
   8:             <Image.Effect>
   9:                 <local:TileEffect x:Name="tile" HorizontalTileCount="2" VerticalTileCount="2" />
  10:             </Image.Effect>
  11:         </Image>
  12:     </Grid>
  13: </UserControl>

The attached sample do something more. The effect properties has been binded to two sliders so moving the slider we can change the vertical and horizontal values of the effect.

Here is a screenshot of the working application:

tiles

Attached you may found the project and a small video showing the effect working.

Download: Elite.Silverlight3.PixelShaders.zip (~2.2 MB)

Video: TilingPixelShader.wmv (~714 KB)

Categories:   Effects
Actions:   E-mail | del.icio.us | Permalink | Comments (14) | Comment RSSRSS comment feed

Comments (14) -

April 7. 2009 05:48

i think this would be correct pixel shader code.
uv.xy = frac(uv.xy * tile.xy);
remember its SIMD - single instruction multiple data = faster.

James Hardaker

April 7. 2009 05:49

tile being float2, pass it as a Point struct

James Hardaker

April 7. 2009 05:52

thank you for your help.

Andrea Boschin

July 21. 2009 17:24

For Silverlight 3 RTW I had to make two changes:

1) Build Action: Resource for tile.ps
2) Use this Uri:

Uri ps = new Uri(@"/Elite.Silverlight3.PixelShaders.Silverlight;component/tile.ps", UriKind.RelativeOrAbsolute);

Kurt

July 21. 2009 20:53

Thank you Kurt.

Andrea Boschin

July 23. 2009 09:39

Very interesting.  Would it be possible to tile an arbitrary sized area, generated at run time?  I have code that generates a region (GeometryGroup) comprised of a bunch of overlapping ellipses that I want tiled with a pattern.

Thanks for your time.

Ed.

July 23. 2009 09:46

@ed
yes of course. You can apply the effect to every visual element and you will get it tiled Smile bye

Andrea Boschin

July 23. 2009 14:35

Would you mind pointing me in the right direction?  When I'm generating the ellipses, do I tile the image I want at that time, or do I do it after I've added all of them to my Path object and them somehow tile the Path with my image?  I'm fairly new to Silverlight.  Thanks.

Ed.

July 23. 2009 14:57

If you scan the properties of an element like Path, or Canvas or every other element in the xaml you will find an "effect" property. You have simply to assign the <local:TileEffect x:Name="tile" HorizontalTileCount="2" VerticalTileCount="2" /> to this property and you will get the element tiled.

<Path>
   <Path.Effect>
      <local:TileEffect x:Name="tile" HorizontalTileCount="2" VerticalTileCount="2" />
   </Path.Effect>
</Path>

Andrea Boschin

August 29. 2009 20:54

I really appreciate your code. But I do have one question. When I tile Horizontally - there seems to be a seam on the left edge of every tile. How do I adjust to make it seemless?

Van Ice

August 29. 2009 21:11

hi, what exactly do you mean with "seam"? can you provide a screenshot ot the junction line?

Andrea Boschin

December 17. 2009 09:45

Hi Andrea,

I've been trying your code out and when I run it under SL 4 Beta, I'm getting a black region with no brushes painted...

I noticed you define a Dep prop called Input (type Brush), but never use it (I mean you never bind to it/assign to it in Xaml).

Any ideas?

Thanks

Rob Cecil

December 17. 2009 09:48

I take that back - I was running that under SL 3, using VS2008.

Rob Cecil

March 17. 2010 01:48

  Unhandled Error in Silverlight Application
Code: 2221    
Category: RuntimeError      
Message: Shader file tile.ps not found.    
MethodName:
I've got this error. I dont know hot to fix it. Can you help me???

umit