November 2011

Volume 26 Number 11

Silverlight 3D - Developing 3D Objects in Silverlight

By Rajesh Lal | November 2011

In this article, I’ll show you how to develop 3D objects in Silverlight. I’ll start with a brief background on 3D and then look at some of the advanced features available in Silverlight that enable creation and display of 3D objects. I’ll take a simple example of a cube and show you three different ways to create 3D transformations. I’ll also explain what key elements you need to display a 3D object on a computer screen. Finally, I’ll explore how Silverlight 5 will allow you to go beyond what’s available today and create much richer 3D objects.

Silverlight supports a right-handed coordinate system, which means the positive z axis is pointing toward the viewer (see Figure1). There are three main elements of 3D that are needed for displaying an object on the screen:

  • Perspective
  • Transformation
  • Effect of light

A Reference Cube, Showing Sides with Perspective
Figure 1 A Reference Cube, Showing Sides with Perspective

Perspective means that parts of objects closer to us appear larger than those that are farther away. For example, in Figure 1, side bd looks bigger than side fh. In real life, perspective creates a vanishing point, which means if you extend the lines ae, bf, cg and dh on the z axis, they’ll meet far back at an arbitrary single point.

The second aspect is transformation. A 3D object, when displayed on a screen, should allow movement in the 3D space in all directions. It can be moved in any single axis—scaled for size—while keeping the perspective intact. It can be rotated 360 degrees in all axes: x, y and z. This gives a 3D object the flexibility it needs for rendering on the screen.

The last element of 3D is the effect of light. Light in 3D creates shading, which is brighter near a light source and fades with distance. In 3D rendering, the two popular kinds of shading are “flat” shading and “gradient” shading. I’ll explain how they differ later. Light will also create a shadow on the sides opposite the light source.

In the forthcoming examples, I’ll explore three different ways in which you can create 3D objects in Silverlight:

  • Using perspective 3D
  • Using multiple frames and a timer
  • Using primitives with the XNA library

In the first method, an object is created using two-dimensional elements, but it looks and behaves as if it were in 3D space. Perspective 3D is a special kind of transformation feature added in Silverlight 4 that allows for basic transformations such as rotation, scaling and translation in 3D space. The second method is the brute force method, where you don’t create the 3D object, but instead create the final output frames for a particular transformation and display them using a timer. The final method is to create a rich 3D object bit by bit using primitives (a list of triangles) with XNA libraries, which will be available in Silverlight 5. Let’s get started.

Creating a Cube Using Perspective 3D

Silverlight 4 supports a PlaneProjection class (see Figure 2), which can be used for the Projection property of any UI element, as shown in the class diagram. The PlaneProjection class allows for perspective 3D transformations on the UI element. Although it doesn’t directly allow for creating a 3D object, you can use multiple “walls” to create an object and transform it in 3D space.

The PlaneProjection Class
Figure 2 The PlaneProjection Class

The PlaneProjection class allows for LocalOffset and GlobalOffset, which are used to translate an object with respect to itself and with respect to another element in the global space, respectively. RotationX, RotationY and RotationZ allow for rotating the element in the x, y and z axes, and CenterOfRotation allows for a center point with respect to the element plane.

In my example, to create a “cube,” I’ll create four sides of the cube and move them in the 3D space by setting their PlaneProjection properties, as shown in Figure 3.

Figure 3 Setting PlaneProjection Properties

<Grid x:Name="LayoutRoot" Background="White" Width="800" Height="700">
  <Rectangle Fill="#9900FF00" Width="250" Height="250" Visibility="Visible">
  <Rectangle.Projection>
    <PlaneProjection x:Name=
      "projectionFront" CenterOfRotationZ="125" RotationX="-180"/>
  </Rectangle.Projection>
</Rectangle>
<Rectangle Fill="#99FF0000" Width="250" Height="250" Visibility="Visible">
  <Rectangle.Projection>
    <PlaneProjection x:Name=
      "projectionBottom" CenterOfRotationZ="125" RotationX="-90" />
  </Rectangle.Projection>
