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:

ControlStructure

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: ///
   4:  
   5: // Created by P.Middlemiss: phil.middlemiss@gmail.com
   6: // blog: http://silverscratch.blogspot.com/
   7:  
   8: sampler2D TextureMap : register(s2);
   9:  
  10:  
  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);
  16:  
  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);
  22:  
  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);
  28:  
  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);
  34:  
  35: sampler2D SourceSampler : register(S0);
  36:  
  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 www.repeatxy.com. 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.

31 comments:

  1. "but Silverlight does not use the GPU (and probably never will),"

    Silverlight does actually support GPU acceleration.

    http://msdn.microsoft.com/en-us/library/ee309563(v=VS.95).aspx

    It was introduced in SL3 and has gotten better in SL4, probably even better in SL5.

    ReplyDelete
  2. Thanks for the link. While it is true that the GPU support exists for some things, pixel shaders seem unlikely to be supported:

    http://forums.silverlight.net/forums/t/155550.aspx

    Although, I hope I am wrong and - as you say - maybe they will be supported in time.

    ReplyDelete
  3. Can I use this code during my presentation?

    ReplyDelete
  4. Sure, not a problem. If it doesn't do damage to your presentation, some attribution would be appreciated.

    ReplyDelete
  5. Thanks,
    It working really good, although the image always have a dropshadow effect in the left upper corner.

    Piet Koole

    ReplyDelete
  6. Hi Piet, I don't see any drop shadow in the sample app at the top of the page. Have you got an example somewhere that shows the problem?

    ReplyDelete
  7. Hey Phil,

    First, many thanks for the control.

    However, there's a weird bug I cannot understand.

    I'm trying to use the control in a SL user control in a Windows Phone 7 application. In design time everything looks pretty fine, but for some reason the tiled background is not displayed in runtime. I haven't found any exceptions being thrown.

    Could you please point me to what I'm doing wrong?

    Thanks,
    Yuriy

    ReplyDelete
  8. Hi Yuriy, I haven't tested it on a Windows Phone, but my understanding is custom pixel shaders are not supported on WP7.

    ReplyDelete
  9. Oh. Seems like I missed this point.

    Many thanks for clarifying, Phil.

    ReplyDelete
  10. wow thanks for your efforts.
    nice control working as expected.

    ReplyDelete
  11. I have updated it today after fixing a bug that caused the edges of each tile to align poorly. The sample tiles I used did not show that up very clearly. Thanks to Keith Harvey for spotting this.

    ReplyDelete
  12. Hey
    This is awesome. It works like a charm but has a small issue.
    When it first loads - there is orange flicker! Cant seem to figure where the color comes from - .PS file?

    ReplyDelete
  13. The orange flash seems to have appeared with Silverlight 5 - you can see from the source code that there is nothing there that should produce it. Perhaps log a bug report with MS?

    ReplyDelete
  14. Gosh! Spent over 3 hours trying to identify / decompile the .PS file to fix the orange flash. Can you share the source of the .PS file pls?

    BTW, cool stuff. Your source saves us from putting large background on the login page.

    Regards/ Tushar Sood

    ReplyDelete
  15. Thanks Tushar,

    the shader file source is given in the article, although there is a fix which I haven't updated it with yet, which is the main routine. It now looks like this:

    float4 main(float2 uv : TEXCOORD) : COLOR
    {
    float xx = (((uv.x * DestinationWidth % TileWidth)-1) / TileWidth);
    float yy = (((uv.y * DestinationHeight % TileHeight)-1) / TileHeight);
    float2 newUv = float2(xx , yy) ;
    float4 color= tex2D( TextureMap, newUv );
    float4 source = tex2D( SourceSampler, uv);
    color *= source.a;
    return color;
    }

    ReplyDelete
  16. Many thanks for the very good article!
    Referenced in a discussion on the CodeProject:
    WPF Sliding Controls Collection – Part 1: Sliding Image Control
    http://www.codeproject.com/Articles/378169/WPF-Sliding-Controls-Collection-Part-1-Sliding-Ima

    ReplyDelete
  17. Thanks for the nice comment Tefik, I really enjoyed your article too. Well written.

    ReplyDelete
  18. Hi Phil,

    Thanks for this control, we really appreciate it. We are also experiencing the orange flickr and wish to incorporate your fix above. Do you have a compiled tilerXY.ps file available for this? If not can you tell me how to compile the HLSL code fix you listed above? Should we use the legacy compiler? Should it be based on Direct3D 9 or 10, and what shader model, etc?

    TIA...Bob Baldwin
    aka VSDotNetGuy

    ReplyDelete
    Replies
    1. I forgot to mention we are on VS2010.

      Delete
    2. Hi Bob,

      You should be able to compile the HLSL code by downloading Shazaam from shazzam-tool.com

      The orange flash began with Silverlight 5 and I have no idea why.

      Delete
    3. Thx Phil,

      So are you saying your HLSL code listed on April 26, 2012 does not address or resolve the orange flash?

      Thx...Bob Baldwin
      aka VSDotNetGuy

      Delete
    4. Correct - The code update was to remove a small alignment bug in the shader code.

      Delete
  19. Gotcha, thx. That is weird, I wonder how SL5 could be causing this to happen?

    We will have research later. I understand the SL4 Jetpack Theme has some sort of Tiling class. Have you taken a look at it?

    Thanks again...Bob Baldwin
    aka VSDotNetGuy

    ReplyDelete
    Replies
    1. No - haven't seen it. These days I'm working mostly in the UX area and don't get much time for Silverlight development.

      Delete
  20. HI Phil,

    Thank you for the post....
    I need the source code to get the idea to implement it.
    But the code is not in Expression Gallery..
    How can I get this code...

    Regards
    Ashish

    ReplyDelete
    Replies
    1. Hi Ashish,

      you can use this link:
      https://dl.dropboxusercontent.com/u/1524737/Silverlight/TileBackgroundImage/TiledBackground.zip

      but please read the comment above about the correction for the shader code

      Delete
    2. Hi Phil

      Thank You for the assistance......

      Regards
      Ashish

      Delete
  21. This comment has been removed by the author.

    ReplyDelete
  22. Great solution, it is exactly what I need in this moment for one project. Thank you.

    ReplyDelete
  23. thanks for this informative blog you give us. keep updating and keep blogging.
    thanks a lot for this.
    Tile Brush Manufacturer

    ReplyDelete