Topic : DrawPrimitive
Author : Mark Feldman
Page : << Previous 4  Next >>
Go to page :


z-buffer image of the same scene. Full z-buffering is then enabled, and the dynamic objects are rendered into the scene. If we are also using a zero-overdraw algorithm to render the scene (e.g. portals) then we can apply the same technique by modifying the following parameters:

D3DRENDERSTATE_ZWRITEENABLE: Setting this to TRUE tells DrawPrimitive to fill the z-buffer with values as it renders primitives. (Unfortunately the format of the z-buffer itself varies from card to card, and DrawPrimitive has no functions to convert formats for you. This feature is one area in which OpenGL is superior).

D3DRENDERSTATE_ZENABLE: When set to TRUE, this parameter causes DrawPrimitive to test the z value of each pixel against the corresponding entry in the z-buffer. If the new pixel is closer then it is rendered over top. Note that the old z value will not be replaced with the new value unless the D3DRENDERSTATE_ZWRITEENABLE state is enabled.

D3DRENDERSTATE_ZFUNC: This state is used to specify how the comparison between z depths is performed. Typically you will want to leave this at it's default value of D3DCMP_LESSEQUAL.

The rule of thumb to remember when setting render states is that you should only set them when you absolutely have to. In general, states will not change until you change them yourself, and you can set them either inside or outside the BeginScene()/EndScene() sandwhich. States such as the view matrix will obviously have to be set once per frame. Others such as the world matrix and texture handle may have to be set numerous times in a given scene. Most however should be set right after you create the D3D device and then let be.

Applying a Texture


As mentioned in the preceding section, you set the current texture by calling SetRenderState() and passing in the D3DRENDERSTATE_TEXTUREHANDLE identifier with a handle to the texture. To get a handle to a texture you call the texture objects' GetHandle() member function. To get the texture itself you can do a QueryInterface() on a regular DirectDraw surface (note that the surface dimensions must be power-of-two).

The first step is thus creating a DirectDraw surface and bltting a bitmap image onto it. We can ask the surface for a device context and then use the regular GDI BitBlt() function to do the copy. The important thing to keep in mind here is that the source image must be a device-indepent bitmap. In my experience, most drivers seem to handle device-dependent bitmaps as well, but a few (notably those made by NumberNine) don't. If your bitmap is a linked resource then the safest bet is to use the LoadImageFunction() like the DirectX samples do.

Here's a very simple function written for MFC which loads a bitmap resource, creates a DirectDraw surface for it, performs the blt and then frees the bitmap. I threw this together pretty quickly, so it's messy and uses gotos (ack!) to handle errors. This is definately not the kind of code you'd want to use in a commercial application, but it'll do for this simple demo. The function accepts a string identifying the bitmap resource, and a boolean variable indicating whether the surface should be created in system memory or VRAM:

LPDIRECTDRAWSURFACE CMainFrame::CreateBitmapSurface(CString resname, BOOL hardware)
{
LPDIRECTDRAWSURFACE pSurface = NULL;
HBITMAP hbm = 0;
BITMAP bminfo;
DDSURFACEDESC ddsd;
CBitmap bitmap, * oldbitmap;
CDC surfacedc, bitmapdc;
HDC dc;

// Load the resource
hbm = (HBITMAP)LoadImage(AfxGetResourceHandle(), resname, IMAGE_BITMAP, 0, 0, LR_CREATEDIBSECTION);
if (!hbm)
  goto texture_error;
bitmap.Attach(hbm);

// Find out how large this bitmap is
if (!GetObject(hbm, sizeof(bminfo), &bminfo))
  goto texture_error;

// Create a surface for this bitmap
ZeroMemory(&ddsd, sizeof(ddsd));
ddsd.dwSize = sizeof(ddsd);
ddsd.dwFlags = DDSD_CAPS | DDSD_WIDTH | DDSD_HEIGHT;
ddsd.dwWidth = bminfo.bmWidth;
ddsd.dwHeight = bminfo.bmHeight;
if (hardware)
  ddsd.ddsCaps.dwCaps = DDSCAPS_TEXTURE | DDSCAPS_VIDEOMEMORY;
else
  ddsd.ddsCaps.dwCaps = DDSCAPS_TEXTURE | DDSCAPS_SYSTEMMEMORY;
if (m_pDD->CreateSurface(&ddsd, &pSurface, NULL) != DD_OK)
  goto texture_error;

// Now copy the bitmap to the surface
if (pSurface->GetDC(&dc) != DD_OK)
  goto texture_error;
surfacedc.Attach(dc);
bitmapdc.CreateCompatibleDC(&surfacedc);
oldbitmap = bitmapdc.SelectObject(&bitmap);
surfacedc.BitBlt(0,0,bminfo.bmWidth,bminfo.bmHeight,&bitmapdc,0,0,SRCCOPY);
bitmapdc.SelectObject(oldbitmap);
pSurface->ReleaseDC(surfacedc.Detach());

// If we made it here then the function was a success
DeleteObject(hbm);
return pSurface;

texture_error:
TRACE("Error creating texture\n");
RELEASE(pSurface);
if (hbm)
  DeleteObject(hbm);
return NULL;
}