</Rectangle>
<Rectangle Fill="#990000FF" Width="250" Height="250" Visibility="Visible">
  <Rectangle.Projection>
    <PlaneProjection x:Name="projectionBack" CenterOfRotationZ="125" />
  </Rectangle.Projection>
 </Rectangle>
<Rectangle Fill="#99FFFF00" Width="250" Height="250" Visibility="Visible">
  <Rectangle.Projection>
    <PlaneProjection x:Name=
      "projectionTop" CenterOfRotationZ="125" RotationX="90"/>
  </Rectangle.Projection>
</Rectangle>
</Grid>

In Figure 4, the sides are rotated by 90, -90 and -180 degrees to create the top, bottom and front projection planes of the cube. The CenterofRotationZ value of 125 creates the center point by which all the planes can be rotated along the z axis.

Projecting the Sides to Simulate a 3D Wall
Figure 4 Projecting the Sides to Simulate a 3D Wall

Once the cube is created using the projection plane, I need to rotate it by the x, y and z axes. This is where I use the storyboard object in Silverlight. I create three storyboards, one for each axis, as shown in Figure 5.

Figure 5 Rotating the Cube with Storyboards

<Storyboard x:Name="storyboardRotateX">
  <DoubleAnimation Storyboard.TargetName="projectionFront" 
    Storyboard.TargetProperty="RotationX" From="-180.0" To="180.0" Duration="0:0:10"
    RepeatBehavior="Forever"  />
  <DoubleAnimation Storyboard.TargetName="projectionBottom"
    Storyboard.TargetProperty="RotationX" From="-90.0" To="270.0" Duration="0:0:10"
    RepeatBehavior="Forever"  />
  <DoubleAnimation Storyboard.TargetName="projectionBack" 
    Storyboard.TargetProperty="RotationX" From="0.0" To="360.0" Duration="0:0:10"
    RepeatBehavior="Forever"   />
  <DoubleAnimation Storyboard.TargetName="projectionTop"
    Storyboard.TargetProperty="RotationX" From="90.0" To="450.0" Duration="0:0:10"
    RepeatBehavior="Forever"   />
  </Storyboard>
  <Storyboard x:Name="storyboardRotateY">
  <DoubleAnimation Storyboard.TargetName="projectionFront"
    Storyboard.TargetProperty="RotationY" From="0.0" To="360.0" Duration="0:0:10"
    RepeatBehavior="Forever" />
  <DoubleAnimation Storyboard.TargetName="projectionBottom"
    Storyboard.TargetProperty="RotationY" From="0.0" To="360.0" Duration="0:0:10"
    RepeatBehavior="Forever" />
  <DoubleAnimation Storyboard.TargetName="projectionBack"
    Storyboard.TargetProperty="RotationY" From="0.0" To="360.0" Duration="0:0:10" 
    RepeatBehavior="Forever" />
  <DoubleAnimation Storyboard.TargetName="projectionTop"
    Storyboard.TargetProperty="RotationY" From="0.0" To="360.0" Duration="0:0:10"
    RepeatBehavior="Forever" />
  </Storyboard>
  <Storyboard x:Name="storyboardRotateZ">
  <DoubleAnimation Storyboard.TargetName="projectionFront"
    Storyboard.TargetProperty="RotationZ" From="0.0" To="360.0" Duration="0:0:10"
    RepeatBehavior="Forever" />
  <DoubleAnimation Storyboard.TargetName="projectionBottom"
    Storyboard.TargetProperty="RotationZ" From="0.0" To="360.0" Duration="0:0:10"
    RepeatBehavior="Forever" />
  <DoubleAnimation Storyboard.TargetName="projectionBack"
    Storyboard.TargetProperty="RotationZ" From="0.0" To="360.0" Duration="0:0:10"
    RepeatBehavior="Forever" />
  <DoubleAnimation Storyboard.TargetName="projectionTop"
    Storyboard.TargetProperty="RotationZ" From="0.0" To="360.0" Duration="0:0:10"
    RepeatBehavior="Forever" />
