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
86 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

 Preface
 The Third
 Dimension

 Transformation
 Math

 Down to the Code
 Conclusion

 Get the source
 Printable version

 


  The Series

 The Basics
 First Steps to
 Animation

 Multitexturing
 Building Worlds
 With X Files


 

FrameMove()

The FrameMove() method handles most of the keyboard input and the matrix stuff. All the rotations and translations for the objects and the camera are set in this method.

At first you need a small DirectInput primer to understand all the input stuff presented in this method.

With DirectInput, which is the input component of DirectX, you can access keyboard, mouse, joystick and all other forms of input devices in a uniform manner. Although DirectInput can be extremely complex if you use all its functionality, it can be quite manageable at the lowest level of functionality, which we will use here.

DirectInput consists of run-time DLLs and two compile-time files: dinput.lib and dinput.h. They import the library and the header. Using DirectInput is straightforward:

Setup DirectInput:

  • Create a main DirectInput object with DirectInputCreateEx()
  • Create one or more input devices with CreateDeviceEx()
  • Set the data format of each device with SetDataFormat()
  • Set the cooperative level for each device with SetCooperativeLevel()

Getting Input:

  • Acquire each input device with Acquire()
  • Receive Input with GetDeviceState()
  • Special Joysticks: call Poll() if it's needed

    DirectInput can send you immediate mode state information or buffer input, time-stamped in a message format. We'll only use the immediate mode of data acquisition here (see the DirectX SDK documentation for information on buffered mode).

We call DirectInputCreateEx() in the CreateDInput() method.

HRESULT CMyD3DApplication::CreateDInput( HWND hWnd ) { // keyboard if( FAILED(DirectInputCreateEx( (HINSTANCE)GetWindowLong( hWnd, GWL_HINSTANCE ), DIRECTINPUT_VERSION, IID_IDirectInput7, (LPVOID*) &g_Keyboard_pDI, NULL) ) ) return 0; return S_OK; }

It's called in WinMain() with

// Create the DInput object if( FAILED(d3dApp.CreateDInput( d3dApp.Get_hWnd() ) ) ) return 0;

To retrieve the instance of the sample, we use GetWindowLong( hWnd, GWL_HINSTANCE ). The constant DIRECTINPUT_VERSION determines which version of DirectInput your code is designed for. The next parameter is the desired DirectInput Interface, which should be used by the sample. Acceptable values are IID_IDirectInput, IID_IDirectInput2 and IID_IDirectInput7. For backward compatibility you can define an older verison of DirectInput there. This is useful, for example, for WinNT which supports only DirectX 3. The last parameter holds the DirectInput interface pointer.

To create one input device - the keyboard - we use CreateDeviceEx() in CreateInputDevice()

HRESULT CMyD3DApplication::CreateInputDevice( HWND hWnd, LPDIRECTINPUT7 pDI, LPDIRECTINPUTDEVICE2 pDIdDevice, GUID guidDevice, const DIDATAFORMAT* pdidDataFormat, DWORD dwFlags ) { // Get an interface to the input device if( FAILED( pDI->CreateDeviceEx( guidDevice,
IID_IDirectInputDevice2, (VOID**)&pDIdDevice, NULL ) ) ) return 0; // Set the device data format if( FAILED( pDIdDevice->SetDataFormat( pdidDataFormat ) ) ) return 0; // Set the cooperativity level if( FAILED( pDIdDevice->SetCooperativeLevel( hWnd, dwFlags ) ) ) return 0; if(guidDevice == GUID_SysKeyboard) g_Keyboard_pdidDevice2 = pDIdDevice; return S_OK; }

It's called in WinMain() with

// Create a keyboard device if( FAILED(d3dApp.CreateInputDevice( d3dApp.Get_hWnd(), d3dApp.g_Keyboard_pDI, d3dApp.g_Keyboard_pdidDevice2, GUID_SysKeyboard, &c_dfDIKeyboard, DISCL_NONEXCLUSIVE | DISCL_FOREGROUND)))

Besides creating the input device it sets the data format of the keyboard with SetDataFormat() and the cooperative level with SetCooperativeLevel(). The first parameter of CreateDeviceEx() is the GUID (Globally Unique Indentifier), that identifies the device you want to create.

You have to enumerate devices with EnumDevices() to get the GUIDs for any weird stuff like joysticks, flightsticks, virtual reality helms and suits.