The next step is to query this surface to get a pointer to the IDirect3DTexture 2 object, and then call the GetHandle() member function to get a handle to pass into the SetRenderState() function. I'll assume the variable m_pTexture points to the surface, here's the code you need:

D3DTEXTUREHANDLE m_TextureHandle;
LPDIRECT3DTEXTURE2 texture;
if (m_pTexture->QueryInterface(IID_IDirect3DTexture2, (LPVOID *)&texture) != S_OK)
return FALSE;
texture->GetHandle(m_pDevice, &m_TextureHandle);


With a handle to the texture we're now ready to draw a texture mapped 3D polygon. I'm going to modify the DrawPrimitive code slightly to draw a triangle strip. As you can see, I specify the 4 endpoints in the correct order so that two triangles are rendered forming a square. You'll also notice that I've set the light intensity of all components to maximum, and I've set the texture coordinates as well. Texture coordinates range from 0-1, and specifying values outside this range cause it to wrap around (handy for tiling textures).

// Set the current texture
m_pDevice->SetRenderState(D3DRENDERSTATE_TEXTUREHANDLE, m_TextureHandle);

// Render a texture mapped square
D3DLVERTEX v[4];
v[0] = D3DLVERTEX(D3DVECTOR(-2, 2,10),D3DRGB(1,1,1),D3DRGB(0,0,0),0,0);
v[1] = D3DLVERTEX(D3DVECTOR( 2, 2,10),D3DRGB(1,1,1),D3DRGB(0,0,0),1,0);
v[2] = D3DLVERTEX(D3DVECTOR(-2,-2,10),D3DRGB(1,1,1),D3DRGB(0,0,0),0,1);
v[3] = D3DLVERTEX(D3DVECTOR( 2,-2,10),D3DRGB(1,1,1),D3DRGB(0,0,0),1,1);
m_pDevice->DrawPrimitive(D3DPT_TRIANGLESTRIP,D3DVT_LVERTEX,(LPVOID)v,4,NULL);