</Storyboard>

On each storyboard, I rotate each of the four projection planes to keep the cube structure intact. Note that for x axis rotation, the RotationX value starts from the original RotationX value of the plane and goes another 360 degrees for ProjectionFront, so it starts at -180 degrees and goes to 180 degrees. As you can see in Figure 6, the cube is ready for rotation along the x, y, and z axes, it can be moved along any axis and it supports coloring of each of the sides.

The Cube, Ready for Rotation
Figure 6 The Cube, Ready for Rotation

In this example, I was easily able to create a cube and transform it in 3D space without a lot of code. This is the real strength of perspective 3D transformation. For basic 3D operations, you should use this option. However, it comes with a number of drawbacks. For advanced 3D objects, the number of projection planes required and their settings can increase dramatically, and you have to manually figure out the angles between each projection plane and the CenterOfRotation. A second issue here is that rotation of the 3D object depends on the storyboards, which makes it CPU-intensive; it doesn’t use the GPU to render the object. Another issue with this approach is that you also render the back of the cube even if it isn’t visible—not an optimal approach.

The third main element needed for displaying 3D objects onscreen is the effect of light. In real life, you have light, so how do you simulate that in 3D space on the screen? As mentioned, two popular ways to do that are flat shading and gradient shading.

Flat shading takes into account the surface of the plane and applies an average shading along the plane. Gradient shading (Gouraud shading) uses a gradient to shade the surface and takes into account each of the vertices of the plane. The plane isn’t flat shaded, but rather “smooth” shaded, based on different vertex colors.

In this example, each of the planes allows for color fill (flat shading) as well as gradient fill (gradient shading), which can be used to simulate the light effect. I’ll discuss these later. A quick way to simulate the light effect is by overlapping a Radial gradient rectangle with transparency using the following code after the 3D object:

<Rectangle x:Name=
  "BulbGradient" Height="700" Width="800" Margin="0 50 0 0" Grid.Row="1"
  Visibility="Collapsed">
  <Rectangle.Fill>
    <RadialGradientBrush RadiusX="0.5" RadiusY="0.5" GradientOrigin="0.25,0.25">
      <GradientStop Color="#00000000" Offset="0"/>
      <GradientStop Color="#FF000000" Offset="2"/>
     </RadialGradientBrush>
  </Rectangle.Fill>
</Rectangle>

Creating a Cube Using Frames

The second way to create a 3D experience is by using the final frames. Here, you don’t create the 3D object itself, but start with the required final output and export it as individual frames. A number of 3D modeling software programs allow you to create 3D objects and transformations that can be exported as multiple frames and then imported into Silverlight.

In this example, I’ll take a simple cube animation and export its rotation on the x, y and z axes into multiple frames of images. Figure 7 shows eight different frames of a cube rotation on the x axis. For this example, I use a minimal set of frames to create a cube rotation, but more frames per second create a smoother and seamless rotation.

Eight Different Frames Simulating Rotation in the X Axis
Figure 7 Eight Different Frames Simulating Rotation in the X Axis

To simulate the rotation in Silverlight, I use a timer as shown in the following code:

DispatcherTimer timer = new DispatcherTimer();
timer.Interval = new TimeSpan(0, 0, 0, 0, 500);
timer.Tick += new EventHandler(Tick);
 
private void Tick(object o, EventArgs sender)
{
  string imageuri = "cube/" + axis + "/" + currentImageIndex + ".png";
  bgImage.Source = new BitmapImage(new Uri(imageuri, UriKind.RelativeOrAbsolute));
  if (currentImageIndex <= 8)
    currentImageIndex++;
  else
    currentImageIndex = 1;
}

Note that this is a simplified version of what you can do with this method. Here I’m using exported images for simplicity, but a number of 3D softwares allow for XAML export, which creates polygons with color and gradients instead of images, providing a seamless, smoother animation. The timer can also be replaced by storyboards with animations.

