Topic : DrawPrimitive
Author : Mark Feldman
Page : << Previous 5  
Go to page :


the texture coordinates for each vertex are in the same structure as the 3D world coordinates. If we have two vertices that share a common world coordinate but have different texture coordinates and/or lighting, then we have to add that vertex into the vertex list twice, once for each occurance. This is usually fine for polygonal mesh models which are mapped with the single continuous texture across many faces, but it's useless for things like a floor and two walls with different textures meeting at the same point. In this case the point has to go through the entire transformation/projection stage three times, once for each surface. This is a serious flaw IMHO, and one that I hope is fixed in the next DirectX release. In the mean time I've created the cube by calling DrawPrimitive 6 times, once for each face (In this particular demo I could have used a single triangle strip for 4 of the faces, but I'll keep things simple).

What follows now is my entire rendering code. It calls the routine to update the camera position, clears the back buffer, starts a scene, draws the cube and finishes up. Notice that I've also adjusted the lighting values for each vertex to add some contrast in the image:

// Update the camera position
UpdateCamera();

// Clear the back buffer
DDBLTFX bltfx;
ZeroMemory(&bltfx, sizeof(bltfx)); // Sets dwFillColor to 0 as well
bltfx.dwSize = sizeof(bltfx);
m_pBackBuffer->Blt(NULL,NULL,NULL,DDBLT_WAIT|DDBLT_COLORFILL,&bltfx);

// Start a DrawPrimitive scene
m_pDevice->BeginScene();

// Set the current texture
if (m_pDevice->SetRenderState(D3DRENDERSTATE_TEXTUREHANDLE, m_TextureHandle) != D3D_OK)
m_pDevice->SetRenderState(D3DRENDERSTATE_TEXTUREHANDLE, NULL);

// Render a texture mapped square
D3DLVERTEX v[4];
v[0] = D3DLVERTEX(D3DVECTOR(-3, 3,10),D3DRGB(1.0,1.0,1.0),D3DRGB(0,0,0),0,0);
v[1] = D3DLVERTEX(D3DVECTOR( 3, 3,10),D3DRGB(1.0,1.0,1.0),D3DRGB(0,0,0),1,0);
v[2] = D3DLVERTEX(D3DVECTOR(-3,-3,10),D3DRGB(1.0,1.0,1.0),D3DRGB(0,0,0),0,1);
v[3] = D3DLVERTEX(D3DVECTOR( 3,-3,10),D3DRGB(1.0,1.0,1.0),D3DRGB(0,0,0),1,1);
m_pDevice->DrawPrimitive(D3DPT_TRIANGLESTRIP,D3DVT_LVERTEX,(LPVOID)v,4,NULL); // front face
v[0] = D3DLVERTEX(D3DVECTOR( 3, 3,10),D3DRGB(0.6,0.6,0.6),D3DRGB(0,0,0),0,0);
v[1] = D3DLVERTEX(D3DVECTOR( 3, 3,16),D3DRGB(0.6,0.6,0.6),D3DRGB(0,0,0),1,0);
v[2] = D3DLVERTEX(D3DVECTOR( 3,-3,10),D3DRGB(0.6,0.6,0.6),D3DRGB(0,0,0),0,1);
v[3] = D3DLVERTEX(D3DVECTOR( 3,-3,16),D3DRGB(0.6,0.6,0.6),D3DRGB(0,0,0),1,1);
m_pDevice->DrawPrimitive(D3DPT_TRIANGLESTRIP,D3DVT_LVERTEX,(LPVOID)v,4,NULL); // right face
v[0] = D3DLVERTEX(D3DVECTOR( 3, 3,16),D3DRGB(1.0,1.0,1.0),D3DRGB(0,0,0),0,0);
v[1] = D3DLVERTEX(D3DVECTOR(-3, 3,16),D3DRGB(1.0,1.0,1.0),D3DRGB(0,0,0),1,0);
v[2] = D3DLVERTEX(D3DVECTOR( 3,-3,16),D3DRGB(1.0,1.0,1.0),D3DRGB(0,0,0),0,1);
v[3] = D3DLVERTEX(D3DVECTOR(-3,-3,16),D3DRGB(1.0,1.0,1.0),D3DRGB(0,0,0),1,1);
m_pDevice->DrawPrimitive(D3DPT_TRIANGLESTRIP,D3DVT_LVERTEX,(LPVOID)v,4,NULL); // back face
v[0] = D3DLVERTEX(D3DVECTOR(-3, 3,16),D3DRGB(0.6,0.6,0.6),D3DRGB(0,0,0),0,0);
v[1] = D3DLVERTEX(D3DVECTOR(-3, 3,10),D3DRGB(0.6,0.6,0.6),D3DRGB(0,0,0),1,0);
v[2] = D3DLVERTEX(D3DVECTOR(-3,-3,16),D3DRGB(0.6,0.6,0.6),D3DRGB(0,0,0),0,1);
v[3] = D3DLVERTEX(D3DVECTOR(-3,-3,10),D3DRGB(0.6,0.6,0.6),D3DRGB(0,0,0),1,1);
m_pDevice->DrawPrimitive(D3DPT_TRIANGLESTRIP,D3DVT_LVERTEX,(LPVOID)v,4,NULL); // left face
v[0] = D3DLVERTEX(D3DVECTOR(-3, 3,16),D3DRGB(0.2,0.2,0.2),D3DRGB(0,0,0),0,0);
v[1] = D3DLVERTEX(D3DVECTOR( 3, 3,16),D3DRGB(0.2,0.2,0.2),D3DRGB(0,0,0),1,0);
v[2] = D3DLVERTEX(D3DVECTOR(-3, 3,10),D3DRGB(0.2,0.2,0.2),D3DRGB(0,0,0),0,1);
v[3] = D3DLVERTEX(D3DVECTOR( 3, 3,10),D3DRGB(0.2,0.2,0.2),D3DRGB(0,0,0),1,1);
m_pDevice->DrawPrimitive(D3DPT_TRIANGLESTRIP,D3DVT_LVERTEX,(LPVOID)v,4,NULL); // top face
v[0] = D3DLVERTEX(D3DVECTOR(-3,-3,10),D3DRGB(0.2,0.2,0.2),D3DRGB(0,0,0),0,0);
v[1] = D3DLVERTEX(D3DVECTOR( 3,-3,10),D3DRGB(0.2,0.2,0.2),D3DRGB(0,0,0),1,0);
v[2] = D3DLVERTEX(D3DVECTOR(-3,-3,16),D3DRGB(0.2,0.2,0.2),D3DRGB(0,0,0),0,1);
v[3] = D3DLVERTEX(D3DVECTOR( 3,-3,16),D3DRGB(0.2,0.2,0.2),D3DRGB(0,0,0),1,1);
m_pDevice->DrawPrimitive(D3DPT_TRIANGLESTRIP,D3DVT_LVERTEX,(LPVOID)v,4,NULL); // bottom face

// Done rendering
m_pDevice->EndScene();



The demo application supplied at the end of this article shows the application at this point, although I've also bumped up the resolution to 640x480x16bpp.


Z-Buffering


Z-buffering is a technique used to perform hidden surface removal, and is often implemented in hardware by the display card. Before a pixel in rendered, it's z-distance from the viewpoint is calculated and compared against a table of z values for all pixels on the screen, i.e. the "z-buffer". If the distance is closer, then the new pixel is plotted and it's corresponding value in the z-buffer is replaced with the new value.

One very important thing to keep in mind is that quite often drivers use integer or fixed-point variables to store the z-values. If the distance between the front and back clipping planes is too large then the range of z values have to be spread out too far in order to cover the entire z range possible. They don't have enough resolution to properly z-buffer objects, and you start getting some really strange looking artifacting. Try and keep the front and back planes as close as you can when you create the projection matrix. In this demo I use values of z=0.1 for the front plane and z=1000.0 for the back.

The first step to implementing a z-buffer is to create the z-buffer itself, and it's here that many people run into problems. The resolution of the z-values (i.e. the number of bits used to store each one) varies between drivers. In order to determine which z-depths are supported we must query the driver to find out which depths it supports. However, in order for z-buffering to work the z-buffer must already exist and be attached to the backbuffer at the time the Direct3D driver object is created.

Fortunately, we only need to make a few minor adjustments to the demo at this point in order to create each object in the correct order. Right after we create the back buffer we should call FindDevice() to get the guid for the device we can use. The D3DFINDDEVICERESULT variable we passed into FindDevice() contains the information for each driver (hardware and software) including the bit depths it supports. Once we've selected a supported bit depth we can create the z-buffer, attach to the back buffer and then go ahead and create the Direct3DDevice2 object.

The D3DDEVICEDESC structures initialized by FindDevice() contain a member called dwDeviceZBufferBitDepth. We determine the bit depths supported by the driver by masking this value with predefined constants, e.g. DDBD_16 for 16 bits per z-value. Current supported bit depths are 8, 16, 24 and 32. I've never tried an 8-bit driver, but my guess is it would look pretty bad for scenes of any complexity, so I choose to make it the last choice. Here's the code I use to select a bit depth (result is the D3DDEVICEDESC structure we passed into the FindDevice function) :

DWORD depths = hardware ? result.ddHwDesc.dwDeviceZBufferBitDepth :
result.ddSwDesc.dwDeviceZBufferBitDepth;
DWORD bitdepth;
if (depths & DDBD_16) bitdepth=16;
else if (depths & DDBD_24) bitdepth=24;
else if (depths & DDBD_32) bitdepth=32;
else if (depths & DDBD_8) bitdepth=8;
else return FALSE;


The next step is to create the z-buffer itself. This is similar to creating the back-buffer, but we pass in the DDSCAPS_ZBUFFER caps flags instead of DDSCAPS_OFFSCREENPLAIN and DDSCAPS_3DDEVICE. We also need to initialize the appropriate member with the z-buffer bit depth:

// Create a z-buffer and attach it to the backbuffer
TRACE("DrawPrim Demo: Creating z-buffer\n");
ZeroMemory(&ddsd, sizeof(ddsd));
ddsd.dwSize = sizeof(ddsd);
ddsd.dwFlags = DDSD_CAPS | DDSD_WIDTH | DDSD_HEIGHT | DDSD_ZBUFFERBITDEPTH;
ddsd.dwWidth = SCREEN_WIDTH;
ddsd.dwHeight = SCREEN_HEIGHT;
ddsd.dwZBufferBitDepth = bitdepth;
if (hardware)
ddsd.ddsCaps.dwCaps = DDSCAPS_ZBUFFER | DDSCAPS_VIDEOMEMORY;
else
ddsd.ddsCaps.dwCaps = DDSCAPS_ZBUFFER | DDSCAPS_SYSTEMMEMORY;
if (m_pDD->CreateSurface(&ddsd, &m_pZBuffer, NULL) != DD_OK)
return FALSE;
if (m_pBackBuffer->AddAttachedSurface(m_pZBuffer) != DD_OK)
return FALSE;


Next, we need to enable z-buffering by changing the appropriate driver states:

m_pDevice->SetRenderState(D3DRENDERSTATE_ZENABLE,TRUE);
m_pDevice->SetRenderState(D3DRENDERSTATE_ZWRITEENABLE,TRUE);


Enabling the first state causes DrawPrimitive to calculate the z-value for each pixel and compare it to the z-buffer table. The second state tells it to write the new z-value if the pixel is in fact rendered. Having seperate control over each state like this allows us to do the neat little trick that games like Quake do for hidden surface removal. Often the static objects in a scene (walls, floors etc) can be rendered with an efficient zero-overdraw technique such as portals. If we set the ZENABLE state to FALSE and the ZWRITEENABLE state to TRUE then DrawPrimitive will always render it's pixels and create a corresponding z-buffer for us, but won't bother wasting time comparing the z-values with those already in the table. Once finished, we'll have a z-buffer corresponding to the scene rendered so far. We can then reenable full z-buffering and render all dynamic objects into the scene.

Don't forget to clear the z-buffer once at the start of each frame, in order to set all values to the maximum. This can be done right after clearing the backbuffer with code such as the following:


// Clear the z-buffer
D3DRECT rect;
rect.x1 = 0;
rect.y1 = 0;
rect.x2 = SCREEN_WIDTH;
rect.y2 = SCREEN_HEIGHT;
m_pViewport->Clear(1, &rect, D3DCLEAR_ZBUFFER);


Finally you might want to render another cube behind the first so as to make sure that z-buffering is working. The demo uses the same code used to render the first cube, but with 20.0 added to all the z world components.

Z-buffering also works well when rendering D3DTLVERTEX vertices. To do this, set up for z-buffering as you would normally but be sure to also set the z component of each screen pixel, ranging from 0 at the front plane to 1 (not inclusive) at the back plane.

Summary


That's it for this version of the tutorial. I've only covered some of more basic DrawPrimitive features here, but it should be enough to get started. In future versions I hope to cover some of the other commonly used features, e.g. lights and materials. So far I've found DrawPrimitive and DirectX 5 as a whole to be a substantial improvement over previous versions. I still think OpenGL is a superior product (not to mention somewhat easier to understand), although I haven't had have nearly as many problems getting DrawPrimitive to actually work than I did for both MS OpenGL 1.1 and CosmoGL. In short, it's a very worthwhile addition to the DirectX Game SDK.

Download the full demo and source code (~260K) from:
http://www.geocities.com/SiliconValley/2151/drawprim.zip

  

Copyright (c) 1997 Mark Feldman (pcgpe@geocities.com) - All Rights Reserved
This article is part of The Win95 Game Programmer's Encyclopedia


Page : << Previous 5