Click here to Skip to main content
15,881,882 members
Articles / Multimedia / DirectX
Article

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

Rate me:
Please Sign up or sign in to vote.
3.27/5 (12 votes)
25 Jun 20046 min read 111.2K   3.9K   24   15
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:

C#
private static Device device = null;
C#
// 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.

C#
// 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.

C#
// 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.

C#
// 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.

C#
// 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.

C#
// 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.

C#
// 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:

C#
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:

C#
// 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.

C#
// 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:

C#
// 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:

C#
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.

C#
// 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.

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

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

C#
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


Written By
Web Developer
United States United States
Karl Baum is president and CEO of KGB Technologies. KGB Technologies specializes in custom application development for businesses. Karl holds M.S. in Computer Science from Syracuse University and is currently pursuing his Ph.D. in Imaging Science.

Comments and Discussions

 
GeneralInteresting approach, but Pin
Steve Hawley5-Feb-07 9:48
Steve Hawley5-Feb-07 9:48 
GeneralRe: Interesting approach, but Pin
Karl 20004-Mar-07 16:20
Karl 20004-Mar-07 16:20 
GeneralGreat article Pin
tess_gain17-Apr-06 11:22
tess_gain17-Apr-06 11:22 
QuestionAttaching Font / Text To Surface Pin
Vahe Karamian27-Jan-06 17:29
Vahe Karamian27-Jan-06 17:29 
AnswerRe: Attaching Font / Text To Surface Pin
Karl 200028-Jan-06 6:30
Karl 200028-Jan-06 6:30 
QuestionQuestion? Pin
Anonymous31-Aug-05 6:39
Anonymous31-Aug-05 6:39 
AnswerRe: Question? Pin
Karl 200031-Aug-05 9:00
Karl 200031-Aug-05 9:00 
GeneralReally Nice! Pin
bneacetp16-May-05 21:55
bneacetp16-May-05 21:55 
GeneralRe: Really Nice! Pin
Karl 200017-May-05 3:01
Karl 200017-May-05 3:01 
GeneralGreat, but some suggestions... Pin
Patrik Svensson28-Jun-04 15:35
Patrik Svensson28-Jun-04 15:35 
I hope that you understand that this is constructive critizism because i think your work is really cool.

1) Try to write some text ABOUT the code instead of pasting the whole source-code.
2) Instead of using HTML to make the code easier to read, try to use the codeprojects "pre" and "code" tags.
3) Shouldn't this article be in the C# section?

As I said before. It's a great article and if you fix these things, more people will probably take their time to read it.


"If knowledge can create problems, it is not through ignorance that we can solve them."
Isaac Asimov
GeneralRe: Great, but some suggestions... Pin
Karl 200029-Jun-04 3:18
Karl 200029-Jun-04 3:18 
GeneralRe: Great, but some suggestions... Pin
Karl 200029-Jun-04 6:33
Karl 200029-Jun-04 6:33 
GeneralRe: Great, but some suggestions... Pin
Anonymous29-Jun-04 19:37
Anonymous29-Jun-04 19:37 
GeneralCrashes on my older video card Pin
Dan Colasanti26-Jun-04 20:08
professionalDan Colasanti26-Jun-04 20:08 
GeneralRe: Crashes on my older video card Pin
Karl 200027-Jun-04 7:16
Karl 200027-Jun-04 7:16 

General General    News News    Suggestion Suggestion    Question Question    Bug Bug    Answer Answer    Joke Joke    Praise Praise    Rant Rant    Admin Admin   

Use Ctrl+Left/Right to switch messages, Ctrl+Up/Down to switch threads, Ctrl+Shift+Left/Right to switch pages.