This approach is straightforward. You have a specific transformation requirement with a particular 3D object. You don’t have to worry about creating the 3D object, and this technique can be used to create any kind of 3D object—you’re no longer restricted to a simple cube. This method can be used for all kinds of transformations. You can translate, rotate and scale sophisticated 3D objects. It even simulates the effect of light, which also translates directly from the modeling software.

The primary limitation of this approach is flexibility of programming against the 3D object. Once you export your 3D object, it generates static code that can be difficult to code against. You can’t move the object with respect to other elements in your Silverlight application. Another disadvantage is that the number of frames required increases linearly with every transformation. Apart from that, the rendering happens on the CPU, so bigger animations with larger numbers of frames would have a performance hit.

This leads to the third approach of using the XNA library in the upcoming Silverlight 5, which, as you’ll see, overcomes most of the problems with the first two approaches. But before that, let’s talk about how a 3D object translates in a 2D screen mathematically—the genius logic behind 3D.

Understanding World, View and Projection Matrix

To display the object, you have to understand three main concepts or “spaces” and how the object is mapped from its own object space to the screen:

  • World
  • View
  • Projection

Figure 8 shows the order in which an object is mapped to the screen.

The Order in Which a 3D Object Is Mapped to the Screen
Figure 8 The Order in Which a 3D Object Is Mapped to the Screen

The first set of coordinates for a 3D object is the x, y, and z coordinates in the object space (also called model space). These coordinates are relative to one another with their centers (0, 0, 0). Remember, with right-hand Cartesian coordinates, the positive z axis is toward the viewer.

For a 3D cube, the top-right corner of the front side will be (1,1,1), and the bottom-left corner of the back side will be (-1,-1,-1), as shown in Figure 9. In object space, coordinates are relative to one another and their position can only range from -1 to +1. For my cube to use 75 percent of the object space, I need to multiply each coordinate by .75, so the new bwill be (.75,.75,.75) and the new g will be (-.75,-.75,-.75).

Coordinates in 3D Space
Figure 9 Coordinates in 3D Space

When the object is put on the world space, the object itself isn’t moved, but rather mapped with respect to the world coordinates by multiplying its coordinates with the world matrix. In the world space, you can transform the 3D object by moving the coordinates to translate the object, changing the size to scale and changing the angle to rotate the object. To express your object’s coordinates in the world space, you need to multiply each vertex position with the world matrix:

Object World Coordinates = Object Coordinates * World Matrix

The next element is the camera view, which denotes the point from which you’re looking at the object. This point can change in the 3D space without changing the coordinates of the actual object in object space as well as world space. To calculate the coordinates of the object with respect to the camera view, you multiply the view matrix with the object’s world matrix:

Object View Coordinates = World Coordinates * View Matrix

Finally, the object view has to be rendered on the screen; this is where you need to calculate the perspective view created because of the distance. So far, my object is in parallel projection (the sides are parallel), but I need to display the object in perspective projection (the sides merge into a vanishing point), so I multiply the product of the object’s view matrix and world matrix with the projection matrix:

Object Final Coordinates = World Coordinates * View Matrix * Projection Matrix

This is the 3D object’s final position on the screen, which is also called the WorldViewProjection.

The Matrix

A Matrix struct, available in Microsoft.Xna.Framework, is included in Silverlight 5.  It has a 4x4 homogeneous matrix with 16 floating-point numbers as fields and a number of methods to generate a transformation matrix (see Figure 10).

A Silverlight 5 Matrix Struct
Figure 10 A Silverlight 5 Matrix Struct

The first three rows by columns (M11- M33) are used for scale and rotate transformations, and the fourth row (M41-M43) is used for translation (see Figure 11).

Figure 11 The 4x4 Matrix

M11 M12 M13 M14
M21 M22 M23 M24
M31 M32 M33 M34
M41 M42 M43 M44

To understand the matrix better, let’s see how it’s used with respect to a transformation. There are five different types of matrices: 4x4 matrix structure, identity matrix, translation matrix, scale matrix and rotation matrix.

Identity matrix (see Figure 12) is the unit matrix of size 4, and this becomes the original position of the 3D object in world space. If you multiply any matrix with identity matrix, you’ll get the original matrix without any change. Matrix structure provides a simple property that returns a Matrix.Identity.

Figure 12 The Identity Matrix

1 0 0 0
0 1 0 0
0 0 1 0
0 0 0 1

To scale the matrix object, provide a method called Matrix.CreateScale. The scale matrix is used for scale transformation on a 3D object, so when you multiply an object with scale matrix (see Figure 13), the resulting matrix resizes accordingly.

Figure 13 The Scale Matrix

Sx 0 0 0
0 Sy 0 0
0 0 Sz 0
0 0 0 1

The matrix object also provides a Matrix.CreateTranslate method for moving the object in the world space. When multiplied by the translation matrix (see Figure 14), the object translates in the world space.

Figure 14 The Translation Matrix

1 0 0 0
0 1 0 0
0 0 1 0
Tx Ty Tz 1

 For rotation, there are multiple methods. The Matrix.CreateFromYawPitchRoll method is used for rotating each of the axes for a floating-number value. The Matrix.CreateRotationX, Matrix.CreateRotationY and Matrix.CreateRotationZ methods are for rotating the object along the x,y and z axes. The rotation matrix with respect to angle theta involves M11-M33 elements as shown in Figure 15.

Figure 15 Rotation Matrix Along X, Y and Z Axes

1 0 0 0
0 Cos θ Sin θ 0
0 -Sin θ Cos θ 0
0 0 0 1
Rotation X      
Cos θ 0 Sin θ 0
0 1 0 0
-Sin θ 0 Cos θ 0
0 0 0 1
Rotation Y      
Cos θ Sin θ 0 0
-Sin θ Cos θ 0 0
0 0 1 0
0 0 0 1
Rotation Z      

Learning the 3D Pipeline for Silverlight-XNA

Silverlight 5 with XNA libraries provides a step-by-step process for creating 3D objects with vertex coordinates for rendering on the screen. This can be divided into five major steps (see Figure 16) involving the components shown here:

  1. Vertex buffer
  2. WorldViewProjection coordinates
  3. Shading: vertex, pixel and texture
  4. Graphical processing: rasterize, clip and cull
  5. Final output: the frame buffer 

Creating 3D Objects with the Silverlight 5 XNA Libraries
Figure 16 Creating 3D Objects with the Silverlight 5 XNA Libraries

I’ll briefly discuss each step and its components.

Vertex Buffer In creating a vertex buffer from the vertex collection, the first step is to create the skeleton of the 3D object by using a set of vertices. Each vertex contains at least the x, y and z coordinates, but normally also has other properties such as color and texture. This collection of vertices is then used to create a vertex buffer, which goes to the next step in the process.

WorldViewProjection Coordinates The final coordinates are calculated by multiplying vertices with world, view and projection matrices. Here, the object’s relation with the world space, view and projection is calculated and applied. Check the last two sections for more details. Once you have the final coordinates, then the actual shading process takes place.

Shading Vertex shading, pixel shading and texture shading are used. In this step, first vertex coloring is done, and then the pixel-by-pixel shading happens. Texture shading is also applied in this step. These final shaded coordinates are then used to create a frame buffer.

Rasterize, Clip and Cull During rasterization an image is converted into pixels, and then clip and cull are used to remove the skeleton of the 
object, along with hidden and invisible layers. This is finally rendered on the screen.

Frame Buffer After the image is rasterized, clipped and culled, a frame buffer is generated, which is then sent to the screen for display.

Creating a Cube Using Primitives

With the knowledge of matrices, world, view, projection and the 3D pipeline in Silverlight 5 with XNA libraries, let’s create a 3D cube and see how it all comes together.

The foremost advantage of this approach is the GPU acceleration, which enables hardware acceleration, freeing the CPU from rendering the 3D object. This is enabled by setting the EnableGPUAcceleration parameter in the Object tag to true in the HTML used to configure the Silverlight plug-in, as shown here:

