Click here to Skip to main content
15,879,474 members
Articles / Desktop Programming / MFC
Article

MFC D3D Application - Direct3D Tutorial Part III

Rate me:
Please Sign up or sign in to vote.
4.82/5 (23 votes)
20 Jul 200723 min read 60.5K   58   4
Part III of the step by step MFC + D3D tutorial, with custom Direct3D framework

Sample Image - halfcap.gif

Introduction

Finally, the long awaited Part III of the custom Direct3D framework and 3D tutorial, encapsulated in class CXD3D. In Part I we covered some 3D concepts, some Direct3D architecture and the enumeration object; you'll also find the demo project there. In Part II we covered the selection of 3D settings from the enumeration objects and the rendering loop. In this issue, we'll cover coordinate spaces and transformations from one to another, along with the actual geometrical data placeholders or buffers.

Transformations

Transformations are used to convert object geoemtry from one coordinate space to another. The most common transformations are done using matrices. A matrix is an essential tool for holding the transformation values and applying them to the data.

So we are dealing with matrix algebra to perform 3D geometrical transformations, to ultimately move, rotate and scale objects, to express the location of an object relative to another object, and to change the viewing position, direction, and/or perspective.

The world transformation (or world matrix) essentially places all objects in the scene into a single reference coordinate system. Picture a unit cube centered at the origin; if the world matrix holds a translation of (5,5,5), the cube gets pushed 5 units to the right, 5 units up and 5 units back. Formally, the object has been converted from object space to world space.

In object space (a.k.a. model space or local space), all of 0the object vertices are defined relative to a common origin. In the example, the unit cube is defined centered at (0,0,0), so that its upper front right corner is at (0.5,0.5,-0.5); in world space, that corner is actually at (5.5,5.5,4.5).

The world transformation can include any combination of translations, rotations, and scalings, provided you follow the matrix algebra rules to construct it. Nevertheless, most applications should avoid changing the world transformation too frequently, for performance reasons.

Think of a set of chairs in a room; you might define the model for a single chair (i.e., a vertex buffer resource), setup the world matrix to place the first chair, change the world matrix to place the second chair, and so forth; when you are done with all the chairs, you restore the world matrix to its original state, so that the rest of the scene renders correctly.

This is nice for say 6 chairs, but if it is a thousand chairs we are talking about, or if the chairs are meant to be kicked around, your application will take a noticeable performance hit when there are too many transformations taking place, so you might need to consider a different approach. Incidentally, simulating a single object that can move (or be kicked) around with a certain degree of fidelity to reality, invloves the laws of physics, and many, many more transformations in real time.

You can compromise by defining groups of chairs, by keeping separate transform matrices for each chair or group, by transforming the vertices manually, by using index buffers or vertex shaders, for whatever they are, by pre-transforming each chair into place and orientation, or by combining two or more of these approaches. It ultimately depends on what it is you want to do with chairs in the scene, so try to design ahead of a potential bottleneck in your application and keep transformation changes to a minimum, because they can be costly operations.

After you place objects in the world, you place the viewer into it; consequently, object world coordinates are transformed with the view transformation.

The view transformation (or view matrix) converts vertices in world space to viewer, eye or camera space. Let's start with the viewer at the origin, looking into positive z, in world space: the view matrix is the identity matrix, and the viewer can see the unit cube hanging out there at (5,5,5). Now the viewer moves up 10 units; the cube descends to (5,-5,5) in camera space, and is probably no longer visible. Now the viewer turns until he is looking straight down; the cube rotates along the x-axis 90 degrees counterclockwise, until it stops at (5,5,5) in camera space; if the viewer now moves forward (into world space positive-z), the cube will move down in camera space. In a sense, is like a cat and mouse game, with objects moving and rotating the exact opposite amount of what the viewer is doing.

I know the example is not easy to visualize, so try to think of the view transformation as the simulation of a video camera moving around through the world; it is not the objects that are moving, it is the camera (or the viewer, i.e., you, there is no spoon), but to simulate the camera movement in 3D graphics, all objects must be transformed to a unique reference coordinate system, in which the camera is always at the origin, looking in the positive z-direction, i.e., camera space.

