Click here to Skip to main content
65,938 articles
CodeProject is changing. Read more.
Articles
(untagged)

Introduction to Managed Direct3D 9 - Creating a 3D clock (updated 6/29/04)

0.00/5 (No votes)
25 Jun 2004 1  
This article shows you how to create a simple Direct 3D application. You will create a 3 dimensional clock using simple meshes and textures.

Sample Image - spinningnumbersclock.jpg

Introduction

This project is written in C# and uses Managed Direct X 9. You will need to have the SDK installed to use this. The code was created with Visual Studio 7.1.

This project is a clock. Each digit of the clock is represented by pages. Each page contains the top half of a number on top and the bottom half on bottom. Two pages are shown next to each other, one with the top of the number and one with the bottom of the number. Put them together and you have a whole number. When shown this way, you can flip the pages, going from one number to another. See the last digit in the picture for an example of this, it is hard to explain.

My code is fairly well documented, so you can probably learn how it did this just by looking at it. Here, I will discuss some of the more important parts.

To do anything, we need to get the device that we will be drawing to. We also need to set up that device. The following code does this:

private static Device device = null;
// PresentParameters are used for the display device

PresentParameters presentParams = new PresentParameters();
// this is a windowed application

presentParams.Windowed = true;
// set the swap effect

presentParams.SwapEffect = SwapEffect.Discard;
// tell the device to use depth information

// and format to use, this will insure that things that

// should be infront will be

presentParams.AutoDepthStencilFormat = DepthFormat.D16;
presentParams.EnableAutoDepthStencil = true;
// create our display device

// we are using the default display device, and doing

// the vertex processing with the cpu, this will insure

// compatability with older cards

device = new Device(0, DeviceType.Hardware, this, 
         CreateFlags.SoftwareVertexProcessing, presentParams);
// we subscribe to the device reset event. A reset occurs

// when something occurs with our window 

//such as resizing.

// We need to capture this event because during a reset

// we lose any information in video memory. 

//Therefore we need to setup some stuff again.

device.DeviceReset += new EventHandler(this.OnDeviceReset);
// Set up the device now

this.OnDeviceReset(device, null);

We need to respond to the DeviceReset event so that our application will survive the lose of its video memory. In the device reset event, we set up the world we are drawing onto. This includes the amount of world we can see (a.k.a. viewing frustum). This is done by setting the projection matrix. By setting the projection matrix, we are defining the near and far plane, as well as the angle from the camera. This angle determines how much of the near and far plane are visible. We also setup the camera. This is done by setting the view matrix. We need to supply the information for where we want the camera, what we want the camera to be looking at, and which direction is up.

// Set up the projection matrix, this defines the volume

// that is visible on the screen, and its perspective.

device.Transform.Projection = 
        Matrix.PerspectiveFovLH((float)Math.PI/4, 
        this.Width/this.Height,1.0f,100.0f);
// Set up the view matrix. This is our camera.

// The camera will be located along the z axis, 

// looking at the origin, which is where we will display the clock.

device.Transform.View = Matrix.LookAtLH(new Vector3(0,0,10f), 
                             new Vector3(),new Vector3(0,1,0));

The other things I choose to do in DeviceReset is set up the lighting. I need to use lighting because I am going to be using a mesh that does not contain color information. The simplest type of lighting is ambient. Ambient light affects everything in your scene equally. We set our ambient light to white, so things appear the color they really are.

// turn on lighting

device.RenderState.Lighting = true;
// place a ambient white light in our scene. An ambient light

// touches all surfaces that we draw, and 

// affects them all equally. We set it to white so that the surfaces

// will look the color they actually are.

device.RenderState.Ambient = Color.White;

Now that our device is set up, we can draw whatever we want in the OnPaint function. I will first discuss drawing the dots, and then I will discuss drawing the numbers. The dots are drawn using a mesh. We use one of Direct3D's built in mesh types. When creating the mesh, we need to tell it what device will be using it and information about the size and density that we desire. The mesh, by default, contains no color information. To add color information to the mesh, we use a material. We create a material that acts like it is black when ambient light is shown on it.

// create the mesh for our dot

dotMesh = Mesh.Sphere(device, .2f, 18, 18);
// because the default sphere mesh contains

// no color information for it's vertices,

// we will use ambient lighting to provide this color.

// the dot should reflect ambient light as if it is a black surface

// the color the dot will actually appear is based

// on its color and the color of the light

dotMaterial = new Material();
dotMaterial.Ambient = Color.Black;

Now that we have our mesh, we can easily draw it. We first need to tell the device what material it should use, and what texture it should use. We do not have a texture for our mesh, so we set this to null.

// set the material and texture. These are properties

// of the device. All things drawn

// will use this material and texture.

// set the material to the dot material

device.Material = dotMaterial;
// set the texture to null