<object data="data:application/x-silverlight-2,"
  type="application/x-silverlight-2" width="100%" height="100%">
  <param name="EnableGPUAcceleration" value="true" />
  <param name="source" value="ClientBin/Cube3d.xap"/>
  <param name="minRuntimeVersion" value="5.0.60211.0" />
</object>

In XAML, I’ll add a DrawingSurface object inside the Grid, which is used to render 3D in Silverlight with the help of the DrawPrimitives method of the GraphicsDevice object (see Figure 17):

<DrawingSurface Loaded="OnLoad" SizeChanged="OnSizeChanged" Draw="OnDraw"/>

The DrawPrimitives Method of the GraphicsDevice Class
Figure 17 The DrawPrimitives Method of the GraphicsDevice Class

I’ll use three methods in the DrawingSurface class to create and render the cube. The OnLoad method is used for creating the cube and initializing all the shaders and the view matrix, which doesn’t change in this application. Note that the 3D object is centered at (0,0,0) using 75 percent of the object space with coordinates ranging from (.75,.75,.75) to (-.75,-.75,-.75). Here, I’ll create a vertex buffer to hold the vertex collection and initialize the shaderStream, pixelStream and imageStream streams, which will all be used later in the shading step. I’ll also initialize the view matrix, which is the angle at which the camera is looking at the object using cameraPosition and cameraTarget with the parameter Vector3.Up (which means the camera is looking upward). This code is shown in Figure 18.

Figure 18 Setting up shaderStream, pixelStream and imageStream

VertexBuffer vertexBuffer;
VertexShader vertexShader;
PixelShader pixelShader;
Texture2D texture;
 
private void OnLoad(object sender, RoutedEventArgs e)
{
  vertexBuffer = CreateCube();
  Stream shaderStream = Application.GetResourceStream(new
    Uri(@"Cube3d;component/shader/shader.vs", UriKind.Relative)).Stream;
  vertexShader = VertexShader.FromStream(resourceDevice, shaderStream);
 
  Stream pixelStream = Application.GetResourceStream(new  
    Uri(@"Cube3d;component/shader/shader.ps", UriKind.Relative)).Stream;
  pixelShader = PixelShader.FromStream(resourceDevice, pixelStream);
           
  Stream imageStream = Application.GetResourceStream(new
    Uri(@"Cube3d;component/scene.jpg",
    UriKind.Relative)).Stream;
  var image = new BitmapImage();
  image.SetSource(imageStream);
  texture = new Texture2D(resourceDevice, image.PixelWidth,
    image.PixelHeight, false, SurfaceFormat.Color);
  image.CopyTo(texture);
        
  Vector3 cameraPosition = new Vector3(0, 0, 5.0f);
  Vector3 cameraTarget = Vector3.Zero; 
  view = Matrix.CreateLookAt(cameraPosition, cameraTarget, Vector3.Up);
}

The next step is to create the vertex buffer for the 3D cube. I’ll create a CreateCube method as shown in Figure 19 that returns a VertexBuffer. I’ll create two rectangles in the 3D space, with ABCD making up the front face of the cube and EFGH the back of the cube. I use the VertexPositionColor structure to create a collection of vertices and the colors associated with each of them.

Figure 19 Creating the VertexBuffer in the CreateCube Method

