Upcoming Events
Unite 2010
11/10 - 11/12 @ Montréal, Canada

GDC China
12/5 - 12/7 @ Shanghai, China

Asia Game Show 2010
12/24 - 12/27  

GDC 2011
2/28 - 3/4 @ San Francisco, CA

More events...
Quick Stats
87 people currently visiting GDNet.
2406 articles in the reference section.

Help us fight cancer!
Join SETI Team GDNet!
Link to us Events 4 Gamers
Intel sponsors gamedev.net search:

  Contents

 Introduction
 Setting up Direct3D
 Rendering a
 Wireframe
 Isometric View

 Let's Texture This
 Beast!

 Adding Lighting
 Something More
 Impressive!

 Wrapping This
 All Up


 Printable version

 


Rendering a Wireframe Isometric View

DEMO1 (see the accompanying source code) draws a scrolling, wireframe isometric display - little more than a bunch of outlined triangles adjacent to one another. This is a good illustration of how isometric tile engines stagger tiles (and of how quads may be broken down into triangles), and is pretty boring… but it's a great start to learning Direct3D. Demo 1, like all of my other demos, is based around a class CEngine. CEngine inherits all of its low-level DirectX initialisation/destruction code from DXEngine.

GameInit, the basic game initialisation, couldn't be simpler for this example. The entire method includes just 2 calls, 1 to initialise DirectDraw/Direct3D and the other to zero my scrolling position counter:

void CEngine :: GameInit() { InitDirectX(); ScrollX = 0; };

GameMain, the function that gets called for each frame, is also pretty simple:

void CEngine :: GameMain() { ScrollX++; if (ScrollX > 64) ScrollX = 0; FillSurface(BBuffer,0,NULL); Demo1Render(ScrollX, 0); Flip(); // check of user is trying to exit if (KEY_DOWN(VK_ESCAPE)) { PostMessage(MainWindow, WM_DESTROY,0,0); } // end if };

The scroller location is incremented and zeroed again if it exceeds the width of a tile. FillSurface is a utility routine I created that simply sets a DirectDraw surface to a solid color; if I didn't take the time to clear the back buffer each frame, things quickly become pretty ugly. Demo1Render is described in detail below - it's the actual Direct3D rendering routine for this example. Finally, the back buffer is flipped, and the program checks to see if the user has pressed ESC to quit.

GameDone is really simple… it does nothing in this example! (The underlying class makes sure that Direct3D/DirectDraw are released properly).

The real meat of this example is the rendering code (I bet you thought I'd never get to it!). The example itself is heavily commented, but here is a step-by-step breakdown of how it works:

First of all, I declare some variables. ScreenX and ScreenY are used to store screen coordinates for rendering. WhereX and WhereY are used to store world coordinates. (If you are confused by these terms, check out one of the tile rendering tutorials… basically, world coordinates work in terms of whole tiles on a larger map, screen coordinates work in terms of pixel locations). The most important variable, however, is the following:

D3DTLVERTEX Vertex[4];

The D3DTLVERTEX structure is central to using Direct3D to improve 2D performance. It is part of Direct3D's "flexible vertex format" system. Other predefined vertex formats include D3DVERTEX and D3DLVERTEX. Each defines a different set of data, and tells the Direct3D pipeline what it needs to do.

  • D3DVERTEX data needs to be both transformed and lit.
  • D3DLVERTEX data already has lighting information, but needs to be transformed.
  • D3DTLVERTEX data already has screen coordinates and lighting information included. As such, it's of the most use in an Enhanced 2D context.

The next step is common to most 3D rendering systems. Every frame of 3D graphics has to be preceded by a call to BeginScene():

D3DDevice->BeginScene();

Next, I inform Direct3D that I have no intention of using its lighting routines. Even though I'm specifying D3DTLVERTEX structures, Direct3D has a habit of wanting to use its own system… so this tells it not to. This doesn't really need to be called every frame, but I kept it in the rendering routine for clarity.

D3DDevice->SetLightState(D3DLIGHTSTATE_MATERIAL,NULL);

Next, and only in this demo, I inform Direct3D that I'd like to render in wireframe mode. This illustrates the SetRenderState command, one of Direct3D's most powerful concepts. D3D maintains a list of variables that may be changed with this command - including fog settings, filtering, perspective correction, and more. Check the SDK for more information.

D3DDevice->SetRenderState(D3DRENDERSTATE_FILLMODE,D3DFILL_WIREFRAME);

Because we are rendering a wireframe demo, we want the lines to be white. This is nice and easy to achieve. For each of the 4 vertices (points at the edges of the square), the color property can be set to white. Direct3D includes a macro, D3DRGB that takes 3 floats (from 0.0f to 1.0f) and converts them into its own D3DCOLOR format:

Vertex[0].color = D3DRGB(1.0f,1.0f,1.0f); Vertex[1].color = D3DRGB(1.0f,1.0f,1.0f); Vertex[2].color = D3DRGB(1.0f,1.0f,1.0f); Vertex[3].color = D3DRGB(1.0f,1.0f,1.0f);

Finishing up the initialisation phase of rendering, WhereY, and WhereX are set to 0. ScreenY is set to -16, ensuring that even if a large scroll offset is in use it will not leave any gaps.

The actual isometric rendering loop is pretty much the same as that described in numerous other articles. It may be summarized as (in pseudocode - see the example for actual code) :

While (ScreenY < ScreenHeight) { If (WhereY MOD 2) = 1 then ScreenX = -64 else ScreenX = -96 While (ScreenX < ScreenWidth) { Setup Vertex Information Render The Triangles ScreenX = ScreenX + 64 (tile width) WhereX = WhereX + 1 } WhereY = WhereY + 1 WhereX = 0 ScreenY = ScreenY + 16 (half the tile height) }

Why did I leave that in pseudocode? Because its pretty standard stuff, and to keep this article short I'd rather focus on the actual rendering code. Besides, pseudocode is good practice. ;-) I've italicized the parts of this loop that concern Direct3D and will be expanded upon.

Setting up the vertex information for rendering a square isn't too hard… although it could be easier. Direct3D doesn't support Quads as a primitive type (OpenGL does). Fortunately, its not all that hard to break a diamond into two triangles. The yellow numbers represent the location of each of the 4 vertices:

An obvious question at this point is… why is 2 the bottom and not the rightmost vertex? The answer is a little optimization known as the TRIANGLESTRIP. Direct3D performs much better if you can batch triangles and send them through the pipeline together. Unfortunately, the triangles you send together have to be using the same texture. Since we will probably want adjacent tiles to look different, I've just grouped two triangles together. Triangles have to be sent to Direct3D in clockwise order - or they don't draw at all! Because of this, I start with the left most triangle:

  and then add one more vertex to get a second one:  

The vertex arrangement has allowed me to just add 1 vertex rather than using a triangle list and listing both triangles in detail. Neat!

The SDK includes some nice pictures illustrating how much farther triangle strips may be taken. Its a problem that you can't change texture during the rendering of a texture strip; sometimes, you might want to glue together a big texture for multiple tiles, but in general their utility is greatly diminished because of this.

Anyway, in terms of this example, the following code fills up the D3DTLVERTEX structures:

Vertex[0].sx = ScreenX + OffsetX; Vertex[0].sy = ScreenY+16 + OffsetY; Vertex[1].sx = ScreenX+32 + OffsetX; Vertex[1].sy = ScreenY + OffsetY; Vertex[2].sx = ScreenX+32 + OffsetX; Vertex[2].sy = ScreenY+32 + OffsetY; Vertex[3].sx = ScreenX+64 + OffsetX; Vertex[3].sy = ScreenY+16 + OffsetY;

There is plenty of room to optimize these allocations - but I wanted the code to remain clear. OffsetX and OffsetY are the key to smooth pixel scrolling - they are simply a pixel offset by which the entire image is shunted. sx and sy in each vertex define screen positions. Vertex[0] is the left of the diamond, Vertex[1] if the top of the diamond, Vertex[2] is the bottom, and Vertex[3] is the right side. Nothing too revolutionary here!

Finally for the inner loop, the quad is sent off to be rendered:

D3DDevice->DrawPrimitive(D3DPT_TRIANGLESTRIP,D3DFVF_TLVERTEX,Vertex,4,D3DDP_WAIT);

DrawPrimitive is the basis of modern Direct3D rendering. All this does is:

  1. Tell DrawPrimitive that Vertex contains a triangle strip (as opposed to a triangle list - which would need 6 vertices)
  2. Explains that the vertices are already transformed and lit (D3DFVF_TLVERTEX)
  3. Indicates where D3D may find the vertex information.
  4. Notes that there are 4 vertices [0-3]
  5. Tells DrawPrimitive to wait if it has to before rendering. You can use 0 for this parameter and it should still work.

Lastly for the render routine, you have to call EndScene: D3DDevice->EndScene();

To recap, this example has shown you how to fire up Direct3D, render wireframe quads, and perform basic isometric scrolling. The tile engine assumes 64x32 tiles, but could be easily adapted for almost any other size. Not bad for a 47 k executable!





Next : Let's Texture This Beast!