Monday, September 6, 2010

A Tiled Image Brush for Silverlight

The Silverlight ImageBrush does not support the TileMode property that exists in WPF, and as a result there is no built in way to have tiled images as a background brush for your controls and styles.

The following control may be useful to you if you find yourself wanting to achieve the same things as a WPF tiled image brush.

You can grab the source here. It is a control that behaves like a Border control, except that it also has a TiledImageSource property that you can use to select an image resource from the project.

The TiledBGControl control (um, didn’t think too hard about naming it) is a viewless control, so you can change its default control template the same way you can for any other control. This is the structure of the default control template:


The template has a grid with two Border controls in it. The back-most Border  (the first one) is a control part that must exist, the source code of the control looks for this control and applies a shader effect to it to produce the tiled effect. The second Border is simply to provide the visible border around the control. I can’t use the first Border control to draw the visible border because all the pixels of that Border control (including the visible border) are replaced by the shader effect. The ContentPresenter is placed in the second Border control so that it appears on top of the tiled background.

The following two sections give a quick description of the pixel shaders and then an overview of how the control works.

Wrapping up a Pixel Shader

I’ve gone looking for a tiled image brush for Silverlight before. It’s just not possible (to the best of my knowledge) to create in Silverlight the same kind of tile brush that WPF has. The closest I’ve seen to accomplishing this effect is with a pixel shader. A pixel shader is a set of instructions that can change the pixels in a given area. For example, the built in Silverlight blur and drop-shadow effects uses a pixel shader. The instructions are written in a language called HLSL, compiled, and wrapped up in the .Net framework ShaderEffect class.

Walt Ritcher’s excellent free Shazzam tool can be used to create and test pixel shaders. It comes with a shader called “Tiler”, authored by A.Boschin, that allows you to achieve the tiled background effect but it is a little awkward to use as-is for a fluid interface. The Tiler shader has four properties for controlling how it is applied:

  • VerticalTileCount – how many tiles to squeeze in vertically in the given space
  • HorizontalTileCount – how many tiles to squeeze in horizontally in the given space
  • HorizontalOffset – a horizontal offset to apply to the first tile
  • VerticalOffset – a vertical offset to apply to the first tile

Using a shader like this is a little awkward because a shader doesn’t know how big an area it is being applied to (in pixels). It processes each location in the area by using a value between 0 and 1 for an x and y coordinate. If you want to keep the tile image at a 1:1 scale, you need to calculate how many tiles fit into the area the shader is applied to, and update this every time the area changes size. In WPF, the pixel shaders are executed on the GPU which saves the CPU for application logic, but Silverlight does not use the GPU (and probably never will), so the shaders must be used sparingly to prevent the CPU from slowing to a crawl.

I used the Tiler sample shader as a starting point and modified it, replacing the properties above with these ones:

  • TextureMap – the tile image to repeat as a background
  • DestinationHeight – the height of the destination area. This needs to be updated as the target area changes size
  • DestinationWidth – the width of the destination area. This also needs to be updated as the target area changes size
  • TileHeight – the height of the tile to be repeated as a background
  • TileWidth – the width of the tile to be repeated as a background