VertexBuffer CreateCube()
{
  var vertexCollection = new VertexPositionColor[36];
 
  // Front coordinates
  Vector3 cubeA = new Vector3(-0.75f, 0.75f, 0.75f);
  Vector3 cubeB = new Vector3(0.75f, 0.75f, 0.75f);
  Vector3 cubeC = new Vector3(-0.75f, -0.75f, 0.75f);
  Vector3 cubeD = new Vector3(0.75f, -0.75f, 0.75f);
  // Back coordinates
  Vector3 cubeE = new Vector3(-0.75f, 0.75f, -0.75f);
  Vector3 cubeF = new Vector3(0.75f, 0.75f, -0.75f);
  Vector3 cubeG = new Vector3(-0.75f, -0.75f, -0.75f);
  Vector3 cubeH = new Vector3(0.75f, -0.75f, -0.75f);
 
  // Colors
  Color cRed = Color.FromNonPremultiplied(255, 0, 0, 156);
  Color cGreen = Color.FromNonPremultiplied(0, 255, 0, 156);
  Color cBlue = Color.FromNonPremultiplied(0, 0, 255, 156);
  Color cYellow = Color.FromNonPremultiplied(255, 255, 0, 156);
  Color cBlack = Color.FromNonPremultiplied(0, 0, 0, 156);
  Color cWhite = Color.FromNonPremultiplied(255, 255, 255, 156);
 
  // Front
  vertexCollection[0] = new VertexPositionColor(cubeA, cGreen);
  vertexCollection[1] = new VertexPositionColor(cubeB, cGreen);
  vertexCollection[2] = new VertexPositionColor(cubeC, cGreen);
  vertexCollection[3] = new VertexPositionColor(cubeB, cBlue);
  vertexCollection[4] = new VertexPositionColor(cubeD, cBlue);
  vertexCollection[5] = new VertexPositionColor(cubeC, cBlue);
  // Back 
  vertexCollection[6] = new VertexPositionColor(cubeG, cBlue);
  vertexCollection[7] = new VertexPositionColor(cubeF, cBlue);
  vertexCollection[8] = new VertexPositionColor(cubeE, cBlue);
  vertexCollection[9] = new VertexPositionColor(cubeH, cGreen);
  vertexCollection[10] = new VertexPositionColor(cubeF, cGreen);
  vertexCollection[11] = new VertexPositionColor(cubeG, cGreen);
  // Top
  vertexCollection[12] = new VertexPositionColor(cubeE, cRed);
  vertexCollection[13] = new VertexPositionColor(cubeF, cRed);
  vertexCollection[14] = new VertexPositionColor(cubeA, cRed);
  vertexCollection[15] = new VertexPositionColor(cubeF, cYellow);
  vertexCollection[16] = new VertexPositionColor(cubeB, cYellow);
  vertexCollection[17] = new VertexPositionColor(cubeA, cYellow);
  // Bottom 
  vertexCollection[18] = new VertexPositionColor(cubeH, cRed);
  vertexCollection[19] = new VertexPositionColor(cubeG, cRed);
  vertexCollection[20] = new VertexPositionColor(cubeC, cRed);
  vertexCollection[21] = new VertexPositionColor(cubeD, cYellow);
  vertexCollection[22] = new VertexPositionColor(cubeH, cYellow);
  vertexCollection[23] = new VertexPositionColor(cubeC, cYellow);
  // Left
  vertexCollection[24] = new VertexPositionColor(cubeC, cBlack);
  vertexCollection[25] = new VertexPositionColor(cubeG, cBlack);
  vertexCollection[26] = new VertexPositionColor(cubeA, cBlack);
  vertexCollection[27] = new VertexPositionColor(cubeA, cWhite);
  vertexCollection[28] = new VertexPositionColor(cubeG, cWhite);
  vertexCollection[29] = new VertexPositionColor(cubeE, cWhite);
  // Right 
  vertexCollection[30] = new VertexPositionColor(cubeH, cWhite);
  vertexCollection[31] = new VertexPositionColor(cubeD, cWhite);
  vertexCollection[32] = new VertexPositionColor(cubeB, cWhite);
  vertexCollection[33] = new VertexPositionColor(cubeH, cBlack);
  vertexCollection[34] = new VertexPositionColor(cubeB, cBlack);
  vertexCollection[35] = new VertexPositionColor(cubeF, cBlack);
 
  var vb = new VertexBuffer(resourceDevice,
    VertexPositionColor.VertexDeclaration,
    vertexCollection.Length, BufferUsage.WriteOnly);
  vb.SetData(0, vertexCollection, 0, vertexCollection.Length, 0);
  return vb;
}

