The next step in the natural progression of rendering objects in 3D space is to take some vertices, assign an index list, and render the object using Vertex and Index buffers. We will do this in a building process so you can slowly get the idea of why we would want to use the buffers DirectX provides for us.
Step 1: Putting Together a Vertex Renderer
Since the vertex groupings that we will be rendering will most likely be part of a whole object, I am going to make the VertexGroup class inherit from our main Object. Here is what VertexGroup looks like right now:
public class HMVertexGroup : HMObject { private CustomVertex.PositionColored[] verts; public HMVertexGroup(CustomVertex.PositionColored[] vertex, Device myDevice) { verts = vertex; } public override void Render(Device myDevice) { myDevice.VertexFormat = CustomVertex.PositionColored.Format; myDevice.DrawUserPrimitives(PrimitiveType.TriangleList, 1, verts); } }
Not a whole lot to it eh? If we created an object from this and ran the demo as-is though, we wouldn’t see anything. Why? Because we haven’t set up a camera yet. The camera tutorial comes later, but for now we will set up the matrices on the device that we will need to see our new vertex object. First of all, in the System class, make a new function called SetupCamera inside our HMSystem class like this:
private void SetupCamera() { myDevice.Transform.World = Matrix.Identity; myDevice.Transform.View = Matrix.LookAtLH( new Vector3(0, 0,-5), new Vector3(0, 0, 0), new Vector3(0, 1, 0) ); myDevice.Transform.Projection = Matrix.PerspectiveFovLH( (float)Math.PI / 4, (float)this.Width / (float)this.Height, // floats for decimal based division, not integer 1.0f, 100.0f ); myDevice.RenderState.Lighting = false; }
Also, be sure to add a call to the setup camera function in the contructor for the system somewhere. I put mine inside an if(InitializeGraphics()) call. Now, with our camera all set up, we can create a vertex object in the demo’s Main class and add it to the scene.
CustomVertex.PositionColored[] verts = new CustomVertex.PositionColored[3]; verts[0] = new CustomVertex.PositionColored(new Vector3( 0, 1, 1), Color.Red.ToArgb()); verts[1] = new CustomVertex.PositionColored(new Vector3(-1,-1, 1), Color.Green.ToArgb()); verts[2] = new CustomVertex.PositionColored(new Vector3( 1,-1, 1), Color.Blue.ToArgb()); HMVertexGroup vg1 = new HMVertexGroup(verts, demo1.MyDevice); demo1.MyScene.AddObject(vg1);
Running this now will give us a big red, green, and blue filled triangle. Nice, but it isn’t very useful.
Step 2: Adding a texture to our VertexGroup
Let’s try something different. Change the vertices in the Main class to look like this:
CustomVertex.PositionTextured[] verts = new CustomVertex.PositionTextured[6]; verts[0] = new CustomVertex.PositionTextured(new Vector3(-1, 1, 1), 0, 0); verts[1] = new CustomVertex.PositionTextured(new Vector3( 1, 1, 1), 1, 0); verts[2] = new CustomVertex.PositionTextured(new Vector3(-1,-1, 1), 0, 1); verts[3] = new CustomVertex.PositionTextured(new Vector3( 1, 1, 1), 1, 0); verts[4] = new CustomVertex.PositionTextured(new Vector3( 1,-1, 1), 1, 1); verts[5] = new CustomVertex.PositionTextured(new Vector3(-1,-1, 1), 0, 1); HMVertexGroup vg1 = new HMVertexGroup(verts, “hmglogo.tga”, demo1.MyDevice);
The first part of the new vertices is obvious, it is their position in world space, but what do the second numbers mean? They are texture coordinates. A texture coordinate is assigned as a number from 0 to 1, (0,0) being the top left corner of the texture, and (1,1) being the bottom right. With this knowledge and a bit of a sketch, we can assign the correct coordinates to each of our vertices fairly easily.
Now that we have new vertices, we need to change the VertexGroup class to use the PositionColored format and to have a new parameter for a texture path, and load the texture from it like we did in our sprite class.
public class HMVertexGroup : HMObject { private CustomVertex.PositionTextured[] verts; private Texture myTexture; private string myPath; public HMVertexGroup(CustomVertex.PositionTextured[] vertex, string imagePath, Device myDevice) { verts = vertex; myPath = imagePath; myTexture = TextureLoader.FromFile(myDevice, myPath); } public override void ReloadResources(Device myDevice) { myTexture = TextureLoader.FromFile(myDevice, myPath); } public override void Render(Device myDevice) { myDevice.VertexFormat = CustomVertex.PositionTextured.Format; myDevice.SetTexture(0, myTexture); myDevice.DrawUserPrimitives(PrimitiveType.TriangleList, verts.Length/3, verts); } }
Now you will have a textured quad on the screen that shows the image you supplied in the parameter of the object creation. This doesn’t seem very useful because we already have this exact same ability with our sprite class, and it uses transparency! Also, it seems kind of dumb to be creating 6 vertex elements when we are only making a square.
Step 3: Switching to Vertex and Index buffers
Before we start making a more ‘useful’ use for our VertexGroup, let’s make it make a bit more sense. Wouldn’t it be great if there was some way we could make just 4 vertices and use them in both of our triangles to create our final textured quad? There is! It is called the index buffer, but to use it, we also need to convert to using vertex buffers in our class. Here is what the new class will look like:
public class HMVertexGroup : HMObject { private CustomVertex.PositionTextured[] verts; private int[] inds; private Texture myTexture; private string myPath; private VertexBuffer vb; private IndexBuffer ib; public HMVertexGroup( CustomVertex.PositionTextured[] vertex, int[] index, string imagePath, Device myDevice ) { verts = vertex; inds = index; myPath = imagePath; ReloadResources(myDevice); // This way we don’t have to type the buffer code twice } public override void ReloadResources(Device myDevice) { myTexture = TextureLoader.FromFile(myDevice, myPath); vb = new VertexBuffer( typeof(CustomVertex.PositionTextured), verts.Length, myDevice, Usage.WriteOnly, CustomVertex.PositionTextured.Format, Pool.Default ); vb.SetData(verts, 0, LockFlags.None); ib = new IndexBuffer( typeof(int), inds.Length, myDevice, Usage.WriteOnly, Pool.Default ); ib.SetData(inds, 0, LockFlags.None); } public override void Render(Device myDevice) { myDevice.VertexFormat = CustomVertex.PositionTextured.Format; myDevice.SetTexture(0, myTexture); myDevice.SetStreamSource(0, vb, 0); myDevice.Indices = ib; myDevice.DrawIndexedPrimitives( PrimitiveType.TriangleList, 0, 0, verts.Length, 0, inds.Length / 3 ); } }
The new vertex and index information that you will pass to this class looks like this:
CustomVertex.PositionTextured[] verts = new CustomVertex.PositionTextured[4]; verts[0] = new CustomVertex.PositionTextured(new Vector3(-1, 1, 1), 0, 0); verts[1] = new CustomVertex.PositionTextured(new Vector3( 1, 1, 1), 1, 0); verts[2] = new CustomVertex.PositionTextured(new Vector3(-1,-1, 1), 0, 1); verts[3] = new CustomVertex.PositionTextured(new Vector3( 1,-1, 1), 1, 1); int[] inds = { 0,1,2, 1,3,2 }; HMVertexGroup vg1 = new HMVertexGroup(verts, inds, “hmglogo.tga”, demo1.MyDevice);
Now we are getting somewhere. We have simplified the process of creating and rendering a small number of vertices using index values so we don’t have to create duplicates into some arrays and a single call to our VertexGroup class. Now that we have this great class, what can we do with it? One of the best examples of an object to create this with is a skybox, now that we have our code down for rendering with these buffers, we will make a simple skybox object to add to the engine
Step 4: Taking Advantage of VertexGroup, Making a Skybox
The skybox code will not contain any real DirectX code, it will only be a storage and creation place for the vertex and index arrays and a list of the textures we want to use for each side of the box. Here’s how I have this implemented:
public class HMSkyBox : HMObject { private string[] textures; HMVertexGroup[] faces = new HMVertexGroup[6]; public HMSkyBox(string[] tex, Device myDevice) { textures = tex; CustomVertex.PositionTextured[] verts; int[] inds = { 0,1,2, 1,3,2 }; // Front Face verts = new CustomVertex.PositionTextured[4]; // Do this every face so they reload correctly verts[0] = new CustomVertex.PositionTextured(new Vector3(-10, 10, 10), 0, 0); verts[1] = new CustomVertex.PositionTextured(new Vector3(10, 10, 10), 1, 0); verts[2] = new CustomVertex.PositionTextured(new Vector3(-10, -10, 10), 0, 1); verts[3] = new CustomVertex.PositionTextured(new Vector3(10, -10, 10), 1, 1); faces[0] = new HMVertexGroup(verts, inds, textures[0], myDevice); // Right Face verts = new CustomVertex.PositionTextured[4]; verts[0] = new CustomVertex.PositionTextured(new Vector3(10, 10, 10), 0, 0); verts[1] = new CustomVertex.PositionTextured(new Vector3(10, 10, -10), 1, 0); verts[2] = new CustomVertex.PositionTextured(new Vector3(10, -10, 10), 0, 1); verts[3] = new CustomVertex.PositionTextured(new Vector3(10, -10, -10), 1, 1); faces[1] = new HMVertexGroup(verts, inds, textures[1], myDevice); // Back Face verts = new CustomVertex.PositionTextured[4]; verts[0] = new CustomVertex.PositionTextured(new Vector3(10, 10, -10), 0, 0); verts[1] = new CustomVertex.PositionTextured(new Vector3(-10, 10, -10), 1, 0); verts[2] = new CustomVertex.PositionTextured(new Vector3(10, -10, -10), 0, 1); verts[3] = new CustomVertex.PositionTextured(new Vector3(-10, -10, -10), 1, 1); faces[2] = new HMVertexGroup(verts, inds, textures[2], myDevice); // Left Face verts = new CustomVertex.PositionTextured[4]; verts[0] = new CustomVertex.PositionTextured(new Vector3(-10, 10, -10), 0, 0); verts[1] = new CustomVertex.PositionTextured(new Vector3(-10, 10, 10), 1, 0); verts[2] = new CustomVertex.PositionTextured(new Vector3(-10, -10, -10), 0, 1); verts[3] = new CustomVertex.PositionTextured(new Vector3(-10, -10, 10), 1, 1); faces[3] = new HMVertexGroup(verts, inds, textures[3], myDevice); // Top Face verts = new CustomVertex.PositionTextured[4]; verts[0] = new CustomVertex.PositionTextured(new Vector3(-10, 10, -10), 0, 0); verts[1] = new CustomVertex.PositionTextured(new Vector3(10, 10, -10), 1, 0); verts[2] = new CustomVertex.PositionTextured(new Vector3(-10, 10, 10), 0, 1); verts[3] = new CustomVertex.PositionTextured(new Vector3(10, 10, 10), 1, 1); faces[4] = new HMVertexGroup(verts, inds, textures[4], myDevice); // Bottom Face verts = new CustomVertex.PositionTextured[4]; verts[0] = new CustomVertex.PositionTextured(new Vector3(-10, -10, 10), 0, 0); verts[1] = new CustomVertex.PositionTextured(new Vector3(10, -10, 10), 1, 0); verts[2] = new CustomVertex.PositionTextured(new Vector3(-10, -10, -10), 0, 1); verts[3] = new CustomVertex.PositionTextured(new Vector3(10, -10, -10), 1, 1); faces[5] = new HMVertexGroup(verts, inds, textures[5], myDevice); } public override void ReloadResources(Device myDevice) { foreach(HMVertexGroup vg in faces) { vg.ReloadResources(myDevice); } } public override void Render(Device myDevice) { foreach(HMVertexGroup vg in faces) { vg.Render(myDevice); } } }
The main class of the demo project has also changed to use the new class.
public static void Main() { string[] textures = { @”skybox\jv_arizona_matin_1.jpg”, @”skybox\jv_arizona_matin_2.jpg”, @”skybox\jv_arizona_matin_3.jpg”, @”skybox\jv_arizona_matin_4.jpg”, @”skybox\jv_arizona_matin_5.jpg”, @”skybox\jv_arizona_matin_6.jpg” }; HMSkyBox sky1 = new HMSkyBox(textures, demo1.MyDevice); demo1.MyScene.AddObject(sky1); demo1.Show(); Application.Run(demo1); }
Now, we have a wonderfully textured skybox in our 3D world. Here is where the part that excites me about 3D engines first shows a small glimmer of itself. Inside the .dll file that gets compiled from the main engine code, we have created the tools that will allow us to later create, texture, and render and full skybox with only a few lines of code in our demos. I don’t know about you, but that sure sounds exciting to me.
One last thing before we finish up this tutorial. If you played around with adding more VertexGroups into the scene you have probably noticed that there are some funny things going on. Some of the objects behind the skybox or other objects are getting rendered in front of them, and it altogether just looks strange. The problem is that we haven’t set up the video card to know when a pixel that was already drawn is in front of a pixel that is being drawn, so it just sticks it on top regardless. The solution to our problem is adding a zbuffer, and it only takes a few lines of code.
In the InitializeGraphics function, below the first group of parameter settings, add these lines:
presentParams.EnableAutoDepthStencil = true; presentParams.AutoDepthStencilFormat = DepthFormat.D16;
Now, change the device Clear call to look like this:
myDevice.Clear(ClearFlags.Target | ClearFlags.ZBuffer, Color.CornflowerBlue, 1.0f, 0);
Now everything should render correctly.