In most 3D games, the camera is constantly moving around and looking into different directions, i.e. the view is constantly changing, i.e. the view matrix must be constantly updated with a combination of translations and rotations along each axis, ergo, 3D applications will again show considerable lag if there are too many objects to transform.

The D3DXMatrixLookAtLH Direct3D extensions function makes it really easy to setup a view matrix given the viewer's 'eye' location and look-at direction in world space. It also takes in a "which way is up?" direction, typ. (0,1,0). Incidentally, if 'up' is (0,-1,0), you are actually using the y-axis inversion feature, popular in some first-person shooters and flight simulators, in which if you push a lever or a stick upwards or to the front, you are actually nose diving.

There is also a D3DXMatrixLookAtRH function, for right-handed coordinate systems, but Direct3D uses a left-handed coordinate system, in which your left hand's thumb points in the positive-z direction. Using (or porting from) a right-handed coordinate system is not easy in Direct3D; it affects the data arrangement and the transformations, so be sure to read the SDK help if you have no choice but to use a RH coordinate system, for it is out of the scope of this article.

There's yet another transformation objects go through: the projection transformation. This is the most complicated one; start by thinking of it as the camera's internals, analogous to choosing a lens for the camera.

Cameras have a certain Field of View (FOV), in simple terms, the maximum spread they can cover. The people that will not make it into the family picture are simply out of the horizontal FOV of the camera, and the treetop behind them is out of the vertical FOV. If you zoom in, less people will make it into the picture, and if you zoom out, more will, so the FOV is affected by the level of magnification, or zoom.

Before taking the shot, you also want to position yourself so that everything's in focus, and that nothing irrelevant to the picture is obstructing it, like that idiot cousin of yours standing somewhat between you and the crowd.

Whatever finally makes it into the shot is inside of a well defined volume: a pyramid with the tip at the camera's position cut by a near clipping plane defined by the closest object, that hot blonde of a third cousin, and a far clipping plane defined by the furthest away object, the mountain range in the background. Such a volume actually has a name, and it is a view frustum.

In 3D graphics, you get to define the view frustum, so that even if your idiot cousin is standing right in front of the camera he can be "clipped" away from the view frustum entirely by setting the near clipping plane to right before the blonde. Furthermore, if you push it a little more, you'll trim down her nose, incidentally her only defect. The point is that only objects (or object sections) inside the view frustum are rendered, and the rest of the world is clipped away from the view.

This setup allows 3D engines to render portions of the world at given times, i.o. the entire world at all times, with a corresponding efficiency boost, but also with an inescapable additional complexity.

I must insist on the view frustum being an artificial construct, for you cannot define the near clipping plane in the analogy; it is though, a pragmatic approach to 3D scene rendering. Incidentally, a typical flaw in 3D games is the see-through wall, one you get so close to that it gets clipped away, allowing you to see everything behind it.

Ok, so the view frustum parameters, namely horizontal/vertical FOVs, and near/far clipping planes, are used to construct a scale and perspective matrix, which is the typical projection matrix most 3D applications use, in order to make objects near the camera appear bigger than objects in the distance.

Formally, the projection transformation converts everything inside the view frustum (in camera space) to projection space. In practice, the projection matrix performs translation and scaling based on the view frustum parameters. I will not go into the math details of such a construct, you can read more about it in the SDK docs; the D3DXMatrixPerspective* set of functions wrap the construction of the projection matrix, but we'll get there after addressing the last geometrical transformation, next!

The projected 3D scene is ultimately presented on a 2D (flat) rectangular section within the render-target, a.k.a., the viewport rectangle. This is the final coordinate system conversion, from projection space to screen plane, or if you prefer, from projection space to viewport rectangle.

Pixel addressing designates the top-left corner of the screen as the origin; consequently, the positive-y direction is actually down, exactly the opposite of what it is in projection space. Furthermore, the viewport rectangle might evaluate to the entire render-target area, but not necessarily so, in which case additional scaling to viewport dimensions is required. This transformation is carried out internally by Direct3D, but it can be tweaked by changing the viewport parameters and the SetViewport device method.

