Walkthrough: Creating and Animating a 3-D Textured Cube in Silverlight
This walkthrough describes how to create and animate a 3D textured cube using the Silverlight 5 tools and runtime. In this walkthrough, you will create a Silverlight 5 application that controls the yaw, pitch, and roll (orientation) of this cube using Slider elements, as shown in the following illustration.

This topic contains the following sections.
This section describes how to create the project for this sample.
Start Visual Studio and create a new Silverlight Application project in C# named SilverlightApplication3DTest.
In the New Silverlight Application dialog box, specify the following settings:
Host the Silverlight Application in a new Web site
ASP.NET Web Application Project
Silverlight 5
Save the following image to your SilverlightApplication3DTest project folder and name it SLXNA.png. You can also use your own .png image that has a 2:1 ratio (where the width of the image in pixels is double the height). This image will be used as the texture for the cube.

In Visual Studio, add the SLXNA.png file to the SilverlightApplication3DTest project.
In the SilverlightApplication3DTest project, add references to the following assemblies:
Microsoft.Xna.Framework
Microsoft.Xna.Framework.Graphics
Microsoft.Xna.Framework.Graphics.Extensions
Microsoft.Xna.Framework.Graphics.Shaders
Microsoft.Xna.Framework.Math
System.Windows.Xna
In the SilverlightApplication3DTest.Web project, open SilverlightApplication3DTestTestPage.aspx and SilverlightApplication3DTestTestPage.html.
In the <object> tag add a <param> value named EnableGPUAcceleration to enable the hardware capabilities of your computer’s graphics processing unit (GPU). The EnableGPUAcceleration parameter is shown in bold the following code.
<object data="data:application/x-silverlight-2," type="application/x-silverlight-2" width="100%" height="100%"> <param name="source" value="ClientBin/<MAIN_PROJECT_NAME>.xap"/> <param name="EnableGPUAcceleration" value="true" /> <param name="onError" value="onSilverlightError" /> <param name="background" value="white" /> <param name="minRuntimeVersion" value="<RUNTIME_VERSION>" /> <param name="autoUpgrade" value="true" /> <a href=”<RUNTIME_LINK>” style="text-decoration:none"><img src="<IMG_LINK>" alt="Get Microsoft Silverlight" style="border-style:none"/> </object>
In Silverlight 5, shader effects can be applied to 3D objects by importing compiled shaders written in High-Level Shading Language (HLSL). Compilation of these files is possible using the Effect-Compiler Tool (FXC) that is provided as part of the DirectX SDK, or by using DirectX APIs. This section describes how to create and compile vertex and pixel shaders using the FXC tool.
Note: |
|---|
To complete this section, you must have the DirectX SDK installed. |
Open Windows Explorer to the project folder that contains MainPage.xaml.
Create a new text file named Cube.vs.hlsl.
Add the following HLSL code.
This code provides the ability to for the cube to have its position and world view modified by the Silverlight application. The output from this vertex shader is passed on to the pixel shader and also to the geometry processor, which changes the viewing angle of the cube when new input data is received.
// transformation matrix provided by the application float4x4 WorldViewProj : register(c0); // vertex input to the shader struct VertexData { float3 Position : POSITION; float2 UV : TEXCOORD; }; // vertex shader output passed through to geometry // processing and a pixel shader struct VertexShaderOutput { float4 Position : POSITION; float2 UV : TEXCOORD; }; // main shader function VertexShaderOutput main(VertexData vertex) { VertexShaderOutput output; // apply standard transformation for rendering output.Position = mul(float4(vertex.Position,1), WorldViewProj); // pass the UV coordinates through to the next stage output.UV = vertex.UV; return output; }Create a new text file named Cube.ps.hlsl.
Add the following HLSL code.
This code provides the ability to for the cube to have its texture modified by the Silverlight application.
texture cubeTexture : register(t0); sampler cubeSampler = sampler_state { texture = <cubeTexture>; }; // output from the vertex shader struct VertexShaderOutput { float4 Position : POSITION; float2 UV : TEXCOORD; }; // main shader function float4 main(VertexShaderOutput vertex) : COLOR { //return float4(1.0f, 1.0f, 1.0f, 1.0f); return tex2D(cubeSampler, vertex.UV).rgba; }Depending on your architecture, create a .bat named CompileShaders_x86.bat or CompileShaders_x64.bat.
To compile these HLSL files, use the FXC tool in the DirectX SDK.
If you are using an x86 architecture, add the following code to CompileShaders_x86.bat.
for /f "tokens=*" %%a in ('dir /b *.vs.hlsl') do ( call "%%DXSDK_DIR%%Utilities\bin\x86\fxc.exe" /T vs_2_0 /O3 /Zpr /Fo %%~na %%a >> hlslcomplog.txt ) for /f "tokens=*" %%a in ('dir /b *.ps.hlsl') do ( call "%%DXSDK_DIR%%Utilities\bin\x86\fxc.exe" /T ps_2_0 /O3 /Zpr /Fo %%~na %%a >> hlslcomplog.txt )If you are using an x64 architecture, add the following code CompileShaders_x64.bat.
for /f "tokens=*" %%a in ('dir /b *.vs.hlsl') do ( call "%%DXSDK_DIR%%Utilities\bin\x64\fxc.exe" /T vs_2_0 /O3 /Zpr /Fo %%~na %%a >> hlslcomplog.txt ) for /f "tokens=*" %%a in ('dir /b *.ps.hlsl') do ( call "%%DXSDK_DIR%%Utilities\bin\x64\fxc.exe" /T ps_2_0 /O3 /Zpr /Fo %%~na %%a >> hlslcomplog.txt )Double-click the .bat file to compile the two HLSL files.
Three files should be created: a text file logging the results of the calls to FXC (hlslcomplog.txt), the compiled pixel shader (Cube.ps), and the compiled vertex shader (Cube.vs).
In Visual Studio, add Cube.ps and Cube.vs to the SilverlightApplication3DTest project.
In the Properties window, set the Build Action for Cube.ps and Cube.vs to Resource.
To create a cube, you use 2D vectors (X and Y coordinates) and 3D vectors (X, Y, and Z coordinates) to create a 3D representation. You use the image to create a texture for the cube. To compute the position, scale, and rotation of the cube, you use matrices. You use the vertex and pixel shaders to draw the cube. This section describes how to create and texture the cube.
In the SilverlightApplication3DTest project, add a new class named Cube.
Add the following code to the class.
using System; using System.Net; using System.Windows; using Microsoft.Xna.Framework; using Microsoft.Xna.Framework.Graphics; using System.IO; using System.Windows.Media.Imaging; using System.Windows.Graphics; namespace SilverlightApplication3DTest { public struct VertexPositionTexture { public Vector3 Position; public Vector2 UV; public VertexPositionTexture(Vector3 position, Vector2 uv) { Position = position; UV = uv; } public static readonly VertexDeclaration VertexDeclaration = new VertexDeclaration( new VertexElement(0, VertexElementFormat.Vector3, VertexElementUsage.Position, 0), new VertexElement(12, VertexElementFormat.Vector2, VertexElementUsage.TextureCoordinate, 0) ); } public class Cube { static readonly GraphicsDevice resourceDevice = GraphicsDeviceManager.Current.GraphicsDevice; VertexBuffer vertexBuffer; VertexShader vertexShader; PixelShader pixelShader; Texture2D texture; public Cube() { // Initialize resources required to draw the Cube // Create geometry and store in vertex buffer vertexBuffer = CreateCube(); // Load vertex shader Stream shaderStream = Application.GetResourceStream(new Uri(@"SilverlightApplication3DTest;component/Cube.vs", UriKind.Relative)).Stream; vertexShader = VertexShader.FromStream(resourceDevice, shaderStream); // Load pixel shader shaderStream = Application.GetResourceStream(new Uri(@"SilverlightApplication3DTest;component/Cube.ps", UriKind.Relative)).Stream; pixelShader = PixelShader.FromStream(resourceDevice, shaderStream); // Load image for texture Stream imageStream = Application.GetResourceStream(new Uri(@"SilverlightApplication3DTest;component/SLXNA.png", UriKind.Relative)).Stream; var image = new BitmapImage(); image.SetSource(imageStream); // Create texture texture = new Texture2D(resourceDevice, image.PixelWidth, image.PixelHeight, false, SurfaceFormat.Color); // Copy image to texture image.CopyTo(texture); } /// <summary> /// Creates a vertex buffer containing a single Cube primitive /// </summary> VertexBuffer CreateCube() { var cube = new VertexPositionTexture[36]; Vector3 topLeftFront = new Vector3(-1.0f, 1.0f, 1.0f); Vector3 bottomLeftFront = new Vector3(-1.0f, -1.0f, 1.0f); Vector3 topRightFront = new Vector3(1.0f, 1.0f, 1.0f); Vector3 bottomRightFront = new Vector3(1.0f, -1.0f, 1.0f); Vector3 topLeftBack = new Vector3(-1.0f, 1.0f, -1.0f); Vector3 topRightBack = new Vector3(1.0f, 1.0f, -1.0f); Vector3 bottomLeftBack = new Vector3(-1.0f, -1.0f, -1.0f); Vector3 bottomRightBack = new Vector3(1.0f, -1.0f, -1.0f); Vector2 topLeftUVXNA = new Vector2(0.0f, 0.0f); Vector2 topRightUVXNA = new Vector2(0.5f, 0.0f); Vector2 bottomLeftUVXNA = new Vector2(0.0f, 1.0f); Vector2 bottomRightUVXNA = new Vector2(0.5f, 1.0f); Vector2 topLeftUVSL = new Vector2(0.5f, 0.0f); Vector2 topRightUVSL = new Vector2(1.0f, 0.0f); Vector2 bottomLeftUVSL = new Vector2(0.5f, 1.0f); Vector2 bottomRightUVSL = new Vector2(1.0f, 1.0f); // Front face cube[0] = new VertexPositionTexture(topRightFront, topRightUVXNA); cube[1] = new VertexPositionTexture(bottomLeftFront, bottomLeftUVXNA); cube[2] = new VertexPositionTexture(topLeftFront, topLeftUVXNA); cube[3] = new VertexPositionTexture(topRightFront, topRightUVXNA); cube[4] = new VertexPositionTexture(bottomRightFront, bottomRightUVXNA); cube[5] = new VertexPositionTexture(bottomLeftFront, bottomLeftUVXNA); // Back face cube[6] = new VertexPositionTexture(bottomLeftBack, bottomRightUVXNA); cube[7] = new VertexPositionTexture(topRightBack, topLeftUVXNA); cube[8] = new VertexPositionTexture(topLeftBack, topRightUVXNA); cube[9] = new VertexPositionTexture(bottomRightBack, bottomLeftUVXNA); cube[10] = new VertexPositionTexture(topRightBack, topLeftUVXNA); cube[11] = new VertexPositionTexture(bottomLeftBack, bottomRightUVXNA); // Top face cube[12] = new VertexPositionTexture(topLeftBack, topLeftUVSL); cube[13] = new VertexPositionTexture(topRightBack, topRightUVSL); cube[14] = new VertexPositionTexture(topLeftFront, bottomLeftUVSL); cube[15] = new VertexPositionTexture(topRightBack, topRightUVSL); cube[16] = new VertexPositionTexture(topRightFront, bottomRightUVSL); cube[17] = new VertexPositionTexture(topLeftFront, bottomLeftUVSL); // Bottom face cube[18] = new VertexPositionTexture(bottomRightBack, topLeftUVSL); cube[19] = new VertexPositionTexture(bottomLeftBack, topRightUVSL); cube[20] = new VertexPositionTexture(bottomLeftFront, bottomRightUVSL); cube[21] = new VertexPositionTexture(bottomRightFront, bottomLeftUVSL); cube[22] = new VertexPositionTexture(bottomRightBack, topLeftUVSL); cube[23] = new VertexPositionTexture(bottomLeftFront, bottomRightUVSL); // Left face cube[24] = new VertexPositionTexture(bottomLeftFront, bottomRightUVSL); cube[25] = new VertexPositionTexture(bottomLeftBack, bottomLeftUVSL); cube[26] = new VertexPositionTexture(topLeftFront, topRightUVSL); cube[27] = new VertexPositionTexture(topLeftFront, topRightUVSL); cube[28] = new VertexPositionTexture(bottomLeftBack, bottomLeftUVSL); cube[29] = new VertexPositionTexture(topLeftBack, topLeftUVSL); // Right face cube[30] = new VertexPositionTexture(bottomRightBack, bottomRightUVXNA); cube[31] = new VertexPositionTexture(bottomRightFront, bottomLeftUVXNA); cube[32] = new VertexPositionTexture(topRightFront, topLeftUVXNA); cube[33] = new VertexPositionTexture(bottomRightBack, bottomRightUVXNA); cube[34] = new VertexPositionTexture(topRightFront, topLeftUVXNA); cube[35] = new VertexPositionTexture(topRightBack, topRightUVXNA); var vb = new VertexBuffer(resourceDevice, VertexPositionTexture.VertexDeclaration, cube.Length, BufferUsage.WriteOnly); vb.SetData(0, cube, 0, cube.Length, 0); return vb; } public void Draw(GraphicsDevice graphicsDevice, TimeSpan totalTime, Matrix viewProjection) { // update cube transform Matrix position = Matrix.Identity; // origin Matrix scale = Matrix.CreateScale(1.0f); // no scale modifier // create a continuously advancing rotation Matrix rotation = Matrix.CreateFromYawPitchRoll(State.sliderYawVal, State.sliderPitchVal, State.sliderRollVal); // the world transform for the cube leaves it centered // at the origin but with the rotation applied Matrix world = scale * rotation * position; // calculate the final transform to pass to the shader Matrix worldViewProjection = world * viewProjection; // setup vertex pipeline graphicsDevice.SetVertexBuffer(vertexBuffer); graphicsDevice.SetVertexShader(vertexShader); graphicsDevice.SetVertexShaderConstantFloat4(0, ref worldViewProjection); // pass the transform to the shader // setup pixel pipeline graphicsDevice.SetPixelShader(pixelShader); graphicsDevice.Textures[0] = texture; // draw graphicsDevice.DrawPrimitives(PrimitiveType.TriangleList, 0, 12); } } }
In the Cube constructor, the .png texture file is loaded, as well as the compiled pixel and vertex shaders, and stored in properties of the Cube class for use in other methods.
The VertexPositionTexture structure contains a 3D vector and a 2D vector as its properties. This structure maps the 3D coordinates of the cube’s vertices to 2D coordinates on the cube’s six different surface planes. There is a one-to-many relationship between the 3D vertices and 2D coordinates. Groups of the 2D coordinates represent a shape in which graphical information from the texture file is rendered.
These associations are assigned in a member method for the Cube class named CreateCube. The 2D coordinates, positioned at their associated 3D vertices, are grouped in square areas comprised of six individual 2D coordinates, and each group represents a face on the cube. Six coordinates are used per face because the square area is actually comprised of two interlocking right triangles, representing polygons. With six sides on the cube, and six coordinates comprising each plane, 36 total associations are entered, mapping triangular areas of the .png graphic file to be used as the texture for polygons comprising the cube.
In the Cube class’s Draw method, which is called once per frame update, the cube is textured and oriented in the 3D scene it inhabits. The orientation of the cube is set using the CreateFromYawPitchRoll method, and the orientation used is derived from the values stored in static fields that are tracking state, which will be covered later in this walkthrough. For now, note that a class named State will provide the cube’s orientation values.
Caution: |
|---|
When making use of 3D in Silverlight, the rendering of the 3D scene takes place in a graphics thread that is separate from the application’s main UI thread, because this rendering is occurring on a completely different processor (the GPU) than is used by the rest of the application. For performance reasons, it is best that the thread that handles 3D rendering is passively “pulling” state information as shown in this sample and not actively “pushing” instructions into another thread (e.g. using Dispatcher.BeginInvoke), and vice versa. |
A cube can now be generated and oriented, but it still must exist somewhere in a three-dimensional space, and to be seen it must be presented in front of a virtual camera that is also positioned in this space, and is oriented to be pointing at the cube. This section describes how to create a 3D scene for the cube.
In the SilverlightApplication3DTest project, add a new class named Scene.
Add the following code to the class.
using System; using System.Net; using System.Windows; using Microsoft.Xna.Framework; using Microsoft.Xna.Framework.Graphics; namespace SilverlightApplication3DTest { public class Scene { static readonly Color transparent = new Color(0, 0, 0, 0); // avoid allocating in draw Matrix view; // The view or camera transform Matrix projection; // The projection transform to convert 3D space to 2D screen space // The single Cube at the root of the scene Cube Cube = new Cube(); public Scene() { Vector3 cameraPosition = new Vector3(0, 0, 4.0f); // the camera's position Vector3 cameraTarget = Vector3.Zero; // the place the camera is looking // the transform representing a camera at a position looking at a target view = Matrix.CreateLookAt(cameraPosition, cameraTarget, Vector3.Up); } public float AspectRatio { set { // update the screen space transform every time the aspect ratio changes projection = Matrix.CreatePerspectiveFieldOfView(MathHelper.PiOver4, value, 1.0f, 100.0f); } } public void Draw(GraphicsDevice graphicsDevice, TimeSpan totalTime) { // clear the existing render target graphicsDevice.Clear(ClearOptions.Target | ClearOptions.DepthBuffer, transparent, 1.0f, 0); // draw the Cube Cube.Draw(graphicsDevice, totalTime, view * projection); } } }
The cube will be rendered at the origin point in the scene (at the 3D coordinate 0, 0, 0), you can point the camera at the origin and know that the cube should be visible as long as the camera is at an appropriate distance. In this sample, the distance is set so that the cube is dead-centered in the camera’s view (meaning that the X and Y coordinates are zero) and the camera is backed away somewhat from the cube (in this case, at a Z coordinate value of 4.0).
The Scene class has a property whose set method handles the SizeChanged event, which fires when the application’s viewable area is changed (usually by the user resizing the client window). This method ensures that rendering of the scene retains the correct aspect ratio, so that the cube will not be distorted that is out of proportion when the client area changes dimensions.
The Scene class has its own Draw method, which simply clears the graphics device’s resource buffers, and calls Cube’s Draw method, giving it a chance to update its orientation according to the latest information in the static state fields.
This section describes how to create the user interface and drawing surface for the cube. It also describes how to implement the event handling.
Open MainPage.xaml.
Replace the existing Grid with the following XAML.
<Grid x:Name="LayoutRoot" Background="#FF57AECE"> <DrawingSurface Draw="OnDraw" SizeChanged="DrawingSurface_SizeChanged" /> <Slider Height="23" HorizontalAlignment="Left" Margin="50,10,0,0" Name="slider1" VerticalAlignment="Top" Width="100" ValueChanged="slider1_ValueChanged" /> <Slider Height="23" HorizontalAlignment="Left" Margin="50,30,0,0" Name="slider2" VerticalAlignment="Top" Width="100" ValueChanged="slider2_ValueChanged" /> <Slider Height="23" HorizontalAlignment="Left" Margin="50,50,0,0" Name="slider3" VerticalAlignment="Top" Width="100" ValueChanged="slider3_ValueChanged" /> <TextBlock Height="23" HorizontalAlignment="Left" Margin="21,14,0,0" Name="label1" Text="Yaw:" VerticalAlignment="Top" Foreground="White" /> <TextBlock HorizontalAlignment="Left" Margin="17,34,0,243" Name="label2" Text="Pitch:" Foreground="White" /> <TextBlock Height="23" HorizontalAlignment="Left" Margin="23,54,0,0" Name="label3" Text="Roll:" VerticalAlignment="Top" Foreground="White" /> </Grid>
In addition to some labeled sliders that will be used to control the yaw, pitch, and roll of the cube, this XAML creates a DrawingSurface element onto which the 3D scene is portrayed. The DrawingSurface element has two primary events. The first one is SizeChanged, which fires whenever the viewable client area changes. This event was discussed earlier when examining the maintenance of the scene’s aspect ratio.
The other important event is Draw, which fires when one of three things happens: 1) the application is first executed, 2) the primary graphics device changes states from Not Available to Available, or 3) the DrawEventArgs method InvalidateSurface (or DrawingSurface method Invalidate) is called, scheduling a callback that is automatically wired to fire the Draw event again. Although Silverlight 5’s 3D functionality is provided by XNA’s core graphics libraries, there is no 3D render loop that is constantly “ticking” as there is in XNA. Instead, the cycle of InvalidateSurface firing the Draw event is what creates a constantly updating display of the 3D scene.
Open MainPage.xaml.cs.
Add the following code to implement event handling.
using System; using System.Collections.Generic; using System.Linq; using System.Net; using System.Windows; using System.Windows.Controls; using System.Windows.Documents; using System.Windows.Input; using System.Windows.Media; using System.Windows.Media.Animation; using System.Windows.Shapes; using System.Windows.Graphics; namespace SilverlightApplication3DTest { public static class State { public static float sliderYawVal = new float(); public static float sliderPitchVal = new float(); public static float sliderRollVal = new float(); } public partial class MainPage : UserControl { // init the 3D scene Scene scene = new Scene(); public MainPage() { InitializeComponent(); } void OnDraw(object sender, DrawEventArgs args) { // draw 3D scene scene.Draw(GraphicsDeviceManager.Current.GraphicsDevice, args.TotalTime); // invalidate to get a callback next frame args.InvalidateSurface(); } // update the aspect ratio of the scene based on the // dimensinos of the surface private void DrawingSurface_SizeChanged(object sender, SizeChangedEventArgs e) { DrawingSurface surface = sender as DrawingSurface; scene.AspectRatio = (float)surface.ActualWidth / (float)surface.ActualHeight; } private void slider1_ValueChanged(object sender, RoutedPropertyChangedEventArgs<double> e) { State.sliderYawVal = (float)e.NewValue; } private void slider2_ValueChanged(object sender, RoutedPropertyChangedEventArgs<double> e) { State.sliderPitchVal = (float)e.NewValue; } private void slider3_ValueChanged(object sender, RoutedPropertyChangedEventArgs<double> e) { State.sliderRollVal = (float)e.NewValue; } } }
This code includes a class named State that receives updated values from the sliders, making them available to any thread that can process them without requiring a local instance of the data be created first. The reading of these values was shown earlier in the Cube.cs file.
The OnDraw method handles the Draw event, which sets up the InvalidateSurface “loop.” An initial firing of the InvalidateSurface event ensures that the loop gets started. As shown earlier, a call to Scene’s Draw method will fire the Cube’s Draw method.
Lastly, the handler function for SizeChanged shows the AspectRatio property is being set, and the code from Scene shown earlier reveals that the custom set method ensures an appropriate scaling of the visual assets.
Note: