Introduction

In contrast to WPF, there's no support for 3D in Silverlight 2.0. Still, it has the power of the .NET runtime built-in, so there's nothing to stop you from rolling your own little 3D engine in Silverlight. Not a high end one, but something to play around with and to add some neat effects to your RIA.

All you need is some understanding of math and the basic primitive of all things 3D - the triangle primitive. Specifically, I'm talking about a textured triangle with an image mapped on it.

Consequently, this article is all about implementing a triangle primitive as a custom control in Silverlight 2.0.

Using the triangle control

Using the control from XAML

Here's a snippet that shows how the ImageTriangle control can be used directly in XAML:

You can then reference the triangle in XAML with <xk:ImageTriangle ... /> and set the corner points (Point1, Point2, and Point3) via attributes as well as the TextureSource. TextureSource is an ImageSource that takes the image that will be rendered as texture onto the triangle. You can also set the texture coordinates by setting the TexturePositions, e.g., TexturePositions="0,0 1,0 0,1".

Using the control from code

Doing that in XAML is perfect for playing around with the control and getting to know how the various properties work and play together. However, when you want to use it for 3D or something similar, you're going to do it in code, be it C#, VB, or maybe IronPython.

In addition to the properties mentioned above, you can set all three corner points in a single method call in imperative code, which has some performance advantages:

tri1.SetPoints(new Point(0,0), new Point(100,0), new Point(0,100));

The TextureSource can be set directly to a BitmapImage, which allows you to use images downloaded from the web, or even from the local file system, as textures:

There is also an IsClockwise property that you might want to use for backface culling in 3D.

tri.IsClockwise

How does it work?

How would you draw a textured triangle in Silverlight? There are two ways I could think of:

use an Image control, and clip half of it away with a clipping path

Use a Path to draw the triangle shape, and add the image as an ImageBrush

I chose the second way, because it seemed to be a little bit faster.

Point1, Point2 and Point3 are implemented as Dependency Properties. Dependency Properties allow to perform calculations in the PropertyChanged callback and add the capability to take part in data-binding and animations.

In order to transform the triangle to fit the three corner points, I've added a MatrixTransform to the Path-element's RenderTransform. It is fairly straightforward to calculate the matrix elements from the given points. An article that explains how that works can be found here.

When you look at the control template and the UpdateCorners method, you will notice that there's a second transform applied as RenderTransform. When I got the code to work for the first time and created two triangles next to each other, I noticed a small, but irritating seam between them. This is due to how the antialiasing works in Silverlight. That's what the second transform is for. It scales the triangle up just a little bit (0.5 - 1 pixels on each side), just enough to overlap the seam.

Adding the texture

Great, now we've got a triangle that we can position arbitrarily. Are we done? Actually, no! Up to that point, the image on the triangle is fixed. What if you want to create a plane made of two triangles (or better 4 or 16, as I will explain below) - you don't want to load a separate image for each half!

We need a way to stretch and move the image position on the triangle itself. We need texture coordinates.

Again, this is done with a MatrixTransform. This time it is applied to the ImageBrush, filling the Path with a texture image. In this case, it is a little bit more tricky, though.

Effectively, what texture coordinates say is, where on the texture image the triangle lies. For the matrix applied to the ImageBrush, it has to be the other way round, we want to position the image as seen from the triangle's coordinate system.

We can do this by first calculating the matrix that maps the triangle onto the image and then inverting that matrix to get a transformation that maps the image onto the triangle. Exactly this is what's happening in the UpdateTexturePositions method:

Limitations

When you create a plane from two triangles and transform it to 3D, you might notice that it doesn't look quite right. This is because the depth information is not used. The only way to get around that is by adding more triangles. If you use eight triangles instead of two, the texture will look more realistic in 3D. You have to find the right tradeoff between performance and visual correctness.

Moving on

This article doesn't explain how to do 3D, it just provides you with a basic building block! However, the project files contain some 3D code to get you started.

My experiments have shown that rendering something around 100 triangles is possible at an acceptable frame-rate on a recent machine. Nothing to get too excited about, you won't be able to build a Halo or Second Life in Silverlight anytime soon. At least, not on a PC. But, it will be enough to do Coverflow-style animations, spinning 3D globes, and 360° panoramas, or maybe even games.

I managed to make it working now. But I descovered a second problem: When I have to overlapping pictures made of triangles, my browser crashes and I get a cryptic .NET Exception. Can anyone reproduce this error?

Great work! I would really love to use it. Anyway I have a problem because I use the latest Silverlight 2 Beta (version 2.0.30523) and your control is not working in this silveright version. I tried to fix that but I had some difficulties. I would be great if you could try to update your control to the current Silverlight Beta so that it will work again.

Thanks for the tip about anti-aliasing, but unfortunately I've already tried that when I was developing my panorama viewer[^] but the results distorted the picture too much so that I decided to just leave the antialiased-lines in place.

When I get the time, I'm going to optimise my code a bit and make it less messy, I'll give the overlapping another try too, maybe I've just overscaled it too much

I also want to try to re-use bitmapdata instead of having to load a new picture for every triangle that is drawn. Eventhough Silverlight caches the image, it messes up rendering if you have too many triangles. My first attempts where unsuccesfull, but I'll keep you informed if I have any breakthroughs

I was talking to my colleagues about this a couple of weeks ago - about how the only way to squeeze perspective out of Silverlight was to use triangles. You've explained this much more clearly than my fumbled whiteboard attempt (and the graphics are really useful too).