The D3DVIEWPORT9 structure's X, Y, Width, and Height members describe the position and dimensions (in pixels) of the viewport rectangle on the render-target surface. When applications render to the entire target-surface, say a 640x480 surface, these members should be set to 0, 0, 640, and 480, respectively. The structure also holds a depth range in the MinZ and MaxZ members, but do not confuse these with the clipping planes; these members are typically set to 0.0 and 1.0 to indicate we want to render the entire range of depth values of the projection space, but can be set to other values to achieve specific effects. For example, you might set them both to 0.0 to force the system to render objects to the foreground of a scene, like a HUD, or both to 1.0 to force other objects into the background, like the sky.

Ok, so here is the connection between setting up the projection transformation and the viewport rectangle. It is actually a practical workaround to calculating the x- and y-scaling coefficients of the projection matrix.

Instead of providing horizontal and vertical FOVs, which carry on complex trigonometrical functions to determine the scaling parameters, we can use the viewport's width and height to factor the near clipping plane and achieve the same results. This just derives from the trigonometry of the view frustum, and Direct3D takes advantage of it to save at least one costly 1/tan(0.5 * FOV) operation. In fact, there is no D3DXMatrixPerspective* function taking in both FOVs; they either take none, or just the vertical FOV, along with the aspect ratio of the viewport, the width to height relation.

In a framework based, typical application the viewport dimensions are internally setup to match the presentation parameters' BackBufferWidth and BackBufferHeight, so that finally, the projection matrix can be easily setup like in the following example:

C++
// setup the projection matrix
float fAspect = (float)m_d3dpp.BackBufferWidth /
                (float)m_d3dpp.BackBufferHeight;

D3DXMatrixPerspectiveFovLH(&m_matProj,  // output
                           D3DX_PI / 4, // vertical FOV
                           fAspect,     // viewport aspect ratio
                           1.0e-1f,     // near clipping plane z
                           1.0e+3f);    // far clipping  plane z

m_pd3dDevice->SetTransform(D3DTS_PROJECTION, &m_matProj);

Finally, I will justify these typical argument values.

The human eye is capable of a 180° (PI) FOV, but we normally pay attention to the 45° in the middle, so 45° (PI/4) is a typical value for the FOV. Incidentally, a good car driver has inadvertedly trained his eyes to a 120° or more FOV, to cover the road and every rearview mirror, and fast readers use the same trick. That said, there are of course birds capable of 360°, but hopefully they won't be playing our 3D games. To achieve some "realism" in your scenes, use something between PI/3 and PI. To acieve the distortion effect of a fisheye (wide-angle) lens, use something between 100° and just below 180°, for Direct3D will not recognize PI or anything above it as a valid FOV.

The aspect ratio of typical fullscreen displays is 4:3, or 1.33:1, resulting from 1024/768, 800/600 and even 640/480, or one and a third times as a wide as they are tall, incidentally, the same used for standard TV. HDTV features an aspect ratio of 16:9, and common motion-picture ratios are 1.85:1 and 2.35:1, all around twice as wide as they are tall. You are always better off matching the viewport dimensions, again to achieve some realism, but then again you can tweak it to stretch or compress the image to achieve some special effect.

The clipping planes, ah, the clipping planes! The values are as important as the ratio between them. In the example shown above, objects as close as 0.1 units and as far as a 1000 units, will be rendered, but the ratio between the planes is 10000, which is around the maximum for typical applications. The thing is that the ratio strongly affects the (ideally even) distribution of depth values across the depth buffer range, especially for the case of 16-bit z-buffers, the most commonly supported ones. This can cause rendering artifacts between close objects (known as z-buffer fighting), and hidden surface artifacts in distant objects. There is a great page by Steve Baker explaining all you need to know about this at Learning to Love your Z-buffer, so I will just recommend keeping the near plane as far as you can tolerate, as he does, and keeping a 1000 to 10000 ratio, as the SDK help recommends.

Tip