The main reason I did it this way was to learn HLSL; I could have just as easily used the same approach as the Tiler shader and calculated the number of vertical and horizontal tiles when the container changed size. The code for my pixel shader is as follows:

   1: /// <class>TilerXY</class>
   2: /// <description>Pixel shader tiles the image to size according to destination width and height</description>
   3: ///
   5: // Created by P.Middlemiss:
   6: // blog:
   8: sampler2D TextureMap : register(s2);
  11: /// <summary>The height of the target area.</summary>
  12: /// <minValue>0</minValue>
  13: /// <maxValue>3000</maxValue>
  14: /// <defaultValue>300</defaultValue>
  15: float DestinationHeight : register(C1);
  17: /// <summary>The width of the target area.</summary>
  18: /// <minValue>0</minValue>
  19: /// <maxValue>3000</maxValue>
  20: /// <defaultValue>300</defaultValue>
  21: float DestinationWidth : register(C2);
  23: /// <summary>The height of the tile.</summary>
  24: /// <minValue>0</minValue>
  25: /// <maxValue>500</maxValue>
  26: /// <defaultValue>100</defaultValue>
  27: float TileHeight : register(C3);
  29: /// <summary>The width of the tile.</summary>
  30: /// <minValue>0</minValue>
  31: /// <maxValue>500</maxValue>
  32: /// <defaultValue>100</defaultValue>
  33: float TileWidth : register(C4);
  35: sampler2D SourceSampler : register(S0);
  37: float4 main(float2 uv : TEXCOORD) : COLOR
  38: {
  39:     float xx = ((uv.x * DestinationWidth % TileWidth) / TileWidth);
  40:     float yy = ((uv.y * DestinationHeight % TileHeight) / TileHeight);
  41:     float2 newUv = float2(xx , yy) ;
  42:     float4 color= tex2D( TextureMap, newUv );
  43:     float4 source = tex2D( SourceSampler, uv);
  44:     color *= source.a;
  45:     return color;
  46: }

I’m still learning HLSL so I’m sure the code above could be improved. I have a fairly new computer and the CPU doesn’t really budge at all when using this component, but I would be interested to hear back from anyone with an older PC to see if performance is an issue, or from anyone who can suggest improvements to the HLSL.

The TiledBGControl

The TiledBGControl wraps up all of the awkwardness of using the pixel shader and lets you just supply the image source to use. The OnApplyTemplate method is overriden and carries out the following tasks:

  • The control looks for the template part called “PART_TiledBorder” and attaches the above shader effect to it.
  • An ImageBrush is created using the supplied TiledImageSource value and set as the value of the TextureMap property of the shader.
  • An event is attached to the border that updates the DestinationHeight and DestinationWidth when the border changes size.

The TileWidth and TileHeight properties of the shader have to be updated whenever the TileImageSource changes. The TileImageSource is a property of type ImageSource and is used as the value for the ImageBrush. There are a couple of things to know about the ImageBrush and ImageSource classes:

  • An ImageSource is not evaluated until is used, which means you don’t necessarily know the size of the image when you assign it.
  • The ImageBrush fires an ImageOpened event when the ImageSource is resolved
  • The resolved image is cached, so if you set the ImageSource to a value that has already been resolved you won’t get an ImageOpened event fired again.
  • The ImageSource, once resolved, can be cast to the BitmapImage type which has the PixelWidth and PixelHeight properties
  • Setting the ImageBrush.Stretch mode to None does not give the desired result – I thought it would just render the image in its actual size, but it doesn’t. I used Stretch.Fill instead

The code for creating the ImageBrush from the provided ImageSource is as follows:

   1: ImageBrush brush = new ImageBrush();
   2: brush.ImageSource = this.TiledImageSource;
   3: bool isOpened = 0 != ((BitmapImage)brush.ImageSource).PixelWidth;
   4: if (!isOpened)
   5: {
   6:     // we don't have the size yet so work that out when the ImageSource is resolved
   7:     brush.ImageOpened += (sender, args) =>
   8:         {
   9:             brush.Stretch = Stretch.Fill;
  10:             this.tilerXY.TileWidth = ((BitmapImage)brush.ImageSource).PixelWidth;
  11:             this.tilerXY.TileHeight = ((BitmapImage)brush.ImageSource).PixelHeight;
  12:         };
  13: }
  14: else
  15: {
  16:     brush.Stretch = Stretch.Fill;
  17:     this.tilerXY.TileWidth = ((BitmapImage)brush.ImageSource).PixelWidth;
  18:     this.tilerXY.TileHeight = ((BitmapImage)brush.ImageSource).PixelHeight;
  19: }
  20: this.tilerXY.TextureMap = brush;

The tile images in the sample above are from I’ve put the control up on the Expression Gallery, so feel free to grab the source and use it according to the license there.