(Last modified: 20. July 2000)
[size="5"]Preface
You may recall that in the first tutorial "The Basics", we determined that all samples which are built with the Direct3D Framework in the DirectX SDK are created by providing overloaded versions of the CD3DApplication methods:
...
ConfirmDevice()
OneTimeSceneInit()
InitDeviceObjects()
FrameMove()
Render()
DeleteDeviceObjects()
FinalCleanup()
...
In this tutorial we will start to write our first animated app. It will show a red and a yellow object, which can be rotated around its x and y axis. The application uses a Z-Buffer and the simplest keyboard interface I can think of. You can move and rotate the camera with the up, down, left, right, c and the x keys. The input is handled via DirectInput. The movement of the camera feels a little bit like the first X-Wing games. Only a space scanner is missing :-).
[bquote]This is in response to a lot of e-mails I received. Readers of the first version of this tutorial wanted to know how to rotate and move more than one object in a scene independently.[/bquote]
As always, you can switch between the fullscreen and windowed mode with ALT-F4. F1 will show you the about box. F2 will give you a selection of useable drivers and ESC will shutdown the app. To compile the source, take a look at The Basics Tutorial. Be sure to also link dinput.lib into the project.
[bquote]One of the best ways to learn how to use world transformations with Direct3D is the Boids sample from the DirectX 7 SDK. I've found a good tutorial on camera orientation with Direct3DX at Mr. Gamemaker. Other interesting documents on camera orientation are at flipCode, CrystalSpace, Dave's Math Tables and the pages on Geometry from Paul Bourke. In addition, the pages of Mayhem on Matrices and of course the book from John de Goes on Direct3D 3D Game Programming with C++ are useful. For the DirectInput part, I've found Andr? LaMothe's Tricks of the Windows Game Programming Gurus very useful.[/bquote]
[size="5"]The Third Dimension
You need a good familiarity with 3-D geometric principles to program Direct3D applications. The first step in understanding these principles is understanding the transformation pipeline as part of the rendering pipeline:
[attachment=2881:rendering_pipeline_t&l_marked.gif]
The figure shows the Direct3D pipeline.
[bquote]The vertex shader unit will be introduced in one of the first tutorials after DirectX 8 is released. In the case of the Geforce2 Graphics Processing Unit, it's a programmable SIMD engine with storage of 192 quadwords of data and 128 app-downloadable instructions, which specify the tasks executed on each vertex. It can do standard transformations, clipping, vertex blending, morphing, animation, skinning, elevation fog, mesh warping, procedural texture coordinate generation such as reflection maps and lighting.[/bquote]
I focus here on the marked T & L Pipeline: You can think of the process of texturing and lighting as an assembly line in a factory, in which untransformed and unlit vertices enter one end, and then several sequential operations are performed on them. At the end of the assembly line, transformed and lit vertices exit. A lot of programmers have implemented their own transformation and lighting algorithms. They can disable parts of this pipeline and send the vertices that are already transformed and lit to Direct3D. [bquote]The pixel shader is a compact but powerful programmable pixel processor that runs, in the case of the Geforce 2, as a nine-instruction program on each pixel. It consists of eight register combiners cascaded together, with each taking inputs from up to four textures (which themselves may have been cascaded together), constants, interpolated values and scratch registers.[/bquote]
But in most cases it will be best to use the Direct3D T&L pipeline, because it's really fast, especially with the new T&L graphics drivers, which are provided with the Geforce and Savage 2000 chipsets. These graphic processors have gained an important role in the last couple of years. Most tasks in the rendering pipeline are now computed by the graphics processing unit:
In the old days, transform and lighting functions of the 3D pipeline have been performed by the CPU of the PC. Since 1999, affordable graphic cards with dedicated hardware T&L acceleration have been available. With these cards higher graphics performance is possible, because they can process graphic functions up to four times the speed of the leading CPUs. On the other side, the CPU can now be better utilized for functions such as sophisticated artificial intelligence (AI), realistic physics and more complex game elements. So the new generation of cards will provide a lot of horsepower for the new game generation. It's a great time for game programmers :-)
[size="5"]Transformation Pipeline
It's a complex task to describe and display 3D graphics objects and environments. Describing the 3D data according to different frames of reference or different coordinate systems reduce the complexity. These different frames of reference are called "spaces" such as model space, world space, view space, projection space. Because these spaces use different coordinate systems, 3D data must be converted or "transformed" from one space to another.
[attachment=2883:t&l_pipeline.gif]
The transformation pipeline transforms each vertex of the object from an abstract, floating-point coordinate space into pixel-based screen space, taking into account the properties of the virtual camera used to render the scene. This transform is done with three transformation matrices: the world-, view- and projection-matrix. The use of world-, view- and projection- transformations ensures that Direct3D only has to deal with one coordinate system per step. Between those steps, the objects are oriented in a uniform manner.
The world transformation stage transforms an object from model or object to world space. Model space is the coordinate space in which an object is defined, independant of other objects and the world itself. In model space, the points of the model or object are rotated, scaled and translated to animate it. For example, think of a Quake 3 model, which rotates his torso and holds his weapon in your direction. With model space it's easier and faster to move an object by simply redefining the transformation from model space to world space than it would be to manually change all the coordinates of the object in world space. For example, to rotate a sphere or cube around its center looks more natural and is much easier when the origin is at the center of the object, regardless of where in world space the object is positioned. Worldspace is the abolute frame of reference for a 3-D world; all object locations and orientations are with respect to worldspace. It provides a coordinate space that all objects share, instead of requiring a unique coordinate system for each object.
To transform the objects from model to world space each object will be rotated about the x-axis, y-axis, or z-axis, scaled (enlarging or shrinking the object) and translated, by moving the object along the x-axis, y-axis, or z-axis to its final orientation, position and size.
Direct3D uses a left-handed coordinate system, in which every positive axis (x, y or z) is pointing away from the viewer:
The view transformation stage transforms the objects from world space into camera space or view space. This transformation puts the camera at the origin and points it directly down the positive z-axis. The geometry has to be translated in suitable 2D shapes. It's also useful for lighting and backface culling. The light coordinates, which are specified in world space, are transformed into camera space at this stage and the effect of light is calculated and applied to the vertices.
[bquote]Backface culling happens here. It would be a waste of CPU time to draw the backface of the cube, when it's not visible. To omit drawing the backface is called backface culling. To cull the backface, we need a way to determine the visibility. One of the simplest ways is the following: a front face is one in which vertices are defined in clockwise order. So Direct3D only draws the faces with vertices in clockwise order by default. To modify backface culling, use
When lighting is enabled, as Direct3D rasterizes a scene in the final stage of rendering, it determines the color of each rendered pixel based on a combination of the current material color (and the texels in an associated texture map), the diffuse and specular colors of the vertex, if specified, as well as the color and intensity of light produced by light sources in the scene or the scene's ambient light level. [/bquote]
The projection transformation takes into consideration the camera's horizontal and vertical fields of view, so it applies perspective to the scene. Objects which are close to the front of the frustrum are expanded and objects close to the end are shrunk. It warps the geometry of the frustrum of the camera into a cube shape by setting the passed in 4x4 matrix to a perspective projection matrix built from the field-of-view or viewing frustrum, aspect ratio, near plane or front clipping plane, and far plane or back clipping plane. This makes it easy to clip geometry that lies both inside and outside of the viewing frustrum. You can think of the projection transformation as controlling the camera's internals; it is analogous to choosing a lens for the camera.// no backface culling
m_pd3dDevice->SetRenderState(D3DRENDERSTATE_CULLMODE, D3DCULL_NONE);
[bquote]Clipping: the geometry is clipped to the cube shape and the resulting clipped geometry is then transformed into 2D by dropping the z-coordinate and scaling the points to screen coordinates.[/bquote]
[size="5"]Transformation Math
Let's give our old math teachers a smile :-) . I learned math from the beginning-seventies to the mid-eighties at school (yes ... we've got another education system here in Germany). At that time, I never thought that there would be such an interesting use (i.e. game-programming) for it. I wonder if math teachers today talk about the use of math in computer games.
Any impressive game requires correct transformations: Consider the following example. An airplane, let's say an F22, is oriented such that its nose is pointing in the positive z direction, its right wing is pointing in the positive x direction and its cockpit is pointing in the positive y direction. So the F22's local x, y and z axes are aligned with the world x, y and z axes. If this airplane is to be rotated 90 degrees about its y axis, its nose would be pointing toward the world -x axis, its right wing toward the world z axis and its cockpit will remain in the world +y direction. From this new position, rotate the F22 about its z axis. If your transformations are correct, the airplane will rotate about its own z-axis. If your transformations are incorrect, the F22 will rotate about the world z axis. In Direct3D you can guarantee the correct transformation by using 4x4 matrices.
Matrices are rectangular arrays of numbers. A 4x4 world matrix contains 4 vectors, which represent the world space coordinates of the x, y and z unit axis vectors, and the world space coordinate which is the origin of these axis vectors:
x x x 0
y y y 0
z z z 0
x y z 1
[bquote]Vectors are one of the most important concepts in 3D games. They are mathematical entities that describe a direction and a magnitude (which can, for example, be used for speed). A general purpose vector consists of two coordinates. You can see the direction of these vectors by drawing a line between the two coordinates. Magnitude is the distance between the points.
The first coordinate is called the initial point and the second is the final point. Three dimensional games often use a specific kind of vector - the free vector. Its initial point is assumed to be the origin, and only the final point is specified.
Vectors are usually denoted by a bold face letter of the alphabet, i.e. a. So, we could say the vector v = (1,2,3). The first column is units in the x direction, the second column is units in the y direction, the third column, units in z.[/bquote]
The first column contains the world space coordinates of the local x axis. The second column contains the local y axis and the third column the world space coordinates of the local z axis. The vectors are unit vectors whose magnitude are 1. Basically unit vectors are used to define directions, when magnitude is not really important. The last row contains the world space coordinates of the object's origin, which translates the object. The first coordinate is called the initial point and the second is the final point. Three dimensional games often use a specific kind of vector - the free vector. Its initial point is assumed to be the origin, and only the final point is specified.
Vectors are usually denoted by a bold face letter of the alphabet, i.e. a. So, we could say the vector v = (1,2,3). The first column is units in the x direction, the second column is units in the y direction, the third column, units in z.[/bquote]
A special matrix is the identity matrix:
1 0 0 0
0 1 0 0
0 0 1 0
0 0 0 1
This matrix could be accessed by
D3DMATRIX mat;
mat._11 = 1.0f; mat._12 = 0.0f; mat._13 = 0.0f; mat._14 = 0.0f;
mat._21 = 0.0f; mat._22 = 1.0f; mat._23 = 0.0f; mat._24 = 0.0f;
mat._31 = 0.0f; mat._32 = 0.0f; mat._33 = 1.0f; mat._34 = 0.0f;
mat._41 = 0.0f; mat._42 = 0.0f; mat._43 = 0.0f; mat._44 = 1.0f;
[bquote]If an object's position in model space corresponds to its position in world space, simply set the world transformation matrix to the identity matrix.[/bquote]
A typical transformation operation is a 4x4 matrix multiply operation. A transformation engine multiplies a vector representing 3D data, typically a vertex or a normal vector, by a 4x4 matrix. The result is the transformed vector. This is done with standard linear algebra:Transform Original Transformed
Matrix Vector Vector
a b c d x ax + by + cy + dw x'
e f g h x y = ex + fy + gz + hw = y'
i j k l z ix + jy + kz + lw z'
m n o p w mx + ny + oz + pw w'
One of the pros of using matrix multiplication is that scaling, rotation and translation all take the same amount of time to perform. So the performance of a dedicated transform engine is predictable and consistent. This allows software developers to make informed decisions regarding performance and quality.
[size="3"]The World Matrix
Usually the world matrix is a combination of translation, rotation and scaling the matrices of the objects. Code for a translate and rotate world matrix could look like this:
struct Object
{
D3DVECTOR vLocation;
FLOAT fYaw, fPitch, fRoll;
...
D3DMATRIX matLocal;
};
class CMyD3DApplication : public CD3DApplication
{
...
Object m_pObjects[NUM_OBJECTS];
...
};
// in FrameMove()
for (WORD i = 0; i < dwnumberofobjects; i++)
{
D3DUtil_SetTranslateMatrix( matWorld, m_pObject.vLocation );
D3DMATRIX matTemp, matRotateX, matRotateY, matRotateZ;
D3DUtil_SetRotateYMatrix( matRotateY, m_pObject.fYaw );
D3DUtil_SetRotateXMatrix( matRotateX, m_pObject.fPitch );
D3DUtil_SetRotateZMatrix( matRotateZ, m_pObject.fRoll );
D3DMath_MatrixMultiply( matTemp, matRotateX, matRotateY );
D3DMath_MatrixMultiply( matTemp, matRotateZ, matTemp );
D3DMath_MatrixMultiply( matWorld, matTemp, matWorld );
m_pObject.matLocal = matWorld;
}
// in Render()
for (WORD i = 0; i < dwnumberofobjects; i++)
{
...
m_pd3dDevice->SetTransform(D3DTRANSFORMSTATE_WORLD, &m_pObject.matLocal );
...
}
|W| = |M| * |T| * |X| * |Y| * |Z|
The above piece of code translates the object into its place with [font="Courier New"][color="#000080"]D3DUtil_SetTranslateMatrix()[/color][/font]. Translation can best be described as a linear change in position. This change can be represented by a delta vector [tx, ty, tz], where tx (often called dx) represents the change in the object's x position, ty (or dy) represents the change in its y position, and tz (or dz) the change in its z position. You can find [font="Courier New"][color="#000080"]D3DUtil_SetTranslateMatrix()[/color][/font] in d3dutil.h.
inline VOID D3DUtil_SetTranslateMatrix( D3DMATRIX& m, FLOAT tx, FLOAT ty, FLOAT tz )
{
D3DUtil_SetIdentityMatrix( m );
m._41 = tx; m._42 = ty; m._43 = tz;
}
=
1 0 0 0
0 1 0 0
0 0 1 0
tx ty tz 1
The next operation that is performed by our code piece is rotation. Rotation can be described as circular motion about some axis. The incremental angles used to rotate the object here represent rotation from the current orientation. That means, by rotating 1 degree about the z axis, you tell your object to rotate 1 degree about its z axis regardless of its current orientation and regardless on how you got the orientation. This is how the real world operates.
[font="Courier New"][color="#000080"]D3DUtil_SetRotateYMatrix()[/color][/font] rotates the objects about the y-axis, where fRads equals the amount you want to rotate about this axis. You can find it, like all the other rotation matrices, in d3dutil.h.
VOID D3DUtil_SetRotateYMatrix( D3DMATRIX& mat, FLOAT fRads )
{
D3DUtil_SetIdentityMatrix( mat );
mat._11 = cosf( fRads );
mat._13 = -sinf( fRads );
mat._31 = sinf( fRads );
mat._33 = cosf( fRads );
}
=
cosf fRads 0 -sinf fRads 0
0 0 0 0
sinf fRads 0 cosf fRads 0
0 0 0 0
VOID D3DUtil_SetRotateXMatrix( D3DMATRIX& mat, FLOAT fRads )
{
D3DUtil_SetIdentityMatrix( mat );
mat._22 = cosf( fRads );
mat._23 = sinf( fRads );
mat._32 = -sinf( fRads );
mat._33 = cosf( fRads );
}
=
1 0 0 0
0 cos fRads sin fRads 0
0 -sin fRads cos fRads 0
0 0 0 0
VOID D3DUtil_SetRotateZMatrix( D3DMATRIX& mat, FLOAT fRads )
{
D3DUtil_SetIdentityMatrix( mat );
mat._11 = cosf( fRads );
mat._12 = sinf( fRads );
mat._21 = -sinf( fRads );
mat._22 = cosf( fRads );
}
=
cosf fRads sinf fRads 0 0
-sinf fRads cos fRads 0 0
0 0 0 0
0 0 0 0
a b c d A B C D
e f g h * E F G H =
i j k l I J K L
m n o p M N O P
a*A+b*E+c*I+d*M a*B+b*F+c*J+d*N a*C+b*G+c*K+d*O a*D+b*H+c*L+d*P
e*A+f*E+g*I+h*M e*B+f*F+g*J+h*N etc.
VOID D3DMath_MatrixMultiply( D3DMATRIX& q, D3DMATRIX& a, D3DMATRIX& b )
{
FLOAT* pA = (FLOAT*)&a
FLOAT* pB = (FLOAT*)&b
FLOAT pM[16];
ZeroMemory( pM, sizeof(D3DMATRIX) );
for (WORD i=0; i< 4; i++)
for (WORD j=0; j< 4; j++)
pM [j]= pA[0] * pB[0][j]
+ pA[1] * pB[1][j]
+ pA[2] * pB[2][j]
+ pA[3] * pB[3][j];
memcpy( &q, pM, sizeof(D3DMATRIX) );
}
VOID D3DMath_MatrixMultiply( D3DMATRIX& q, D3DMATRIX& a, D3DMATRIX& b )
{
FLOAT* pA = (FLOAT*)&a
FLOAT* pB = (FLOAT*)&b
FLOAT pM[16];
ZeroMemory( pM, sizeof(D3DMATRIX) );
for( WORD i=0; i<4; i++ )
for( WORD j=0; j<4; j++ )
for( WORD k=0; k<4; k++ )
pM[4*i+j] += pA[4*i+k] * pB[4*k+j];
memcpy( &q, pM, sizeof(D3DMATRIX) );
}
[size="3"]The View Matrix
The view matrix describes the position and the orientation of a viewer in a scene. This is normally the position and orientation of you, looking through the glass of your monitor into the scene. This thinking model is abstracted by a lot of authors by talking about a camera through which you are looking into the scene.
To rotate and translate the viewer or camera in the scene, three vectors are needed. These could be called the LOOK, UP and RIGHT vectors.
They define a local set of axes for the camera and will be set at the start of the application in the [font="Courier New"][color="#000080"]InitDeviceObjects()[/color][/font] or in the [font="Courier New"][color="#000080"]FrameMove()[/color][/font] method of the framework.
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);
[bquote]Taking the cross product of any two vectors forms a third vector perpendicular to the plane formed by the first two. The cross product is used to determine which way polygons are facing. It uses two of the polygon's edges to generate a normal. Thus, it can be used to generate a normal to any surface for which you have two vectors that lie within the surface. Unlike the dot product, the cross product is not commutative. a x b = - (b x a). The magnitude of the cross product of a and b, ||axb|| is given by ||a||*||b||*sin(@). The direction of the resultant vector is orthogonal to both a and b.
Furthermore, the cross product is used to derive the plane equation for a plane determined by two intersecting vectors.[/bquote]
Now imagine, the player is sitting in the cockpit of an F22 instead of looking at it from outside. If the player pushes his foot pedals in his F22 to the left or right, the LOOK and the RIGHT vector has to be rotated about the UP vector (YAW effect) or y axis. If he pushes his flight stick to the right or left, the UP and RIGHT vectors have to be rotated around the LOOK vector (ROLL effect) or z axis. If he pushes the flight stick forward and backward, we have to rotate the LOOK and UP vectors around the RIGHT vector (PITCH effect) or x axis. Furthermore, the cross product is used to derive the plane equation for a plane determined by two intersecting vectors.[/bquote]
There's one problem: when computers handle floating point numbers, little accumulation errors happen whilst doing all this rotation math. After a few rotations these rounding errors make the three vectors un-perpendicular to each other. It's obviously important for the three vectors to stay at right angles from each other. The solution is Base Vector Regeneration. It must be performed before the vectors are rotated around one another. We'll use the following code to handle base vector regeneration:
vLook = Normalize(vLook);
vRight = CrossProduct( vUp, vLook); // Cross Product of the UP and LOOK vector
vRight = Normalize (vRight);
vUp = CrossProduct (vLook, vRight); // Cross Product of the RIGHT and LOOK vector
vUp = Normalize(vUp);
[font="Courier New"][color="#000080"] x?+y?+z? = m?
[/color][/font]
The length of the vector is retrieved by[/color][/font]
[font="Courier New"][color="#000080"] ||A|| = sqrt (x? + y? + z?)
[/color][/font]
It's the square root of the Pythagorean theorem. The magnitude of a vector has a special symbol in mathematics. It's a capital letter designated with two vertical bars ||A||. [/color][/font]
To normalize a vector, the following inline functions in [font="Courier New"][color="#000080"]d3dvec.inl[/color][/font] are defined:
inline _D3DVECTOR
Normalize (const _D3DVECTOR& v)
{
return v / Magnitude(v);
}
inline D3DVALUE
Magnitude (const _D3DVECTOR& v)
{
return (D3DVALUE) sqrt(SquareMagnitude(v));
}
inline D3DVALUE
SquareMagnitude (const _D3DVECTOR& v)
{
return v.x*v.x + v.y*v.y + v.z*v.z;
}
[font="Courier New"][color="#000080"][bquote]sqrt()[/color][/font] is a mathematical function from the math library of Visual C/C++ provided by Microsoft(TM). Other compilers should have a similar function.[/bquote]
After normalizing the LOOK vector, we create the RIGHT vector by assigning it the cross product of UP and LOOK vector and normalize it. The UP vector is created out of the cross product of the LOOK and RIGHT vector and a normalization thereafter. After that, we build the pitch, yaw and roll matrices out of these vectors:
// 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)
=
vx ux nx 0
vy uy ny 0
vz uz nz 0
-(u * c) -(v * c) -(n * c) 1
The x, y and z translation factors are computed by taking the negative of the dot product between the camera position and the u, v, and n vectors. They are negated because the camera works the opposite to objects in the 3D world.
To rotate the vectors two about another, we change fPitch, fYaw and fRoll variables like this:
fPitch=-0.3f * m_fTimeElapsed;
...
fPitch=+0.3f * m_fTimeElapsed;
...
fYaw=-0.3f * m_fTimeElapsed;
...
fYaw=+0.3f * m_fTimeElapsed;
...
fRoll=-0.3f * m_fTimeElapsed;
...
fRoll=+0.3f * m_fTimeElapsed;
...
vPos.x+=fspeed*vLook.x;
vPos.y+=fspeed*vLook.y;
vPos.z+=fspeed*vLook.z;
vPos.x-=fspeed*vLook.x;
vPos.y-=fspeed*vLook.y;
vPos.z-=fspeed*vLook.z;
An interesting transform is the perspective projection, which is used in Direct3D. It converts the camera's viewing frustrum (the pyramid-like shape that defines what the camera can see) into a cube space, as seen above (with a cube shaped geometry, clipping is much easier). Objects close to the camera are enlarged greatly, while objects farther away are enlarged less. Here, parallel lines are generally not parallel after projection. This transformation applies perspective to a 3D scene. It projects 3D geometry into a form that can be viewed on a 2D display.
The projection matrix is set with [font="Courier New"][color="#000080"]D3DUtil_SetProjectionMatrix()[/color][/font] in d3dutil.cpp.
HRESULT D3DUtil_SetProjectionMatrix( D3DMATRIX& mat, FLOAT fFOV, FLOAT fAspect,
FLOAT fNearPlane, FLOAT fFarPlane )
{
if( fabs(fFarPlane-fNearPlane) < 0.01f )
return E_INVALIDARG;
if( fabs(sin(fFOV/2)) < 0.01f )
return E_INVALIDARG;
FLOAT w = fAspect * ( cosf(fFOV/2)/sinf(fFOV/2) );
FLOAT h = 1.0f * ( cosf(fFOV/2)/sinf(fFOV/2) );
FLOAT Q = fFarPlane / ( fFarPlane - fNearPlane );
ZeroMemory( &mat, sizeof(D3DMATRIX) );
mat._11 = w;
mat._22 = h;
mat._33 = Q;
mat._34 = 1.0f;
mat._43 = -Q*fNearPlane;
return S_OK;
}
=
w 0 0 0
0 h 0 0
0 0 Q 1
0 0 -QZN 0
After this last transform, the geometry must be clipped to the cube space and converted from homogenous coordinates to screen coordinates by dividing the x-, y- and z-coordinates of each point by w. Direct3D performs these steps internally.
[bquote]Homogenous coordinates: Just think of a 3x3 matrix. As you've learned above, in a 4x4 matrix, the first three elements, let's say i,j and k, in the fourth row are needed to translate the object. With 3x3 matrices an object cannot be translated without changing its orientation. If you add some vector (representing translation) to the i, j and k vectors, their orientation will also change. So we need fourth dimension with the so called homogenous coordinates.
In this 4D space, every point has a fourth component that measures distance along an imaginary fourth-dimensional axis called w. To convert a 4D point into a 3D point, you have to divide each component x, y, z and w by w. So every multiplication of a point whose w component is equal to 1 represent that same point. For example (4, 2, 8, 2) represents the same point as (2, 1, 4, 1).
To describe distance on the w-axis, we need another vector l. It points in the positive direction of the w-axis and its neutral magnitude is 1, like the other vectors.
In Direct3D, the points remain homogenous, even after being sent through the geometry pipeline. Only after clipping, when the geometry is ready to be displayed, are these points converted into Cartesian coordinates by dividing each component by w.
Before doing the transformation stuff, normally the viewport is set by the Direct3D IM Framework. It defines how the horizontal, vertical and depth components of a polygon's coordinates in cube space will be scaled before the polygon is displayed.[/bquote]
In this 4D space, every point has a fourth component that measures distance along an imaginary fourth-dimensional axis called w. To convert a 4D point into a 3D point, you have to divide each component x, y, z and w by w. So every multiplication of a point whose w component is equal to 1 represent that same point. For example (4, 2, 8, 2) represents the same point as (2, 1, 4, 1).
To describe distance on the w-axis, we need another vector l. It points in the positive direction of the w-axis and its neutral magnitude is 1, like the other vectors.
In Direct3D, the points remain homogenous, even after being sent through the geometry pipeline. Only after clipping, when the geometry is ready to be displayed, are these points converted into Cartesian coordinates by dividing each component by w.
Before doing the transformation stuff, normally the viewport is set by the Direct3D IM Framework. It defines how the horizontal, vertical and depth components of a polygon's coordinates in cube space will be scaled before the polygon is displayed.[/bquote]
[size="5"]Down to the Code
The sample uses (as usual in this series) the Direct3D Framework. The application class in animated objects.cpp looks like:
class CMyD3DApplication : public CD3DApplication
{
D3DVERTEX m_pvObjectVertices[16];
WORD m_pwObjectIndices[30];
Object m_pObjects[2];
FLOAT m_fStartTimeKey, // Time reference for calculations
m_fTimeElapsed;
static HRESULT ConfirmDevice( DDCAPS* pddDriverCaps,
D3DDEVICEDESC7* pd3dDeviceDesc );
protected:
HRESULT OneTimeSceneInit();
HRESULT InitDeviceObjects();
HRESULT FrameMove( FLOAT fTimeKey );
HRESULT Render();
HRESULT DeleteDeviceObjects();
HRESULT FinalCleanup();
public:
CMyD3DApplication();
};
[size="3"]OneTimeSceneInit()
The [font="Courier New"][color="#000080"]OneTimeSceneInit()[/color][/font] function performs basically any one-time resource allocation and is invoked once per application execution cycle. Here it contains the code to construct the two objects:
HRESULT CMyD3DApplication::OneTimeSceneInit()
{
// Points and normals which make up a object geometry
D3DVECTOR p1 = D3DVECTOR( 0.00f, 0.00f, 0.50f );
D3DVECTOR p2 = D3DVECTOR( 0.50f, 0.00f,-0.50f );
D3DVECTOR p3 = D3DVECTOR( 0.15f, 0.15f,-0.35f );
D3DVECTOR p4 = D3DVECTOR(-0.15f, 0.15f,-0.35f );
D3DVECTOR p5 = D3DVECTOR( 0.15f,-0.15f,-0.35f );
D3DVECTOR p6 = D3DVECTOR(-0.15f,-0.15f,-0.35f );
D3DVECTOR p7 = D3DVECTOR(-0.50f, 0.00f,-0.50f );
D3DVECTOR n1 = Normalize( D3DVECTOR( 0.2f, 1.0f, 0.0f ) );
D3DVECTOR n2 = Normalize( D3DVECTOR( 0.1f, 1.0f, 0.0f ) );
D3DVECTOR n3 = Normalize( D3DVECTOR( 0.0f, 1.0f, 0.0f ) );
D3DVECTOR n4 = Normalize( D3DVECTOR(-0.1f, 1.0f, 0.0f ) );
D3DVECTOR n5 = Normalize( D3DVECTOR(-0.2f, 1.0f, 0.0f ) );
D3DVECTOR n6 = Normalize( D3DVECTOR(-0.4f, 0.0f, -1.0f ) );
D3DVECTOR n7 = Normalize( D3DVECTOR(-0.2f, 0.0f, -1.0f ) );
D3DVECTOR n8 = Normalize( D3DVECTOR( 0.2f, 0.0f, -1.0f ) );
D3DVECTOR n9 = Normalize( D3DVECTOR( 0.4f, 0.0f, -1.0f ) );
// Vertices for the top
m_pvObjectVertices[ 0] = D3DVERTEX( p1, n1, 0.000f, 0.500f );
m_pvObjectVertices[ 1] = D3DVERTEX( p2, n2, 0.500f, 1.000f );
m_pvObjectVertices[ 2] = D3DVERTEX( p3, n3, 0.425f, 0.575f );
m_pvObjectVertices[ 3] = D3DVERTEX( p4, n4, 0.425f, 0.425f );
m_pvObjectVertices[ 4] = D3DVERTEX( p7, n5, 0.500f, 0.000f );
// Vertices for the bottom
...
// Vertices for the rear
...
Point #1 is m_pvObjectVertices[0] and m_pvObjectVertices[5], point #2 is m_pvObjectVertices[1] and m_pvObjectVertices[6], point #3 is m_pvObjectVertices[3] and m_pvObjectVertices[11], etc.
Every point is declared as a vector with D3DVECTOR. For every face of the object a normal is defined, so that there are nine normals.
[bquote]The normal vector is used in Gouraud shading mode, to control lighting and do some texturing effects. Direct3D applications do not need to specify face normals; the system calculates them automatically when they are needed.[/bquote]
The normal vectors are normalized with a call toD3DVECTOR n1 = Normalize( D3DVECTOR( 0.2f, 1.0f, 0.0f ) );
The last two variables of D3DVERTEX are the texture coordinates. Most textures, like bitmaps, are a two dimensional array of color values. The individual color values are called texture elements, or texels. Each texel has a unique address in the texture: its texel coordinate. Direct3D programs specify texel coordinates in terms of u,v values, much like 2-D Cartesian coordinates are specified in terms of x,y coordinates. The address can be thought of as a column and row number. However, in order to map texels onto primitives, Direct3D requires a uniform address range for all texels in all textures. Therefore, it uses a generic addressing scheme in which all texel addresses are in the range of 0.0 to 1.0 inclusive.
[bquote]Direct3D maps texels in texture space directly to pixels in screen space. The screen space is a frame of reference in which coordinates are related directly to 2-D locations in the frame buffer, to be displayed on a monitor or other viewing device. Projection space coordinates are converted to screen space coordinates, using a transformation matrix created from the viewport parameters. This sampling process is called texture filtering. There are four texture filtering methods supported by Direct3D: Nearest Point Sampling, Linear Texture Filtering, Anisotropic Texture Filtering, Texture Filtering With Mipmaps.[/bquote]
We're not using a texture here, so more on texture mapping in Tutorial #3 "Multitexturing". Now on to the next part of the [font="Courier New"][color="#000080"]OneTimeSceneInit()[/color][/font] method:
// Vertex indices for the object
m_pwObjectIndices[ 0] = 0; m_pwObjectIndices[ 1] = 1; m_pwObjectIndices[2] = 2;
m_pwObjectIndices[ 3] = 0; m_pwObjectIndices[ 4] = 2; m_pwObjectIndices[5] = 3;
m_pwObjectIndices[ 6] = 0; m_pwObjectIndices[ 7] = 3; m_pwObjectIndices[8] = 4;
m_pwObjectIndices[ 9] = 5; m_pwObjectIndices[10] = 7; m_pwObjectIndices[11] = 6;
m_pwObjectIndices[12] = 5; m_pwObjectIndices[13] = 8; m_pwObjectIndices[14] = 7;
m_pwObjectIndices[15] = 5; m_pwObjectIndices[16] = 9; m_pwObjectIndices[17] = 8;
m_pwObjectIndices[18] = 10; m_pwObjectIndices[19] = 15; m_pwObjectIndices[20] = 11;
m_pwObjectIndices[21] = 11; m_pwObjectIndices[22] = 15; m_pwObjectIndices[23] = 12;
m_pwObjectIndices[24] = 12; m_pwObjectIndices[25] = 15; m_pwObjectIndices[26] = 14;
m_pwObjectIndices[27] = 12; m_pwObjectIndices[28] = 14; m_pwObjectIndices[29] = 13;
[bquote]There are two ways of grouping the vertices that define a primitive: using non-indexed primitives and using indexed primitves. To create a nonindexed primitive, you fill an array with an ordered list of vertices. Ordered means that the order of the vertices in the array indicates how to build the triangles. The first triangle consists of the first three vertices, the second triangle consists of the next three vertices and so on. If you have two triangles that are connected, you'll have to specify the same vertices multiple times. To create an indexed primitive, you fill an array with an unordered list of vertices and specify the order with a second array (index array). This means that vertices can be shared by multiple triangles, simply by having multiple entries in the index array refer to the same vertex. Most 3D models share a number of vertices. Therefore, you can save bandwith and CPU time sharing these vertices among multiple triangles.
Defining indices into a list of vertices has one disadvantage: the cost of memory. There could be problems with sharing vertices of a cube. Lighting a cube is done by using its face normals, which is perpendicular to the face's plane. If the vertices of a cube are shared, there's only one shared vertex for two triangles. This shared vertex has only one normal to calculate the face normal, so the lighting effect wouldn't be what you want.[/bquote]
In [font="Courier New"][color="#000080"]OneTimeSceneInit()[/color][/font] the two objects are defined with the help of the [color="#000080"]m_pObjects[/color] structure. Defining indices into a list of vertices has one disadvantage: the cost of memory. There could be problems with sharing vertices of a cube. Lighting a cube is done by using its face normals, which is perpendicular to the face's plane. If the vertices of a cube are shared, there's only one shared vertex for two triangles. This shared vertex has only one normal to calculate the face normal, so the lighting effect wouldn't be what you want.[/bquote]
...
// yellow object
m_pObjects[0].vLoc = D3DVECTOR(-1.0f, 0.0f, 0.0f);
m_pObjects[0].fYaw = 0.0f;
m_pObjects[0].fPitch = 0.0f;
m_pObjects[0].fRoll = 0.0f;
m_pObjects[0].r = 1.0f;
m_pObjects[0].g = 0.92f;
m_pObjects[0].b = 0.0f;
// red object
m_pObjects[1].vLoc = D3DVECTOR(1.0f, 0.0f, 0.0f);
m_pObjects[1].fYaw = 0.0f;
m_pObjects[1].fPitch = 0.0f;
m_pObjects[1].fRoll = 0.0f;
m_pObjects[1].r = 1.0f;
m_pObjects[1].g = 0.0f;
m_pObjects[1].b = 0.27f;
return S_OK;
}
// 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 );
The [font="Courier New"][color="#000080"]InitDeviceObjects()[/color][/font] is used to initialize per-device objects such as loading texture bits onto a device surface, setting matrices and populating vertex buffers. First, we'll use it here to set a material. When lighting is enabled, as Direct3D rasterizes a scene in the final stage of rendering, it determines the color of each rendered pixel based on a combination of the current material color (and the texels in an associated texture map), the diffuse and specular colors at the vertex, if specified, as well as the color and intensity of light produced by light sources in the scene or the scene's ambient light level.
You must use materials to render a scene if you are letting Direct3D handle lighting.
HRESULT CMyD3DApplication::InitDeviceObjects()
{
D3DMATERIAL7 mtrl;
D3DUtil_InitMaterial( mtrl, 1.0f, 1.0f, 1.0f );
m_pd3dDevice->SetMaterial( &mtrl );
...
[font="Courier New"][color="#000080"]D3DUtil_InitMaterial()[/color][/font] sets the RGBA values of the material. Color values of materials represent how much of a given light component is reflected by a surface that is rendered with that material. A material's properties include diffuse reflection, ambient reflection, light emission and specular hightlighting:
- Diffuse reflection: Defines how the polygon reflects diffuse lighting (any light that does not come from ambient light). This is described in terms of a color, which represents the color best reflected by the polygon. Other colors are reflected less in proportion to how different they are from the diffuse color.
- Ambient reflection: Defines how the polygon reflects ambient lighting. This is described in terms of a color, which, as with diffuse reflection, represents the color best reflected by the polygon.
- Light emission: Makes the polygon appear to emit a certain color of light (this does not actually light up the world; it only changes the appearance of the polygon).
- Specular highlighting: Describes how shiny the polygon is. A material whose color components are R: 1.0, G: 1.0, B: 1.0, A: 1.0 will reflect all the light that comes its way. Likewise, a material with R: 0.0, G: 1.0, B: 0.0, A: 1.0 will reflect all of the green light that is directed at it. [font="Courier New"][color="#000080"]SetMaterial()[/color][/font] sets the material properties for the device.
After setting the material, we can setup the light. Color values for light sources represent the amount of a particular light component it emits. Lights don't use an alpha component, so you only need to think about the red, green, and blue components of the color. You can visualize the three components as the red, green, and blue lenses on a projection television. Each lens might be off (a 0.0 value in the appropriate member), it might be as bright as possible (a 1.0 value), or some level in between. The colors coming from each lens combine to make the light's final color. A combination like R: 1.0, G: 1.0, B: 1.0 creates a white light, where R: 0.0, G: 0.0, B: 0.0 results in a light that doesn't emit light at all. You can make a light that emits only one component, resulting in a purely red, green, or blue light, or the light could use combinations to emit colors like yellow or purple. You can even set negative color component values to create a "dark light" that actually removes light from a scene. Or, you might set the components to some value larger than 1.0 to create an extremely bright light. Direct3D employs three types of lights: point lights, spotlights, and directional lights.
You choose the type of light you want when you create a set of light properties. The illumination properties and the resulting computational overhead varies with each type of light source. The following types of light sources are supported by Direct3D 7:
- Point lights
- Spotlights
- Directional lights
DirectX 7.0 does not use the parallel-point light type offered in previous releases of DirectX. Tip: You should avoid spotlights, because there are more realistic ways of creating spotlights than the default method supplied by Direct3D: Such as texture blending: see the "Multitexturing" tutorials. The sample sets up an ambient light and, if the graphic card supports it, two directional lights.
An ambient light is effectively everywhere in a scene. It's a general level of light that fills an entire scene, regardless of the objects and their locations within that scene. Ambient light is everywhere and has no direction or position. There's only color and intensity. [font="Courier New"][color="#000080"]SetRenderState()[/color][/font] sets the ambient light by specifying [font="Courier New"][color="#000080"]D3DRENDERSTATE_AMBIENT[/color][/font] as the dwRenderStateType parameter, and the desired RGBA color as the dwRenderState parameter. Keep in mind that the color values of the material represent how much of a given light component is reflected by a surface. So the light properties are not the only properties which are responsible for the color of the object you will see....
// Set up the lights
m_pd3dDevice->SetRenderState( D3DRENDERSTATE_AMBIENT, 0x0b0b0b0b);
if( m_pDeviceInfo->ddDeviceDesc.dwVertexProcessingCaps &
D3DVTXPCAPS_DIRECTIONALLIGHTS )
{
D3DLIGHT7 light;
if( m_pDeviceInfo->ddDeviceDesc.dwMaxActiveLights > 0 )
{
D3DUtil_InitLight( light, D3DLIGHT_DIRECTIONAL, 0.5f, -1.0f, 0.3f );
m_pd3dDevice->SetLight( 0, &light );
m_pd3dDevice->LightEnable( 0, TRUE );
}
if( m_pDeviceInfo->ddDeviceDesc.dwMaxActiveLights > 1 )
{
D3DUtil_InitLight( light, D3DLIGHT_DIRECTIONAL, 0.5f, 1.0f, 1.0f );
light.dcvDiffuse.r = 0.5f;
light.dcvDiffuse.g = 0.5f;
light.dcvDiffuse.b = 0.5f;
m_pd3dDevice->SetLight( 1, &light );
m_pd3dDevice->LightEnable( 1, TRUE );
}
m_pd3dDevice->SetRenderState( D3DRENDERSTATE_LIGHTING, TRUE );
}
...
Additionally there are up to two directional lights used by the sample. Although we use directional lights and an ambient light to illuminate the objects in the scene, they are independent of one another. Directional light always has direction and color, and it is a factor for shading algorithms, such as Gouraud shading. It is equivalent to use a point light source at an infinite distance.
The sample first checks the capabilities of the graphics device. If it supports directional light, the light will be set by a call to the [font="Courier New"][color="#000080"]SetLight()[/color][/font] method, which uses the [font="Courier New"][color="#000080"]D3DLIGHT7[/color][/font] structure.
The position, range, and attenuation properties are used to define a light's location in world space, and how the light behaves over distance. The [font="Courier New"][color="#000080"]D3DUtil_InitLight()[/color][/font] method in d3dutil.cpp sets a few default values.typedef struct _D3DLIGHT7 {
D3DLIGHTTYPE dltType;
D3DCOLORVALUE dcvDiffuse;
D3DCOLORVALUE dcvSpecular;
D3DCOLORVALUE dcvAmbient;
D3DVECTOR dvPosition;
D3DVECTOR dvDirection;
D3DVALUE dvRange;
D3DVALUE dvFalloff;
D3DVALUE dvAttenuation0;
D3DVALUE dvAttenuation1;
D3DVALUE dvAttenuation2;
D3DVALUE dvTheta;
D3DVALUE dvPhi;
} D3DLIGHT7, *LPD3DLIGHT7;
Only the light position is set explicitly for the first light. The light position is described using a D3DVECTOR with the x-, y- and z-coordinates in world space. The first light is located under the objects and the second light is located above these. The second light is only set if the graphics device supports it. It's a bit darker.VOID D3DUtil_InitLight( D3DLIGHT7& light, D3DLIGHTTYPE ltType,
FLOAT x, FLOAT y, FLOAT z )
{
ZeroMemory( &light, sizeof(D3DLIGHT7) );
light.dltType= ltType;
light.dcvDiffuse.r = 1.0f;
light.dcvDiffuse.g = 1.0f;
light.dcvDiffuse.b = 1.0f;
light.dcvSpecular = light.dcvDiffuse;
light.dvPosition.x = light.dvDirection.x = x;
light.dvPosition.y = light.dvDirection.y = y;
light.dvPosition.z = light.dvDirection.z = z;
light.dvAttenuation0 = 1.0f;
light.dvRange = D3DLIGHT_RANGE_MAX;
}
[bquote]Directional lights don't use range and attentuation variables. A light's range property determines the distance, in world space, at which meshes in a scene no longer receive light. So the dvRange floating point value represents the light's maximum range. The attentuation variables controls how a light's intensity decreases toward the maximum distance, specified by the range property. There are three attentuation values, controlling a light's constant, linear and quadratic attentuation with floating point variables. Many applications set the dvAttentuation1 member to 1.0f and the others to 0.0f.[/bquote]Beneath the material and lights, the [font="Courier New"][color="#000080"]InitDeviceObjects()[/color][/font] method sets the projection matrix and aspect ratio of the viewport.
[size="3"]FrameMove()
The [font="Courier New"][color="#000080"]FrameMove()[/color][/font] 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 [font="Courier New"][color="#000080"]DirectInputCreateEx()[/color][/font]
- Create one or more input devices with [font="Courier New"][color="#000080"]CreateDeviceEx()[/color][/font]
- Set the data format of each device with [font="Courier New"][color="#000080"]SetDataFormat()[/color][/font]
- Set the cooperative level for each device with [font="Courier New"][color="#000080"]SetCooperativeLevel()[/color][/font]Getting Input:
- Acquire each input device with [font="Courier New"][color="#000080"]Acquire()[/color][/font]
- Receive Input with [font="Courier New"][color="#000080"]GetDeviceState()[/color][/font]
- Special Joysticks: call [font="Courier New"][color="#000080"]Poll()[/color][/font] 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 [font="Courier New"][color="#000080"]DirectInputCreateEx()[/color][/font] in the [font="Courier New"][color="#000080"]CreateDInput()[/color][/font] method.
It's called in [font="Courier New"][color="#000080"]WinMain()[/color][/font] withHRESULT 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;
}
To retrieve the instance of the sample, we use [font="Courier New"][color="#000080"]GetWindowLong( hWnd, GWL_HINSTANCE )[/color][/font]. The constant [font="Courier New"][color="#000080"]DIRECTINPUT_VERSION[/color][/font] 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.// Create the DInput object
if( FAILED(d3dApp.CreateDInput( d3dApp.Get_hWnd() ) ) )
return 0;
To create one input device - the keyboard - we use [font="Courier New"][color="#000080"]CreateDeviceEx()[/color][/font] in [font="Courier New"][color="#000080"]CreateInputDevice()[/color][/font]
HRESULT CMyD3DApplication::CreateInputDevice( HWND hWnd,
LPDIRECTINPUT7 pDI,
LPDIRECTINPUTDEVICE2 pDIdDevice,
GUID