There are too many images to help you grasp these concepts, either on the web or the SDK help, but I purposedly included none; they are ultimately projections on paper (or web pages) and that's the whole point of 3D in your computer, isn't it? Try to visualize these verbal "images" mentally, for it will help a lot in improving your 3D visualization skills. This is a key feature of us arguably intelligent beings, so put your brain to some good use by reading again until you get it! Ok, so maybe there's also a lazyness factor in including images, but then again it's my article and I cry if I want to.

Final words on 3D Transformations

What else can I say about them? Well, first, you cannot escape them; second, they are part of the rendering state, so restrain yourself from changing them too frequently; third, the projection matrix does not change at all in most applications, so you can typically set it up in RestoreDeviceObjects and leave it alone for the rest of the entire rendering loop; fourth, in certain cases, you might be able to combine the world and view matrices into a single world-view matrix and use it as the world matrix, gaining some performance; fifth, choose the clipping planes carefully; and ultimately, play with/tweak the matrices to see what happens!

Finally, use the CD3DArcBall (d3dutil.h) class to setup the world (or view) matrix from mouse input; most of the SDK samples do, letting you rotate, translate and zoom the entire scene with the mouse. Incidentally, my humble contribution to the class is a handler for the WM_MOUSEWHEEL message, but I will let you implement it, as homework.

And, finally (for real), remember that 3D transformations operate on vertex data, the collection of points in space that define our models or objects, which leads to the next topic, vertex buffers.

Vertex Buffers

Vertex buffers are Direct3D resources living in memory, represented by their own IDirect3DVertexBuffer9 interface, that can be rendered through the use of device methods.

The vertex buffer format is flexible0 by design. This allows applications to extend the definition of a vertex from simple positional (x,y,z) data to positional data plus normal, point size, diffuse color, specular color, up to eight texture coordinate data sets, up to three vertex blending weights, or pre-transformed and lit vertices, a.k.a. TL-vertices for use by both the fixed vertex pipeline or the programmable vertex pipeline.

OK, so we are way over our heads into this. Let's start with a simple flexible vertex format (FVF) that includes untransformed positional data, and diffuse color.

C++
// first, you define the format as a combination of D3DFVF flags:
#define D3DFVF_CUSTOMVERTEX D3DFVF_XYZ | D3DFVF_DIFFUSE

// next, you define a custom vertex as:
struct CUSTOMVERTEX
{
    FLOAT x, y, z;  // position
 DWORD color;  // diffuse ARGB color
};

// so that you can create a square with the following data:
static CUSTOMVERTEX s_Vertices[] =
{
 // x      y     z     ARGB color
 { -1.0f, -1.0f, 0.0f, 0xFFFF0000 }, // bottom-left corner, red
 { +1.0f, -1.0f, 0.0f, 0xFF00FF00 }, // bottom-right corner, green
 { +1.0f, +1.0f, 0.0f, 0xFF0000FF }, // top-right corner, blue
 { -1.0f, +1.0f, 0.0f, 0xFFFFFFFF }, // top-left corner, white
};

If you wanted to add a couple of (u,v) texture coordinate sets, both the FVF and the custom vertex structure must accomdate for them, with additional help from some D3DFVFmacros:

C++
#define D3DFVF_CUSTOMVERTEX D3DFVF_XYZ | \
        D3D_FVF_DIFFUSE         | \
        D3DFVF_TEX2             | \ // 2 set of texture coordinates
        D3DFVF_TEXCOORDSIZE2(0) | \ // the 1st set (at index 0) has size 2
        D3DFVF_TEXCOORDSIZE2(1)     // the 2nd set (at index 1) has size 2

struct CUSTOMVERTEX
{
    FLOAT x, y, z;   // position
    DWORD color;     // diffuse ARGB color
    FLOAT tu1, tv1;  // 1st set of u,v texture coords
    FLOAT tu2, tv2;  // 2nd set of u,v texture coords
};