device.SetTexture(0,null);

Our final say on how our mesh will be drawn is done with the world transform. By default, our mesh is located at the origin. We will use a translation (movement) to place our mesh where we would like the dot to be located.

// translate (move along the x,y,z axis) all things draw

device.Transform.World = Matrix.Translation(2.5f,.6f,0);

Now, we draw our mesh. Each mesh is composed of subsets, which are little pieces of the mesh. When drawing a mesh, you need to draw each of its subsets. However, since we have a really simple mesh, it only has one subset. To actually draw our mesh, we draw this one subset. Notice that we don't need to specify the device to draw on because we have already told the mesh which device it was meant to be used with.

// draw one dot

dotMesh.DrawSubset(0);

Now, we will talk about drawing our numbers. In my code, each number is a class and is drawn by calling its Draw function. I will go into detail about how the class draws itself. Each page of our number has a front and a back face. For example, one of the pages has the top half of the 0 on its front and the bottom half of the 1 upside down on its back. That way, as the top of the 0 gets turned down, the bottom of the 1 becomes visible in the right location. Each of the pages has 2 textures associated with it. A texture is basically a bitmap. The textures have the bitmaps of the half of numbers we just talked about. The following code loads all of our textures from embedded resources:

texTop = new Texture[10];
texTop[0] = new Texture(device, new Bitmap(this.GetType(), 
                "0top.bmp"),Usage.Dynamic, Pool.Default);
texTop[1] = new Texture(device, new Bitmap(this.GetType(),
                "1top.bmp"),Usage.Dynamic, Pool.Default);
texTop[2] = new Texture(device, new Bitmap(this.GetType(),
                "2top.bmp"),Usage.Dynamic, Pool.Default);
texTop[3] = new Texture(device, new Bitmap(this.GetType(),
                "3top.bmp"),Usage.Dynamic, Pool.Default);
texTop[4] = new Texture(device, new Bitmap(this.GetType(),
                "4top.bmp"),Usage.Dynamic, Pool.Default);
texTop[5] = new Texture(device, new Bitmap(this.GetType(),
                "5top.bmp"),Usage.Dynamic, Pool.Default);
texTop[6] = new Texture(device, new Bitmap(this.GetType(),
                "6top.bmp"),Usage.Dynamic, Pool.Default);
texTop[7] = new Texture(device, new Bitmap(this.GetType(),
                "7top.bmp"),Usage.Dynamic, Pool.Default);
texTop[8] = new Texture(device, new Bitmap(this.GetType(),
                "8top.bmp"),Usage.Dynamic, Pool.Default);
texTop[9] = new Texture(device, new Bitmap(this.GetType(),
                "9top.bmp"),Usage.Dynamic, Pool.Default);
texBottom = new Texture[10];
texBottom[0] = new Texture(device, new Bitmap(this.GetType(),
                   "0bottom.bmp"),Usage.Dynamic, Pool.Default);
texBottom[1] = new Texture(device, new Bitmap(this.GetType(),
                   "1bottom.bmp"),Usage.Dynamic, Pool.Default);
texBottom[2] = new Texture(device, new Bitmap(this.GetType(),
                   "2bottom.bmp"),Usage.Dynamic, Pool.Default);
texBottom[3] = new Texture(device, new Bitmap(this.GetType(),
                   "3bottom.bmp"),Usage.Dynamic, Pool.Default);
texBottom[4] = new Texture(device, new Bitmap(this.GetType(),
                   "4bottom.bmp"),Usage.Dynamic, Pool.Default);
texBottom[5] = new Texture(device, new Bitmap(this.GetType(),
                   "5bottom.bmp"),Usage.Dynamic, Pool.Default);
texBottom[6] = new Texture(device, new Bitmap(this.GetType(),
                   "6bottom.bmp"),Usage.Dynamic, Pool.Default);
texBottom[7] = new Texture(device, new Bitmap(this.GetType(),
                   "7bottom.bmp"),Usage.Dynamic, Pool.Default);
texBottom[8] = new Texture(device, new Bitmap(this.GetType(),
                   "8bottom.bmp"),Usage.Dynamic, Pool.Default);
texBottom[9] = new Texture(device, new Bitmap(this.GetType(),
                   "9bottom.bmp"),Usage.Dynamic, Pool.Default);

To actually draw our numbers, we need to draw primitives (in our case triangles) that the textures will be drawn on. The drawing is done with triangles because there are no squares. We make a square by sticking 2 triangles together. Therefore, each of our textures will be drawn by specifying 6 points or vertices. These size points will then be connected to form 2 triangles. One important detail is that we will be listing the vertices in counter-clockwise order. DirectX looks at the order that the vertices are listed in to determine if we are facing the texture. If we are not, it won't draw it unless we specifically tell it to. Because of this little thing, our page is actually consisted of 12 vertices, 6 for the front and 6 for the back. We define our vertices with the following code:

// the coordinates in the front face of the page,

// this is the top half of a number

verts[0] = new Microsoft.DirectX.Direct3D.CustomVertex.PositionTextured(-1.0f, 
                                                          1.0f,0.0f,1.0f,0.0f);
verts[1] = new Microsoft.DirectX.Direct3D.CustomVertex.PositionTextured(-1.0f, 
                                                          0.0f,0.0f,1.0f,1.0f);
verts[2] = new Microsoft.DirectX.Direct3D.CustomVertex.PositionTextured(1.0f, 
                                                          1.0f,0.0f,0.0f,0.0f);
verts[3] = new Microsoft.DirectX.Direct3D.CustomVertex.PositionTextured(-1.0f,
                                                          0.0f,0.0f,1.0f,1.0f);
verts[4] = new Microsoft.DirectX.Direct3D.CustomVertex.PositionTextured(1.0f, 
                                                          0.0f,0.0f,0.0f,1.0f);
verts[5] = new Microsoft.DirectX.Direct3D.CustomVertex.PositionTextured(1.0f, 
                                                          1.0f,0.0f,0.0f,0.0f);
// the coordinate in the back face of the page,

// this is the bottom half of a number

verts[6] = new Microsoft.DirectX.Direct3D.CustomVertex.PositionTextured(-1.0f, 
                                                          1.0f,0.0f,1.0f,1.0f);
verts[7] = new Microsoft.DirectX.Direct3D.CustomVertex.PositionTextured(1.0f, 
                                                          1.0f,0.0f,0.0f,1.0f);
verts[8] = new Microsoft.DirectX.Direct3D.CustomVertex.PositionTextured(1.0f, 
                                                          0.0f,0.0f,0.0f,0.0f);
verts[9] = new Microsoft.DirectX.Direct3D.CustomVertex.PositionTextured(1.0f, 
                                                          0.0f,0.0f,0.0f,0.0f);
verts[10] = new Microsoft.DirectX.Direct3D.CustomVertex.PositionTextured(-1.0f, 
                                                          0.0f,0.0f,1.0f,0.0f);
verts[11] = new Microsoft.DirectX.Direct3D.CustomVertex.PositionTextured(-1.0f, 
                                                          1.0f,0.0f,1.0f,1.0f);

Notice that each vertex is defined by 5 numbers. The x, y, and z coordinates of the vertex, and the x and y coordinate of the texture that should line up with the vertex. (Note: these coordinates are normalized to a 0-1 scale.) When drawing our triangles, the texture will be laid on them in such a way that the coordinates of the texture will match up with those that we assign to the vertex.

We place the vertices in a vertex buffer that we created using the following code. We will provide this buffer to the device when drawing our vertices.

// vertex buffer storing coordinates used when drawing digit

private static VertexBuffer vb = null;
// create vertex buffer

vb = new VertexBuffer(typeof(CustomVertex.PositionTextured), 
         12,device,Usage.Dynamic | Usage.WriteOnly, 
         CustomVertex.PositionTextured.Format, Pool.Default);

We place the vertices in the buffer using the following line of code:

// take the date from verts and put in our vertex buffer

buffer.SetData(verts,0,LockFlags.None);

The last thing we need to do before drawing our page is setting up the material. Again, we are not supplying any color information with our vertices, and so we need to do this:

private static Material material;
material = new Material();
material.Ambient = Color.White;

Now, we draw our page. First, we tell the device what material and texture to use. We also tell it what format our vertices are in, and where to find our vertices. To make our page appear at the location and at the rotation that we would like, we change to world transform.

// set up the material, so light will get reflected

device.Material = material;
// tell the device what format the vertices we will be drawing are in

device.VertexFormat = CustomVertex.PositionTextured.Format;
// tell the device which buffer to get the vertices from

device.SetStreamSource(0,vb,0);
// set texture for front of page and draw it

device.SetTexture(0,top);
// set world transform to draw page at right location and angle

device.Transform.World = Matrix.RotationX(angle) * Matrix.Translation(x,y,z);

We use DrawPrimitives to actually draw our pages (remember they are really triangles). We need to specify what type of primitive, what index in our buffer to start at, and how many primitives we will be drawing.

device.DrawPrimitives(PrimitiveType.TriangleList,0,2);

To draw the back face, we just change the texture and draw the triangles defining the back face.

device.SetTexture(0,bottom);
device.DrawPrimitives(PrimitiveType.TriangleList,6,2);

We repeat these processes for each page, making up our number.

I hope that you could understand and follow my explanation. Don't hesitate to ask for more information.

Enjoy!!

License

This article has no explicit license attached to it but may contain usage terms in the article text or the download files themselves. If in doubt please contact the author via the discussion board below.

A list of licenses authors might use can be found here