You won't need to perform an enumeration process for the keyboard, because all computers are required to have one and won't boot without it. So the GUID for keyboards is predefined by DirectInput. The next parameter is for the desired interface. Accepted values are currently IID_DirectInputDevice, IID_DirectInputDevice2 and IID_DirectInputDevice7. CreateDeviceEx() returns the interface pointer pDIdDevice which will be stored later in g_Keyboard_pdidDevice2.

By setting the Data format with SetDataFormat(), you tell DirectInput how you want the data from the device to be formatted and represented. You can define your own DIDATAFORMAT structure, or you can use one of the predefined global constants: c_dfDIKeyboard is the constant for the keyboard. Generally you won't need to define a custom structure, because the predefined ones will allow your application to use most of the off-the-shelf devices.

The next step you need to perform before you can access the DirectInput device (in this case the keyboard) is to use the method SetCooperativeLevel() to set the device's behaviour. It determines how the input from the device is shared with other applications. For a keyboard you have to use the DISCL_NONEXCLUSIVE flag, because DirectInput doesn't support exclusive access to keyboard devices.

Even Ctrl+Alt+Esc wouldn't work with an exclusive keyboard.

DISCL_FOREGROUND restricts the use of DirectInput on the foreground. The device is automatically unaquired when the associated window moves to the background. Whereas DISCL_BACKGROUND gives your app the possiblity to use a DirectInputDevice in fore- and background.

In addition, this method needs the handle of the window, to set the exclusivity.

To get the keyboard input, we call, in the FrameMove() method, the following functions:

BYTE diks[256]; // DInput keyboard state buffer ZeroMemory( diks, sizeof(diks) ); if (FAILED(g_Keyboard_pdidDevice2->GetDeviceState( sizeof(diks), &diks ))) { g_Keyboard_pdidDevice2->Acquire(); if (FAILED(g_Keyboard_pdidDevice2->GetDeviceState( sizeof(diks), &diks ))) return 0; }

The array disks[256] holds the keyboard states. To get access to the DirectInput Device, you have to acquire it. You retrieve the keyboard states with GetDeviceState(). The values are used with

// yellow object if (diks[DIK_J] &&0x80) // j key m_pObjects[0].fRoll -= 1.0f * m_fTimeElapsed;

To test if any key is down, you must test the 0x80 bit in the 8-bit byte of the key in question; in other words the uppermost bit.

At the end of the sample, the DirectInput device is released with a call to