static CUSTOMVERTEX s_Vertices[] =
{
 // x      y     z     ARGB color  u1    v1    u2    v2
 { -1.0f, -1.0f, 0.0f, 0xFF0000FF, 1.0f, 1.0f, 1.0f, 1.0f },
 { +1.0f, -1.0f, 0.0f, 0xFFFF0000, 0.0f, 1.0f, 0.0f, 1.0f },
 { +1.0f, +1.0f, 0.0f, 0xFFFFFF00, 0.0f, 0.0f, 0.0f, 0.0f },
 { -1.0f, +1.0f, 0.0f, 0xFFFFFFFF, 1.0f, 0.0f, 1.0f, 0.0f },
};

We'll get to discuss texture coordinates later on. Right now I just want you to consider the next piece of code, which works for both example FVFs:

// create a vertex buffer
LPDIRECT3DVERTEXBUFFER9 pVB;

m_pd3dDevice->CreateVertexBuffer(4 * sizeof(CUSTOMVERTEX),
            D3DUSAGE_WRITEONLY,
            D3DFVF_CUSTOMVERTEX,
            D3DPOOL_DEFAULT,
            &pVB,
            NULL);

// lock its vertices and transfer data to them
void* pVerts;

pVB->Lock(0, 0, &pVerts, 0);
memcpy(pVerts, s_Vertices, 4 * sizeof(CUSTOMVERTEX));

// done transferring, unlock it
pVB->Unlock();

...

// render the vertex buffer
m_pd3dDevice->SetStreamSource(0, pVB, sizeof(CUSTOMVERTEX));
m_pd3dDevice->SetFVF(D3DFVF_CUSTOMVERTEX);
m_pd3dDevice->DrawPrimitive(D3DPT_TRIANGLEFAN, 0, 2);

So therein relies the power of FVFs: as long as we (a) use valid combinations of flags to define it, (b) declare the vertex elements with the correct types and in the correct order, and (c) use the correct number of vertices and vertex component size in device methods, the device will know how to render them.

The Vertex Formats SDK topic is somewhat digestible, so I encourage you to try it; just ignore anything to do with blending weights, definetely an advanced topic, and maybe texture coordinates, which we'll cover eventually.

The CreateVertexBuffer device method takes in a size in bytes, always the number of vertices times the size of a vertex component; it also takes in the FVF definition. pVB is the actual buffer returned, and we want it in the default pool for maximum performance, which is achieved when combined with the 'write only' usage; this means that we will not be able to read data from the vertex buffer, but we don't usually need to, at least not in this example. Incidentally, this pool/usage combination is so critical in terms of efficiency, that the Direct3D SDK help states that "Buffers created with D3DPOOL_DEFAULT that do not specify D3DUAGE_WRITEONLY might suffer a severe performance penalty", so I'd rather not contradict Microsoft for once, and always use it to create my vertex buffers.

I mentioned the terms vertex components and vertex elements somewhat loosely; let me explain.

A vertex buffer is a stream of data. A stream is a uniform array of data components. Each component is made up by distinct data elements. The stride of the stream is the size of a single component, in bytes.

In pre-DirectX 8, vertices where setup in a single source; nowadays, a vertex buffer can hold vertex components with position and normal vertex elements, while another vertex buffer keeps the colors and texture coordinates vertex elements, so to make a complete vertex component, you feed each stream to the device and it 'composes' each vertex.

The main benefit of this approach is that they remove the vertex data costs previously associated with multitexturing. Before streams, users had to either duplicate vertex data sets to handle the single and multitexture case with no unused data elements, or carry data elements that would be used only in the multitexture case. This was inherently a bandwidth waste. To illustrate, consider the following multiple stream example:

C++
// stream 0, pos, diffuse, specular
#define D3DFVF_POSCOLORVERTEX D3DFVF_XYZ | D3DFVF_DIFFUSE | D3DFVF_SPECULAR

struct POSCOLORVERTEX
{
    FLOAT x, y, z;
    DWORD diffColor, specColor;
};

// stream 1, tex coord 0
#define D3DFVF_TEXCOORDS0 D3DFVF_TEX0

struct TEXCOORDS0VERTEX
{
    FLOAT tu1, tv1;
};