The OnSizeChanged method of the drawing surface is used to update the projection and the aspect ratio of the screen based on surface dimension:

private void OnSizeChanged(object sender, SizeChangedEventArgs e)
{
  DrawingSurface surface = sender as DrawingSurface;
  float sceneAspectRatio = (float)surface.ActualWidth / (float)surface.ActualHeight
  projection = Matrix.CreatePerspectiveFieldOfView
               (MathHelper.PiOver4, sceneAspectRatio, 1.0f, 100.0f);
}

The last method is OnDraw, which allows for dynamic transformation on the 3D cube. This is where I apply Matrix.CreateScale to resize the cube, Matrix.CreateFromYawPitchRoll to rotate it and Matrix.CreateTranslate to move it. Here, the worldViewProjection matrix is calculated and sent to the vertexShader method for shading the vertices, which in turn transfers to pixelShader to shade the “walls” of the cube. This in turn can also be sent to textureShader, which can be based on an image. Finally, the GraphicDevice Object calls a DrawPrimitives method to render the output frames, as shown in Figure 20.

Figure 20 Calling the DrawPrimitives Method to Render the Output Frames

void OnDraw(object sender, DrawEventArgs args)
{
  Matrix position = Matrix.Identity;
  Matrix scale = Matrix.CreateScale(1.0f);
  float xf = 0.0f; float yf = 0.0f; float zf = 0.0f;
 
  if (cubeXAxis) xf = MathHelper.PiOver4 * (float)args.TotalTime.TotalSeconds;
  if (cubeYAxis) yf = MathHelper.PiOver4 * (float)args.TotalTime.TotalSeconds;
  if (cubeZAxis) zf = MathHelper.PiOver4 * (float)args.TotalTime.TotalSeconds;
 
  Matrix rotation = Matrix.CreateFromYawPitchRoll(xf, yf, zf);
  Matrix world;
  if (translateZ != 0)
    world = rotation * Matrix.CreateTranslation(0, 0, (float)translateZ);
  else
    world = scale * rotation * position;
           
  // Calculate the final coordinates to pass to the shader.
  Matrix worldViewProjection = world * view * projection;
  args.GraphicsDevice.Clear(ClearOptions.Target | ClearOptions.DepthBuffer,
    new Microsoft.Xna.Framework.Color(0, 0, 0, 0), 10.0f, 0);
  
  // Set up vertex pipeline.
  args.GraphicsDevice.SetVertexBuffer(vertexBuffer);
  args.GraphicsDevice.SetVertexShader(vertexShader);
  args.GraphicsDevice.SetVertexShaderConstantFloat4(0, ref worldViewProjection);
 
  // Set up pixel pipeline.
  args.GraphicsDevice.SetPixelShader(pixelShader);
  args.GraphicsDevice.Textures[0] = texture;
  args.GraphicsDevice.DrawPrimitives(PrimitiveType.TriangleList, 0, 12);
  args.InvalidateSurface();
}

This dynamically renders the final 3D cube on the drawing surface (see Figure 21).

The Final 3D Cube on the Drawing Surface
Figure 21 The Final 3D Cube on the Drawing Surface

This approach uses GPU acceleration and doesn’t render the sides of the 3D object that are hidden or the back side, unlike my first approach. This also uses the frames buffer in memory to render the final output like the second approach, but with complete control and flexibility of programming against the object, as with the OnDraw method.

You’ve now learned three different ways to create 3D objects in Silverlight, each with its own strengths and weaknesses. These should be useful additions to your toolbox as you explore the world of 3D in Silverlight.   


Rajesh Lal is passionate about Silverlight and Web technologies. He has written multiple books on Windows gadget, Web widget and mobile Web technologies, including an upcoming book, “Fun with Silverlight 4” (CreateSpace, 2011). For more information, contact mconnectrajesh@hotmail.com or visit silverlightfun.com.

Thanks to the following technical experts for reviewing this article: Marc CliftonRick Kingslan and Josh Smith