VOID CMyD3DApplication::DestroyInputDevice() { // keyboard if(g_Keyboard_pdidDevice2) { g_Keyboard_pdidDevice2->Unacquire(); g_Keyboard_pdidDevice2->Release(); g_Keyboard_pdidDevice2 = NULL; } }

That's all with DirectInput. Now back to graphics programming.

FrameMove() uses a timing code to ensure that all the objects and the camera move/rotate in the same speed at every possible fps.

// timing code: // the object should move/rotate in the same speed // at every possible fps const cTimeScale = 5; // calculate elapsed time m_fTimeElapsed=(fTimeKey-m_fStartTimeKey)* cTimeScale; // store last time m_fStartTimeKey=fTimeKey;

To calculate the elapsed time, you have to subtract m_fStartTimeKey from fTimeKey.

To rotate the yellow object about its x- and z- axis, we have to change the variables fRoll and fPitch in the m_pObject structure.

// yellow object if (diks[DIK_J] &&0x80) // j key m_pObjects[0].fRoll -= 1.0f * m_fTimeElapsed; if (diks[DIK_L] &&0x80) // l key m_pObjects[0].fRoll += 1.0f * m_fTimeElapsed; if (diks[DIK_I] &&0x80) // i key m_pObjects[0].fPitch -= 1.0f * m_fTimeElapsed; if (diks[DIK_K] &&0x80) // k key m_pObjects[0].fPitch += 1.0f * m_fTimeElapsed;

They are used in the following translate and rotate matrix methods.

D3DMATRIX matWorld; // object matrix for yellow object D3DUtil_SetTranslateMatrix( matWorld, m_pObjects[0].vLoc ); D3DMATRIX matTemp, matRotateX, matRotateY, matRotateZ; D3DUtil_SetRotateYMatrix( matRotateY, -m_pObjects[0].fYaw ); D3DUtil_SetRotateXMatrix( matRotateX, -m_pObjects[0].fPitch ); D3DUtil_SetRotateZMatrix( matRotateZ, -m_pObjects[0].fRoll ); D3DMath_MatrixMultiply( matTemp, matRotateX, matRotateY ); D3DMath_MatrixMultiply( matTemp, matRotateZ, matTemp ); D3DMath_MatrixMultiply( matWorld, matTemp, matWorld ); m_pObjects[0].matLocal = matWorld;

As described above, the method D3DUtil_SetTranslateMatrix() would translate the yellow object into its place and D3DUtil_SetRotateXMatrix() and D3DUtil_SetRotateZMatrix() would rotate it around the x-axis and z-axis. We won't use D3DUtil_SetRotateYMatrix() here. They are useful for the upcoming tutorials. At last, the position of the yellow object in the world matrix will be stored in the m_pObjects structure.

The same functionality lies behind the code for the red object.

// red object if (diks[DIK_D] &&0x80) // Key d m_pObjects[1].fRoll -= 1.0f * m_fTimeElapsed; if (diks[DIK_A] &&0x80) // Key a m_pObjects[1].fRoll += 1.0f * m_fTimeElapsed; if (diks[DIK_S] &&0x80) // Key s m_pObjects[1].fPitch -= 1.0f * m_fTimeElapsed; if (diks[DIK_W] &&0x80) // Key w m_pObjects[1].fPitch += 1.0f * m_fTimeElapsed; // object matrix for red object D3DUtil_SetTranslateMatrix( matWorld, m_pObjects[1].vLoc ); D3DUtil_SetRotateYMatrix( matRotateY, -m_pObjects[1].fYaw ); D3DUtil_SetRotateXMatrix( matRotateX, -m_pObjects[1].fPitch ); D3DUtil_SetRotateZMatrix( matRotateZ, -m_pObjects[1].fRoll ); D3DMath_MatrixMultiply( matTemp, matRotateX, matRotateY ); D3DMath_MatrixMultiply( matTemp, matRotateZ, matTemp ); D3DMath_MatrixMultiply( matWorld, matTemp, matWorld ); m_pObjects[1].matLocal = matWorld;

The only differences are the use of other keys and the storage of the variables in another object struture.

After translating the objects, the camera has to be placed and pointed in the right direction. The vLook, vUp, vRight and vPos vectors are holding the position and the LOOK, UP and RIGHT vectors of the camera.

//************************************************************ // camera stuff //************************************************************ static D3DVECTOR vLook=D3DVECTOR(0.0f,0.0f,1.0); static D3DVECTOR vUp=D3DVECTOR(0.0f,1.0f,0.0f); static D3DVECTOR vRight=D3DVECTOR(1.0f,0.0f,0.0f); static D3DVECTOR vPos=D3DVECTOR(0.0f,0.0f,-5.0f); FLOAT fPitch,fYaw,fRoll; fPitch = fYaw = fRoll = 0.0f; FLOAT fspeed= 1.0f * m_fTimeElapsed;

The LOOK vector points in the direction of the positive z-axis. The UP vector points into the direction of the positive y-axis and the RIGHT vector points in the direction of the positive x-axis. The variables fPitch, fYaw and fRoll are responsible for the orientation of the camera. The camera is moved back and forward with vPos, whereas speed holds the back and forward speed of it.

// fPitch if (diks[DIK_UP] && 0x80) fPitch=-0.3f * m_fTimeElapsed; if (diks[DIK_DOWN] && 0x80) fPitch=+0.3f * m_fTimeElapsed; // fYaw if (diks[DIK_C] && 0x80) // c key fYaw=-0.3f * m_fTimeElapsed; if (diks[DIK_X] && 0x80) // x key fYaw=+0.3f * m_fTimeElapsed; // fRoll if (diks[DIK_LEFT] && 0x80) fRoll=-0.3f * m_fTimeElapsed; if (diks[DIK_RIGHT] && 0x80) fRoll=+0.3f * m_fTimeElapsed; // camera forward if (diks[DIK_HOME] && 0x80 ) // Key HOME { vPos.x+=fspeed*vLook.x; vPos.y+=fspeed*vLook.y; vPos.z+=fspeed*vLook.z; } // camera back if (diks[DIK_END] &&0x80 ) // Key END { vPos.x-=fspeed*vLook.x; vPos.y-=fspeed*vLook.y; vPos.z-=fspeed*vLook.z; }

The three orientation vectors are normalized with Base Vector Regeneration, by normalizing the LOOK vector, building a perpendicular vector out of the UP and LOOK vector, normalizing the RIGHT vector and building the perpendicular vector of the LOOK and RIGHT vector, the UP vector. Then the UP vector is normalized.

Normalization produces a vector with a magnitude of 1. The cross product method produces a vector, which is perpendicular to the two vectors provided as variables.

vLook = Normalize(vLook); vRight = CrossProduct( vUp, vLook); // Cross Produkt of the UP and LOOK Vector vRight = Normalize (vRight); vUp = CrossProduct (vLook, vRight); // Cross Produkt of the RIGHT and LOOK Vector vUp = Normalize(vUp);

The rotation matrices are built with D3DUtil_SetRotationMatrix() and executed with D3DUtil_MatrixMultiply().

// Matrices for pitch, yaw and roll // This creates a rotation matrix around the viewers RIGHT vector. D3DMATRIX matPitch, matYaw, matRoll; D3DUtil_SetRotationMatrix(matPitch, vRight, fPitch); // Creates a rotation matrix around the viewers UP vector. D3DUtil_SetRotationMatrix(matYaw, vUp, fYaw ); // Creates a rotation matrix around the viewers LOOK vector. D3DUtil_SetRotationMatrix(matRoll, vLook, fRoll); // now multiply these vectors with the matrices we've just created. // First we rotate the LOOK & RIGHT Vectors about the UP Vector D3DMath_VectorMatrixMultiply(vLook , vLook, matYaw); D3DMath_VectorMatrixMultiply(vRight, vRight,matYaw); // And then we rotate the LOOK & UP Vectors about the RIGHT Vector D3DMath_VectorMatrixMultiply(vLook , vLook, matPitch); D3DMath_VectorMatrixMultiply(vUp, vUp, matPitch); // now rotate the RIGHT & UP Vectors about the LOOK Vector D3DMath_VectorMatrixMultiply(vRight, vRight, matRoll); D3DMath_VectorMatrixMultiply(vUp, vUp, matRoll); D3DMATRIX view=matWorld; D3DUtil_SetIdentityMatrix( view );// defined in d3dutil.h and d3dutil.cpp view._11 = vRight.x; view._12 = vUp.x; view._13 = vLook.x; view._21 = vRight.y; view._22 = vUp.y; view._23 = vLook.y; view._31 = vRight.z; view._32 = vUp.z; view._33 = vLook.z; view._41 = - DotProduct( vPos, vRight ); // dot product defined in d3dtypes.h view._42 = - DotProduct( vPos, vUp ); view._43 = - DotProduct( vPos, vLook ); m_pd3dDevice->SetTransform(D3DTRANSFORMSTATE_VIEW, &view);

Render()

The Render() method is called once per frame and is the entry point for 3d rendering. It clears the viewport, and renders the two objects with proper material.

HRESULT CMyD3DApplication::Render() { D3DMATERIAL7 mtrl; // Clear the viewport m_pd3dDevice->Clear( 0, NULL, D3DCLEAR_TARGET | D3DCLEAR_ZBUFFER, 0x00000000, 1.0f, 0L ); // Begin the scene if( FAILED( m_pd3dDevice->BeginScene() ) ) return S_OK; // Don't return a "fatal" error // yellow object // Set the color for the object D3DUtil_InitMaterial( mtrl, m_pObjects[0].r, m_pObjects[0].g, m_pObjects[0].b ); m_pd3dDevice->SetMaterial( &mtrl ); // Apply the object's local matrix m_pd3dDevice->SetTransform(D3DTRANSFORMSTATE_WORLD, &m_pObjects[0].matLocal ); // Draw the object m_pd3dDevice->DrawIndexedPrimitive( D3DPT_TRIANGLELIST, D3DFVF_VERTEX, m_pvObjectVertices, 16, m_pwObjectIndices, 30, 0 );

We are using a Z-Buffer here by calling

m_pd3dDevice->SetRenderState( D3DRENDERSTATE_ZENABLE, TRUE );

in InitDeviceObjects() and clearing the z-buffer with Clear() shown above. That's not a big thing ... is it? But z-buffers play an important role in task of visible surface determination. Switch it off and you'll see what I mean. Polygons closer to the camera must obscure polygons which are farther away. There are a number of solutions for this task, for example drawing all the polygons back to front, which is slow and not supported by most hardware, Binary Space Partition trees, Octrees and so on. Direct3D supports the creation of a DirectDraw surface that stores depth information for every pixel on the display. Before displaying your virtual world, Direct3D clears every pixel on this depth buffer to the farthest possible depth value. Then when rasterizing, Direct3D determines the depth of each pixel on the polygon. Is a pixel closer to the camera than the one previously stored in the depth buffer, the pixel is displayed and the new depth value is stored in the depth buffer. This process will continue until all pixels are drawn.

There's not only a z-buffer, but there's also a w-buffer. Think of the w-buffer as a higher quality z-buffer, which isn't supported in hardware as often as z-buffers. It reduces problems exhibited in z-buffers with objects at a distance and has a constant performance for both near and far objects. You only have to replace TRUE in the SetRenderState() call through D3DZB_USEW to use it.

As usual the Render() method uses the BeginScene()/EndScene() pair. The first function is called before performing rendering, the second after that. BeginScene causes the system to check its internal data structures, the availability and validity of rendering surfaces, and sets an internal flag to signal that a scene is in progress. Attempts to call rendering methods when a scene is not in progress fail, returning D3DERR_SCENE_NOT_IN_SCENE. Once your rendering is complete, you need to call EndScene(). It clears the internal flag that indicates that a scene is in progress, flushes the cached data and makes sure the rendering surfaces are OK.

The second parameter of DrawIndexedPrimitive(), D3DFVF_VERTEX, describes the vertex format used for this set of primitives. The d3dtypes.h header file declares these flags to explicitly describe a vertex format and provides helper macros that act as common combinations of such flags.

#define D3DFVF_VERTEX ( D3DFVF_XYZ | D3DFVF_NORMAL | D3DFVF_TEX1 )

Each of the rendering methods of IDirect3Ddevice7 accepts a combination of these flags, and uses them to determine how to render primitives. Basically, these flags tell the system which vertex components—position, normal, colors, and the number of texture coordinates–your application uses and, indirectly, which parts of the rendering pipeline you want Direct3D to apply to them. In addition, the presence or absence of a particular vertex format flag communicates to the system which vertex component fields are present in memory, and which you've omitted. By using only the needed vertex components, your application can conserve memory and minimize the processing bandwidth required to render models.

D3DFVF_XYZ includes the position of an untransformed vertex. You have to specify a vertex normal, a vertex color component (D3DFVF_DIFFUSE or D3DFVF_SPECULAR), or include at least one set of texture coordinates (D3DFVF_TEX1 through D3DFVF_TEX8). D3DFVF_NORMAL shows that the vertex format includes a vertex normal vector and D3DFVF_TEX1 shows us the number of texture coordinate sets for this vertex. Here it's one texture coordinate set.

The unlit and untransformed vertex format is equivalent to the older pre DirectX 6 structure D3DVERTEX:

typedef struct _D3DVERTEX { union { D3DVALUE x; /* Homogeneous coordinates */ D3DVALUE dvX; }; union { D3DVALUE y; D3DVALUE dvY; }; union { D3DVALUE z; D3DVALUE dvZ; }; union { D3DVALUE nx; /* Normal */ D3DVALUE dvNX; }; union { D3DVALUE ny; D3DVALUE dvNY; }; union { D3DVALUE nz; D3DVALUE dvNZ; }; union { D3DVALUE tu; /* Texture coordinates */ D3DVALUE dvTU; }; union { D3DVALUE tv; D3DVALUE dvTV; };

DeleteDeviceObjects()

This method is not used here. It's empty.

FinalCleanup()

We're destroying the DirectInput device here. You should use this method in DeleteDeviceObjects(), because if you switch, for example, from windowed to fullscreen mode, the device would be destroyed every time.

HRESULT CMyD3DApplication::FinalCleanup() { // release keyboard stuff DestroyInputDevice(); return S_OK; }

Finale

I hope you enjoyed our small trip into the world of the Direct3D 7 IM Framework and transformation. This will be a work in progress in the future. If you find any mistakes or if you have any good ideas to improve this tutorial or if you dislike or like it, give me a sign at wolf@direct3d.net.


© 1999-2000 Wolfgang Engel, Mainz, Germany