// stream 2, tex coord 1
#define D3DFVF_TEXCOORDS1 D3DFVF_TEX1

struct TEXCOORDS1VERTEX
{
    FLOAT tu2, tv2;
};
...
// assuming we created and initialised buffers for each FVF...
m_pd3dDevice->SetStreamSource(0, m_pVBPosColor,   0, sizeof(POSCOLORVERTEX));
m_pd3dDevice->SetStreamSource(1, m_pVBTexCoords0, 0, sizeof(TEXCOORDS0VERTEX));
m_pd3dDevice->SetStreamSource(2, m_pVBTexCoords1, 0, sizeof(TEXCOORDS1VERTEX));

// a single DrawPrimitive invocation suffices for 3 streams of 42 triangles
m_pd3dDevice->DrawPrimitive(D3DPT_TRIANGLEFAN, 0, 42);

The MaxStreams member of the device capabilities structure tells you the maximum number of concurrent streams, and it should be in the range from 1 to 16. My poor S3 SuperSavage only supports one, so that is all I have to say about multiple streams!

We jumped over the initialization of vertex buffers, which employ the lock/unlock mechanism, so let's get it over with.

Locking a resource means granting CPU access to its storage, so the application must relinquish such access by unlocking the resource after done with it. Each resorce interface has its own lock and unlock method; vertex (and index) buffers take in an offset into the vertex data to lock, in bytes, and a size of the vertex data to lock, also in bytes; when both arguments are set to 0 (like in the shown example), the entire buffer is locked and returned in the memory buffer supplied as the third argument.

The last argument describe the type of lock to perform, with a value of either 0 or a combination of D3DLOCK flags. The flags are only a hint to the intended usage of the locked data; e.g., D3DLOCK_NOOVERWRITE is saying that the application "promises" not to overwrite to the returned buffer, which allows the driver to continue rendering the vertex buffer, even when locked. Nevertheless, you may find yourself working against these flags, with unexpected results, performance penalties and most likely, no support in future Direct3D releases, so if I were you, I'd leave it alone with a value of 0, unless you really know what you are doing.

Incidentally, the offset to lock and the size to lock will let you lock subportions of the vertex buffer, but keep in mind that it does not imply locking a geometrical portion of the model; if you design your helicopter model carefully, you might be able to lock the first 42 vertex components to animate the main rotors, and in a subsequent lock, vertices 358 to 400 to animate the tail rotor, but this is in no way the best approach to do that! You are better off keeping separate buffers for each piece of your model.

The IDirect3DDevice9::DrawPrimitive method renders a sequence of non-indexed, geometric primitives of the specified type from the current set of data input streams. It requires a previous IDirect3DDevice9::SetFVF if you are using a custom FVF (other than D3DFVF_XYZ). You should never call it to render a single triangle, and the MaxPrimitiveCount member of the device capabilities will tell you how many primitives it can handle in a single call, though in practical terms, too many primitives will result in say, 0.55 FPS. Primitives are either lists of points, lines or triangles, the latter in either list, strip or fan arrangements. This type choice dependes exclusively on your application, but keep in mind that each type interprets the data differently.

You can also specify the index of the first vertex to load and draw, 0 loading the entire buffer; this might be useful to draw the first half of the buffer with some effect or rendering state, and the other half with another, but once again it has nothing to do with positional data; it has to do with the order in which vertices were copied into the buffer, which does not necessarily reflect the actual model geometry.

Index Buffers

Index buffers, represented by the IDirect3DIndexBuffer9 interface, are memory buffers that contain index data. Index data, or indices, are integer offsets into vertex buffers and are used to render primitives using the IDirect3DDevice9::DrawIndexedPrimitive method.

As resources per se, index buffers have a format, a usage, and a pool. The format is limited to either D3DFMT_INDEX16 or D3DFMT_INDEX32, indicating the bit depth of the buffer. The preferred pool is the D3DPOOL_DEFAULT, except for some configurations using AGP memory. The usage is best left alone with D3DUSAGE_WRITEONLY, unless you need to read from index buffers (in which case the best pool would be system memory). The usage can also enforce software index processing (indices, just like vertices, can be processed in hardware) when using mixed VP, but there must be a really good reason to do so.

