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 presentParams = new PresentParameters();
presentParams.Windowed = true;
presentParams.SwapEffect = SwapEffect.Discard;
presentParams.AutoDepthStencilFormat = DepthFormat.D16;
presentParams.EnableAutoDepthStencil = true;
device = new Device(0, DeviceType.Hardware, this,
CreateFlags.SoftwareVertexProcessing, presentParams);
device.DeviceReset += new EventHandler(this.OnDeviceReset);
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.
device.Transform.Projection =
Matrix.PerspectiveFovLH((float)Math.PI/4,
this.Width/this.Height,1.0f,100.0f);
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.
device.RenderState.Lighting = true;
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.
dotMesh = Mesh.Sphere(device, .2f, 18, 18);
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
.
device.Material = dotMaterial;
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.
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.
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:
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);
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.
private static VertexBuffer vb = null;
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:
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.
device.Material = material;
device.VertexFormat = CustomVertex.PositionTextured.Format;
device.SetStreamSource(0,vb,0);
device.SetTexture(0,top);
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!!