(Note that the quality of this image is rather poor. This is due to a combination of my running the demo in 320x240 mode, and the fact that I can't draw textures to save myself!).

Motion


The next step is to add some kind of motion. One way of doing this is to have a fixed camera and spin the objects around, which is typically accomplished by adjusting the object's m_World matrix. Another way, and the way I'll preset here, is to keep the objects fixed and have the user control the camera, i.e. the m_View matrix.

In order to achieve smooth motion that has a consistent speed regardless of rendering rate you need some method of accurately measuring the time between successive frames. Probably the easiest way of doing this is by calling the multimedia function timeGetTime(). Under Win95, It's accurate to within 1 millisecond, more than enough for our needs.

In this demo I have the camera respond to keystrokes. To keep things simple I'm relying on the GetAsyncKeyState() function to determined which keys are pressed, rather than using DirectInput. The following define allows you to pass in a key's virtual key code and it returns 0 (FALSE) if the key is not pressed and 0x8000 (TRUE) if it is:

#define KeyPressed(key) (GetAsyncKeyState(key) & 0x8000)
Then for each frame I call the function UpdateCamera() which checks to see what keys are pressed and applies the appropriate transform to the camera matrix m_View. I also maintain a DWORD variable call m_LastTime which is used to calculate the elapsed time between frames. The transforms are scaled by this value to achieve a consistant animation speed.

Here is the code I use to update the camera. It doesn't use acceleration and resistance, so it's not the smoothest motion you'll find, but it'll do. Notice how I start off by getting the elapsed time and then doubling that if the user is holding the SHIFT key down. The left and right keys turn the camera left and right, unless the ALT key is pressed in which cases it causes the user to slide left and right. UP and DOWN keys move the user forward and back. PAGEUP and PAGEDOWN cause the user to tilt up and down. HOME and END cause the user to move up and down along the camera's y axis. INSERT and DELETE cause the user to swivel left and right about the camera's local z axis:

// Get current time, calculate elapsed time and update m_LastTime for the next frame
DWORD thistime = timeGetTime();
DWORD elapsed = thistime - m_LastTime;
m_LastTime = thistime;

// If the user is holding down the shift key then double the elapsed time, this effectively speeds up the motion
if (KeyPressed(VK_SHIFT)) elapsed *= 2;

// Check for depressed keys and update camera matrix accordingly

#define MOTION_SPEED 0.01f
#define TURN_SPEED   0.001f

if (KeyPressed(VK_UP))   m_View = MatrixMult(Translate(0,0,-MOTION_SPEED*elapsed), m_View);
if (KeyPressed(VK_DOWN)) m_View = MatrixMult(Translate(0,0, MOTION_SPEED*elapsed), m_View);
if (GetAsyncKeyState(VK_MENU) & 0x8000)
{
if (KeyPressed(VK_LEFT))  m_View = MatrixMult(Translate( MOTION_SPEED*elapsed,0,0), m_View);
if (KeyPressed(VK_RIGHT)) m_View = MatrixMult(Translate(-MOTION_SPEED*elapsed,0,0), m_View);
}
else
{
if (KeyPressed(VK_LEFT))  m_View = MatrixMult(RotateY(-TURN_SPEED*elapsed), m_View);
if (KeyPressed(VK_RIGHT)) m_View = MatrixMult(RotateY( TURN_SPEED*elapsed), m_View);
}
if (KeyPressed(VK_NEXT))   m_View = MatrixMult(RotateX( TURN_SPEED*elapsed), m_View);
if (KeyPressed(VK_PRIOR))  m_View = MatrixMult(RotateX(-TURN_SPEED*elapsed), m_View);
if (KeyPressed(VK_INSERT)) m_View = MatrixMult(RotateZ( TURN_SPEED*elapsed), m_View);
if (KeyPressed(VK_DELETE)) m_View = MatrixMult(RotateZ(-TURN_SPEED*elapsed), m_View);
if (KeyPressed(VK_HOME))   m_View = MatrixMult(Translate(0,-MOTION_SPEED*elapsed,0), m_View);
if (KeyPressed(VK_END))    m_View = MatrixMult(Translate(0, MOTION_SPEED*elapsed,0), m_View);

// We've made our changes, so set the new view matrix
m_pDevice->SetTransform(D3DTRANSFORMSTATE_VIEW, &m_View);



Now that we have motion, the artifacting that occurrs due to non-subpixel accurate texture mapping becomes a lot more apparent. In order to fix this we need to set the render state to enable both the D3DRENDERSTATE_TEXTUREPERSPECTIVE and D3DRENDERSTATE_SUBPIXEL parameters.

At this point we should also try and display something a bit more interesting than a single polygon. In the code that follows, I've written extra code to render the six sides of a cube, and it's here that we run into one of DrawPrimitive major short-comings:

Page : << Previous 4  Next >>