Index data in itself is a collection of indices into 'all' other concurrent streams. Let me make that clear: if you are using multiple vertex streams, all streams are indexed together, by the index buffer you pass to the IDirect3DDevice9::SetIndices Method.

Index buffers allow you to gain some data storage efficiency by compressing the vertex buffer data. Let's say you want to render a square made up by two triangles (there is no 'square' primitive type, so you have no choice). The triangles share two vertices, each appearing twice in the vertex buffer, so to draw a square you need 6 vertices in the buffer, even when the geometry requires just 4!

1---3
|\  |
| \ |
|  \|
0---2

The alternative is to create a vertex buffer with only the four distinct vertices, and setup the index buffer so that the first triangle is drawn with vertices (0,1,2), and the second one with vertices (1,3,2). The index buffer contents is 0,1,2,1,3,2.

Sure, you may think there is redundancy, and that we now require 10 data elements to draw the original 4 (!), and you'd be right, except for this two-part catch: first, the index buffer holds integer values, which take less memory and are processed many times faster than floating point values, and second, indexed vertices are stored by the adapter in a vertex cache, from which vertices 1 and 2 can be fetched to draw the second triangle, since they were recently-used to draw the first one. This saves us from reading again from the vertex buffer, and rewards us with a big performance boost, translated into "thank you for using only 4 vertices!".

The index buffer can still be made more efficient by the type of primitive; the contents shown are used for a triangle list, which assumes that the triangles are unconnected. If we use a triangle fan instead, in which one vertex is shared by all triangles, the buffer reduces to 1,3,2,0, because the system interprets that you mean to use (3,2,1) for the first triangle and (2,0,1) for the second. Similarly, if we use a triangle strip, the original index buffer reduces to 0,1,2,3, because strips assume that the triangles are connected, and draws (0,1,2) and (1,3,2). In both cases, we just saved two indices, and the system gives us a pat in the back and says "thank you for using only 4 indices!".

The choice of primitive is usually dictated by the actual geometry of your models, but in any case strips and fans will always be better for connected triangles sets.

There are even more ways to take advantage of index buffers and the DrawIndexedPrimitive method, so be sure to jump into them from here at Rendering from Vertex and Index Buffers, in the DirectX programing guide.

I have yet to find a downside to index buffers, but then again I'm sure there is one; I'll just end this topic by saying that just because they are so great, it does not mean that they are appropriate for all cases. I use line lists to render polygons (other than triangles) with typically unique vertices, so there is no point in indexing them, for the index would be exactly the order in which they appear in the vertex buffer; and that's that.

Ok, so we covered enough. Stay tuned to learn about lights, materials and textures, coming soon!

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
Software Developer (Senior) Texas Capital Bank
United States United States
Professional software engineer with 30+ years of experience delivering systems across diverse industries, looking for the next opportunity to deliver cutting edge end-to-end technology solutions.

Avid reader, disciplined writer and enthusiastic tinkerer with a background in electronics, looking inside and thinking outside the box, genuinely passionate about robust, extensible, reusable and performant code.

Framework developer leading, coaching and learning about best practices, code quality, DevOps and software and data lifecycle management with an agile mindset to create the most elegant and sustainable solutions.

Comments and Discussions

 
GeneralMy vote of 5 Pin
peter baas25-Aug-11 3:17
peter baas25-Aug-11 3:17 
GeneralGreat! Pin
CelsoGui24-Jun-07 16:29
CelsoGui24-Jun-07 16:29 
GeneralSource Code Pin
Alan Buchanan14-Dec-06 18:57
Alan Buchanan14-Dec-06 18:57 
GeneralThanks! Pin
baloneyman8-Dec-06 11:51
baloneyman8-Dec-06 11:51 
Just happened to come across this Part III. Thanks for the great article!
I'm going back to Part 1 so I can see what I missed.
Thanks again for taking the time to write this series. It looks very
useful.

Roy

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.