Introduction
This article describes a Direct3D
demonstration program, and shows how to build it using the MinGW C++ compiler (a free, Windows-hosted implementation of the GNU C++ compiler). I hope that the material presented will be useful for Direct3D
programmers wishing to apply their knowledge using MinGW, as opposed to Visual Studio.
In addition, the article gives a quick introduction to Direct3D
, in particular to its more advanced "immediate" mode of operation. The demo presented renders a single solid - a cube - complete with texture and lighting. This cube is rotated about all three axes while being viewed by a stationary camera.
The article attempts to be as explicit and self-contained as possible. Even if you do not have MinGW and/or Direct3D
installed, the article contains instructions (e.g., download URLs) to get you started.
Using a non-Microsoft compiler to build a Direct3D
app presents some unique challenges. The article aims to provide as much relevant information as possible. For example, the precise build command used is shown, and explained in some detail.
Before going further, a few caveats are in order. First, although the article gives a rather complete description of the demonstration code, many Direct3D
topics are necessarily left unexplored. For example, the demo solid is rendered using triangle strips. This is only one of several ways to construct 3D solids, and the other methods are left unexplored.
In addition, the article completely omits one particularly important topic for production-quality Direct3D
development: the enumeration of device capabilities. Optimal, production quality Direct3D
applications typically behave differently on different graphics hardware. High-performance devices will have more GPU capabilities than low-performance devices, and a production-quality app should exploit each piece of hardware to the greatest extent possible.
The demo code presented here - in the interest of simplicity - takes a "one size fits all" approach, in which only the bare minimum of 3D capabilities are assumed, even on high-end computers. These simplifications were deemed acceptable for this article, which strives to introduce Direct3D
as briefly as possible. However, such a design would be out-of-place in more polished Direct3D
code.
Similarly, very little error handling is attempted here. Again, this was viewed as a necessary sacrifice in order to provide a concise introduction to some very broad and complex topics. Another compromise is the requirement that a texture file bitmap ("PlainTex.png") be present alongside the demo executable. A more full-featured app would be capable of emitting this file, as needed, at program startup.
Finally, the code presented here is unabashedly procedural in nature; no attempt at a more sophisticated design has been made. Again, this was considered acceptable for a brief demo, but does not necessarily translate to large production applications.
A picture of the demo in action is shown below:
Background
MinGW and Direct3D
are two well-respected and widely-used development tools. Until recently, though, I had not used them together. It seemed natural to build Direct3D
apps using Microsoft Visual Studio. After all, Direct3D is a Microsoft technology.
However, the use of MinGW offers several advantages. It is a cheaper and more lightweight development tool than Visual Studio, and can be quickly installed on most Windows systems. Recently, I had the opportunity to work through all the issues related to building a Direct3D
application using MinGW. This article presents what I learned.
The result is a self-contained guide to getting started with Direct3D, and this guide can be used without obtaining costly licenses.
Using the Code
To build the code, it is necessary to install the DirectX
SDK, along with MinGW. The DirectX
SDK includes all of the header and library files necessary to build DirectX applications. It also contains the runtime necessary to execute DirectX apps, although this is not installed by default. So, make sure to select this for installation if you do not have it installed on your computer already.
I used the August 2007 version of the DirectX SDK. This is available for free download here. There are newer versions of the SDK available, and these should also work. However, MinGW seems to target only version 9 (or earlier) of DirectX at the time of writing this article, and it is the version 9 DirectX types that are used by my example code. Another consideration is that version 10 of DirectX does not support Windows XP.
I obtained MinGW from this URL. I used the defaults for installation, except that I had to manually select the C++ compiler for installation.
The source code archive provided with this article consists of two files, named "MinGW3D.cpp" and "PlainTex.png". Once MinGW and the DirectX
SDK are installed, it should be possible to build MinGW3D.cpp into a demo executable by executing a single command in the Windows "Command Prompt". To do this, it is advisable to first use the "cd
" command to change the working directory to the folder where MinGW3D.cpp is located. After changing to the correct folder (i.e., wherever you extracted the source code archive for this article), the following command should be executed:
c:\mingw\bin\g++ mingw3d.CPP -fcheck-new -o mingw3d.exe -mwindows -I
"C:\Program Files\Microsoft DirectX SDK (August 2007)\Include" -I
c:\mingw\include -L c:\mingw\lib\ -ld3d9
"C:\Program Files\Microsoft DirectX SDK (August 2007)\Lib\x86\d3dx9.lib"
Note that, if the command text shown above is used to build an actual command using cut-and-paste, it is necessary to convert the carriage returns into spaces, i.e. the text shown above should be entered as a single, unbroken line. One approach is to paste the command from this page into Windows Notepad, edit it in Notepad to reside on a single line, and then paste the resultant text into a Command Prompt window. (The context menu of a Command Prompt window contains an "Edit" menu item which offers a "Paste" command.)
This build command is a key piece of information, and is explained clause-by-clause below.
First, the clause c:\mingw\bin\g++ is the path to the compiler itself. Note that I have used the default MinGW install path. Your path - in particular, the drive letter - may differ, if non-default options were selected during MinGW setup.
Next, the clause MinGW3d.CPP specifies the name of the file to be compiled.
The option -fcheck-new is necessary to squelch some warnings related to the implementation of the new
operator for some DirectX types. These warnings originate from files in the DirectX SDK, so it was deemed preferable to simply squelch the warning as opposed to editing Microsoft's files.
The clause -o mingw3d.exe gives the name of the executable to be generated by MinGW.
The option -mwindows instructs MinGW to link in the default Windows libraries necessary to access User32.dll, GDI32.dll, etc.
The options -I "C:\Program Files\Microsoft DirectX SDK (August 2007)\Include" and -I c:\mingw\include tell MinGW where to obtain header files. These are the DirectX SDK and MinGW include folders, respectively. Note that the SDK folder is listed first, and is thus searched first. The demo .CPP file includes only "windows.h" (which is obtained from the MinGW include path) and "d3dx9.h" (which comes from the SDK folder).
The path to your SDK headers may be different, especially if you use a later version of the SDK. The correct folder path can be obtained by searching the file system for one of the headers, e.g., "d3dx9.h".
The clause -L c:\mingw\lib\ tells MinGW where to look for .LIB files. The clause -ld3d9 tells MinGW to link in the Direct3D
9 library present in this folder.
Finally, the clause "C:\Program Files\Microsoft DirectX SDK (August 2007)\Lib\x86\d3dx9.lib" tells MinGW to link in the D3DX9.LIB library provided by the DirectX SDK. Interestingly, MinGW provides only a debug version of this library, named D3DX9D.LIB. This version is not as portable as the regular, non-debug version provided by Microsoft. I did not have the requisite DLL present on my system to use the MinGW debug version, for example, and ended up linking in the SDK version of the library instead, as shown in the build command provided here.
Once this build command is successfully executed, it will deposit the executable MinGW3D.exe into the same folder as MinGW3D.cpp. Assuming you have the DirectX 9+ runtime installed, and assuming "PlainTex.png" is present with the executable, then the executable should run correctly.
Points of Interest
A brief explanation of the Direct3D
code itself is given here. Much of this is centered around four globally accessible objects, whose declarations are shown below:
LPDIRECT3D9 d3d=NULL; LPDIRECT3DDEVICE9 device=NULL; LPDIRECT3DVERTEXBUFFER9 vertex_buffer=NULL; LPDIRECT3DTEXTURE9 texture_1=NULL;
The first two of these are necessary for essentially any Direct3D
application. They expose the basic foundation necessary for 3D, e.g., setting the display mode and turning options like lighting on or off.
The instance of type LPDIRECT3DVERTEXBUFFER9
holds the spatial definition of the demo's central cube, and the instance of LPDIRECT3DTEXTURE9
holds the texture that is applied to the surface of the cube. All of these instances are initialized to NULL
, which allows some garbage collection code (discussed later) to ascertain whether each object has been instantiated.
The use of global accessibility for these variables allows the function WindowProc()
, and all of the various functions it calls, to use the variables. The parameters of WindowProc()
are set by Microsoft, and these parameters do not include these Direct3D
pointers. Making the pointers global allows access from the necessary functions. In keeping with good design practice, this set of global variables has been kept as small as possible.
The application begins processing with the WinMain()
function. This begins by using CreateWindowEx()
to make a window of the appropriate size. Then, a function called init_device()
is called, which is shown below this paragraph. Its main role is to instantiate the necessary DIRECT3D9
and DIRECT3DDEVICE9
objects.
void init_device(HWND hWnd)
{
d3d = Direct3DCreate9(D3D_SDK_VERSION);
D3DPRESENT_PARAMETERS d3dpp;
ZeroMemory(&d3dpp, sizeof(d3dpp));
d3dpp.Windowed = TRUE;
d3dpp.SwapEffect = D3DSWAPEFFECT_DISCARD;
d3dpp.hDeviceWindow = hWnd;
d3dpp.BackBufferFormat = D3DFMT_X8R8G8B8;
d3dpp.BackBufferWidth = APP_WIDTH;
d3dpp.BackBufferHeight = APP_HEIGHT;
d3d->CreateDevice(
D3DADAPTER_DEFAULT,
D3DDEVTYPE_HAL,
hWnd,
D3DCREATE_SOFTWARE_VERTEXPROCESSING,
&d3dpp,
&device);
init_graphics();
init_lights();
device->SetRenderState(D3DRS_LIGHTING, TRUE); device->SetRenderState(D3DRS_ZENABLE, TRUE);
device->SetRenderState(D3DRS_CULLMODE,D3DCULL_CCW);
device->SetRenderState(D3DRS_AMBIENT,
D3DCOLOR_XRGB(AMBIENT_BRIGHTNESS,AMBIENT_BRIGHTNESS,AMBIENT_BRIGHTNESS));
D3DXMATRIX matView;
D3DXVECTOR3 camera(0.0f, 0.0f, -CAMERA_DISTANCE);
D3DXVECTOR3 lookat(0.0f, 0.0f, 0.0f);
D3DXVECTOR3 upvector(0.0f, 1.0f, 0.0f);
D3DXMatrixLookAtLH(&matView,
&camera, &lookat, &upvector);
device->SetTransform(D3DTS_VIEW, &matView);
D3DXMATRIX matProjection;
D3DXMatrixPerspectiveFovLH
(&matProjection,
D3DXToRadian(VERTICAL_VIEWFIELD_DEGREES),
(FLOAT)APP_WIDTH / (FLOAT)APP_HEIGHT,
FRUSTUM_NEAR_Z, FRUSTUM_FAR_Z);
device->SetTransform(D3DTS_PROJECTION, &matProjection);
device->SetFVF(CUSTOMFVF);
}
This code is designed to initialize Direct3D
using a lowest-common-denominator approach. That is, the settings selected were chosen to work reasonably well on a wide variety of graphics hardware. The use of D3DSWAPEFFECT_DISCARD
, for example, grants leeway to the graphics card to manage back buffers (the unseen scratchpad in memory where the scene is drawn before being shown) as it sees fit. The backbuffer format D3DFMT_X8R8G8B8
is the most widely used format. It specifies 24-bit RGB color, with 8 bits of padding. Use of D3DDEVTYPE_HAL
forces hardware rasterization, which seems to be widely supported as of this writing, even on budget hardware.
Setting D3DCREATE_SOFTWARE_VERTEXPROCESSING
ensures that devices without hardware vertex processing can still run the demo. This seems to still be necessary for certain graphics hardware.
The calls to init_graphics()
and init_lights()
, which are defined elsewhere in MinGW3D.cpp, perform some other setup chores which are described later below.
The calls to SetRenderState()
ensure that lighting and Z-buffering are turned on, along with clockwise surface culling (discussed later). Z-buffering is a rendering strategy which retains data about the position of pixels in the Z-dimension even after the rules of perspective have been used to create a 2D version of the scene. This allows the 3D engine to avoid wasting time presenting objects obscured by other objects that are nearer in the Z-dimension. These settings seem to be acceptable for all modern video hardware.
Next, some ambient light is requested to make sure that the demo cube is actually visible. This is white light, i.e., it has equal red, green, and blue components. These are each set to the constant value AMBIENT_BRIGHTNESS
, which is defined at the top of MinGW3D.cpp to equal 140, out of a possible 255. Leaving the ambient light somewhat dim allows for a more dramatic spotlight effect (described below).
The constants APP_WIDTH
and APP_HEIGHT
are also used in this function. These are repeated in various places in the demo code. In the code as provided, these equal 800 and 600, respectively. This resolution was selected because it provides a demo of acceptable size on most computers. Note, though, that these constants (which are defined near the top of MinGW3D.cpp) are freely adjustable within the limits of the device.
At this point, a "view transformation" matrix must be created and supplied to the Direct3D
device. This defines the position of the camera (i.e., the point in the Direct3D
coordinate system from which the user sees the demo), as well as the direction in which it is facing, and the "up" direction that defines the orientation of the top of the scene as presented to the user. In reading this code, keep in mind that Direct3D
uses a "left-handed" coordinate system in which X coordinates range positively from the left of the screen to the right; Y coordinates extend positively upward from 0 at the screen bottom; and positive movement in the Z dimension goes forward from the computer user through the back of the screen.
The function D3DXMatrixLookAtLH()
is a Direct3D
utility that makes the necessary view transformation matrix. It takes as parameters a vector pointing to the camera position, a vector pointing at the coordinate where the camera is pointed, and a vector extending upward from the top of the camera. For this demo, the first vector places the camera 19.5 units (defined as constant CAMERA_DISTANCE
) in front of the screen. To give this number some context, consider that the solid rendered by the demo will have dimensions 10 x 10 x 10.
The second vector points the camera towards the origin at position (0,0,0), which will also be the center of the cube. The final coordinate establishes that the camera is right side up, i.e., that an imaginary vector extending from the top of the camera would point straight up in the Y-dimension. Once D3DXMatrixLookAtLH()
is called to make a view matrix based on these values, this matrix is passed as a parameter in the call device->SetTransform(D3DTS_VIEW, &matView);
.
Now, a "projection matrix" must be passed to the Direct3D
device. As was the case for the view transformation matrix, this is done by first using a Direct3D
utility function to create the matrix, and then passing the resultant matrix in a call to SetTransform()
. In this case, the utility function is called D3DXMatrixPerspectiveFovLH()
.
The shape of the projection matrix - and its relation to the camera just defined - can be understood if one imagines the camera as a movie projector, projecting a cone-like beam of light out onto the theater's screen. Of course, the Direct3D
camera does not project literal light, at least not by default. But the volume of the 3D universe that the Direct3D
camera can see is indeed a cone-like solid extending (and widening) from its front end. In Direct3D
, this shape is simulated using a sort four-sided pyramid.
The point at the top of the pyramid resides at the camera location, although the visible portion of the 3D world actually begins a constant distance from the camera. This results in a visible volume having the shape of a "frustum," i.e. a pyramid with its top shaved off. The frustum sits directly in front of the camera, rotated such that its sides are parallel to the camera "up" vector.
The utility function D3DXMatrixPerspectiveFovLH
serves to establish this frustum. This function's first parameter is the address of the matrix to be returned. This is followed by the vertical viewfield angle, which is set to the constant VERTICAL_VIEWFIELD_DEGREES
, defined as 45 degrees. This is the angle between those two side faces of the frustum that are perpendicular to the camera's "up" vector.
The next parameter is the aspect ratio of the top and base, which is set to APP_WIDTH
divided by APP_HEIGHT
. This gives the viewable volume the same aspect ratio as the window which contains it, which helps keep the display presented to the user filled. Finally, the near and far extremes of the frustum in the Z-dimension are passed. The values used for this purpose are FRUSTUM_NEAR_Z
and FRUSTUM_FAR_Z
, defined as 1.0 and 100.0, respectively.
All of these values were selected to keep most of the demo cube visible at all times, without rendering it too small. For example, consider the dimensions of the frustum at the coordinate system origin (0,0,0). Because the vertical viewfield angle is 45 degrees, the height of the field-of-view at any distance from the camera equals said distance from the camera. In trigonometric terms, these two lengths constitute the opposite and adjacent legs of a 45-45-90 triangle, which are equal, since tangent(45) = 1. This means that, at Z coordinate 0, a plane 18.5 units high visible. At a 4:3 aspect ratio (e.g. 1024 x 768), this results in a plane nearly 25 units wide. This is more than enough to accommodate the 10 x 10 x 10 demo cube, without being orders-of-magnitude larger.
Finally, the call to SetFVF()
finishes up init_device()
. This sets up our vertex format. Direct3D
is a very open-ended technology; it is designed to adapt to advances in display hardware. As such, Direct3D
does not specify a format for storing vertex data. This is instead left to the developer. For this demo, the following code (present near the top of the file) defines our vertex format:
struct MYVERTEXTYPE {FLOAT X, Y, Z; D3DVECTOR NORMAL; FLOAT U, V;};
#define CUSTOMFVF (D3DFVF_XYZ | D3DFVF_NORMAL | D3DFVF_TEX1)
In summary, single-precision X
, Y
, and Z
members define the location of the vertex in the Direct3D
coordinate system. The NORMAL
member defines a vector that is perpendicular to the solid at the vector. This enables Direct3D to light and shade the solid correctly. Finally, the U
and V
members are used for mapping textures onto the solid. Textures are square, two-dimensional images, and the coordinate (U, V) defines the point in the texture where it should touch that vertex. U
and V
both range from 0.0 (top/left corner of the texture) to 1.0 (bottom/right corner of the texture).
In addition to defining the vertex format, we must also define a constant - named CUSTOMFVF
- which is passed to Direct3D
to give it information about the content of the vertex format. Recall that CUSTOMFVF
was passed to Direct3D
in a call to SetFVF()
at the bottom of init_graphics()
.
In this case, we are providing 3D position, surface normal vector, and texture mapping information, and the three constants that make up CUSTOMFVF
communicate these facts to Direct3D
. D3DFVF_XYZ
indicates that 3D position is part of the vertex format. D3DFVF_NORMAL
indicates that surface normal is part of the vertex format as well. Lastly, D3DFVF_TEX1
indicates the presence of the texture mapping coordinates U
and V
with each vertex's data.
Now, the operation of the init_graphics()
function is discussed. Recall that this gets called at startup, after init_device()
. The major role of init_graphics()
is to define the cube rendered by the demo. The cube has dimensions of 10 x 10 x 10 in the Direct3D
coordinate system, which is a real-number-based system independent of any device coordinate system.
A cube has 8 vertices. On a six-sided die, for example, there are 8 points. However, simply listing these 8 vertices does not unambiguously identify a cube. Consider, for example, the figure shown below, in which a cube containing an X-shaped solid (outlined in the thicker lines) is shown. Note that both the cube and the other solid share the same 8 vertices.
As a result, it is typical to define 3D solids in terms of triangular facets. This turns out to be an unambiguous - and optimal - way to represent solid solids. Under such a system, a flat, rectangular surface like one side of a six-sided die will consist of two triangular surfaces that form a plane. Direct3D
, like most 3D engines, allows these triangular facets to be defined in "strips". A triangle strip is a series of triangles sharing common edges. The first three coordinates in each strip form a triangle, but then the fourth point defines another triangle, which shares the second and third coordinates from the first triangle. Any subsequent points after the fourth also each define another complete - but connected - triangle, in similar fashion. Each of these subsequent triangles includes the last two points from its predecessor.
For the demo cube, each rectangular face is a triangle strip, consisting of four coordinates forming two triangles. The figure below this paragraph gives an overview of how the front face of the cube is rendered using a two-triangle strip consisting of four vertex coordinates, labeled 0, 1, 2, and 3.
One final aspect of triangle strips bears mention. Note that, in the figure above, the first triangle is described by listing its vertices in clockwise order. Direct3D
relies on this ordering to determine which faces of the cube actually need to be drawn. Consider what will happen to this ordering if the cube is rotated 180 degrees about the Y-axis, i.e., rotated such that this "front" facet is actually facing backwards. In this case, the ordering of the triangle coordinates will be reversed; the first triangle will be listed such that its coordinates form a counter-clockwise ordering from the perspective of the camera. The 3D engine detects this out-of-order condition, and realizes that the face is obscured and does not need to be rendered.
In order to get the benefit of this optimization, it is necessary to set the rendering system to use "counter-clockwise surface culling". This was done in the init_device()
function, with the statement device->SetRenderState(D3DRS_CULLMODE,D3DCULL_CCW);
. Once a Direct3D
application executes such a call, it must thereafter take care that triangle strips are defined beginning with a clockwise pattern.
The initializer for the data structure that defines the demo cube comprises that major portion of function init_graphics()
. This data structure is named demo_vertices
and is subsequently copied into Direct3D
object vertex_buffer
. Function init_graphics()
is shown below:
void init_graphics()
{
D3DXCreateTextureFromFileEx(
device,
TEXTURE_FILE_NAME,
TEXTURE_SIZE,TEXTURE_SIZE,
D3DX_DEFAULT,0,D3DFMT_X8R8G8B8,D3DPOOL_MANAGED,
D3DX_DEFAULT,D3DX_DEFAULT,0, NULL, NULL, &texture_1
);
MYVERTEXTYPE demo_vertices[] =
{
{ -5.0f, 5.0f, -5.0f, 0, 0, -1, 0, 0 }, { 5.0f, 5.0f, -5.0f, 0, 0, -1, 1, 0 },
{ -5.0f, -5.0f, -5.0f, 0, 0, -1, 0, 1 },
{ 5.0f, -5.0f, -5.0f, 0, 0, -1, 1, 1 },
{ 5.0f, 5.0f, 5.0f, 0, 0, 1, 0, 0 }, { -5.0f, 5.0f, 5.0f, 0, 0, 1, 1, 0 },
{ 5.0f, -5.0f, 5.0f, 0, 0, 1, 0, 1 },
{ -5.0f, -5.0f, 5.0f, 0, 0, 1, 1, 1 },
{ -5.0f, 5.0f, 5.0f, 0, 1, 0, 0, 0 }, { 5.0f, 5.0f, 5.0f, 0, 1, 0, 1, 0 },
{ -5.0f, 5.0f, -5.0f, 0, 1, 0, 0, 1 },
{ 5.0f, 5.0f, -5.0f, 0, 1, 0, 1, 1 },
{ 5.0f, 5.0f, -5.0f, 1, 0, 0, 0, 0 }, { 5.0f, 5.0f, 5.0f, 1, 0, 0, 1, 0 },
{ 5.0f, -5.0f, -5.0f, 1, 0, 0, 0, 1 },
{ 5.0f, -5.0f, 5.0f, 1, 0, 0, 1, 1 },
{ -5.0f, -5.0f, -5.0f, 0, -1, 0, 0, 0 }, { 5.0f, -5.0f, -5.0f, 0, -1, 0, 1, 0 },
{ -5.0f, -5.0f, 5.0f, 0, -1, 0, 0, 1 },
{ 5.0f, -5.0f, 5.0f, 0, -1, 0, 1, 1 },
{ -5.0f, 5.0f, 5.0f, -1, 0, 0, 0, 0 }, { -5.0f, 5.0f, -5.0f, -1, 0, 0, 1, 0 },
{ -5.0f, -5.0f, 5.0f, -1, 0, 0, 0, 1 },
{ -5.0f, -5.0f, -5.0f, -1, 0, 0, 1, 1 },
};
device->CreateVertexBuffer(
CUBE_VERTICES*sizeof(MYVERTEXTYPE), 0,
CUSTOMFVF,
D3DPOOL_MANAGED,
&vertex_buffer,
NULL);
VOID* pVoid;
vertex_buffer->Lock(0, 0, (void**)&pVoid, 0);
memcpy(pVoid, demo_vertices, sizeof(demo_vertices));
vertex_buffer->Unlock();
}
Each of the bracketed lists in the declaration of the cube initializes data for a single cube vertex. Each group of four initializers defines a face of the cube, i.e. a strip of two triangles. The first three fields of each initializer give the position of the vertex in 3D space. The first initializer, { -5.0f, 5.0f, -5.0f, 0, 0, -1, 0, 0 },
sets up a point at the top left side of the cube, at its front. The X coordinate -5.0 is at the left extreme of the cube. The Y coordinate 5.0 is at its top; and Z coordinate -5.0 is at the front of the cube.
This vertex belongs to more than one face of the cube; it is a part (corner) of the left, top, and front faces. As indicated by the comments in the code, though, in this first case the vertex is being used as a part of the definition of the front face.
The image below this paragraph shows the demo cube, with an arrow extending forward from this first vertex. This arrow represents the "normal vector" of the front surface, i.e. the direction in which the surface reflects light. This top, left, front vertex will be associated with a different normal vector for each of the face definitions in which it plays a role. It will be used in conjunction with a left-facing normal vector in the definition of the left cube face, for example.
In the vertex format used here, each initializer lists the normal vector using X, Y, and Z magnitudes which appear immediately after the three vertex coordinates. For the first four vertices, these vectors are all <0,0,-1>, i.e. straight forward in the Z dimension, toward the user. This is appropriate for the front face, which will indeed reflect any light straight toward the user.
The second initializer, { 5.0f, 5.0f, -5.0f, 0, 0, -1, 1, 0 },
resides at a similar location at the top right front corner of the cube. This is followed by two initializers that set up the bottom front left and right corners of the cube, respectively.
All of these share a common Z-coordinate of -5.0. This is so because these first four vertices define the cube's front, which resides at Z coordinate -5.0.
Finally, note the texture coordinates present at the end of each of the four initializers. The texture coordinates use only two dimensions, and the three-dimensional vertex coordinates must be mapped into these two dimensions by the programmer. This must be done in a way that accurately describes how the two-dimensional texture appearance is applied to the three-dimensional solid. For this cube front, this is easy; the face is a plane in the X/Y coordinate system, as is the texture. Its left thus corresponds to the left of the texture, its right to the texture's right, etc.
This mapping process is more complicated for faces that are planes in other coordinate systems. For example, the top and bottom of the cube are planes in the X and Z dimensions. For these faces of the cube, the Y dimension is constant (-5.0 for the bottom and 5.0 for the top). The Z dimension of the face is mapped to the Y dimension of the texture. Texture top/bottom maps to face front/back.
One useful technique for the polygon designer is to rotate the solid until a side is front-facing. Then, the vertices that appear near (for example) the left of the display can be mapped to left-side texture coordinates for that side.
Note that this rotate-to-front technique applies to faces skewed into odd planes, e.g. those that do not line up with any two dimensions. Whatever mental gymnastics are used to develop the texture mapping model, as long as the mapping system is consistent, the texture will get mapped properly. In this case, for example, the diagonal transitions between the second and third vertices must be matched by diagonal transitions between the second and third texture coordinates. If this basic requirement is fulfilled, plausible texture mapping occurs.
To present a realistic model of the solid to the user requires it to be lighted as well. Lighting gives 3D solids a deeper, shaded appearance. The code for function init_lights()
is shown below this paragraph. This sets up a single spotlight, which augments the ambient lighting already set up.
void init_lights()
{
D3DMATERIAL9 material;
D3DVECTOR look = {0.00f, 0.0f, 1.0f}; D3DVECTOR at = {0.0f, 0.0f, -LIGHT_DISTANCE};
D3DLIGHT9 flashlight;
ZeroMemory(&flashlight, sizeof(D3DLIGHT9));
flashlight.Type = D3DLIGHT_SPOT;
flashlight.Diffuse.r = flashlight.Ambient.r = 1.0f; flashlight.Diffuse.g = flashlight.Ambient.g = 1.0f;
flashlight.Diffuse.b = flashlight.Ambient.b = 1.0f;
flashlight.Diffuse.a = flashlight.Ambient.a = 1.0f;
flashlight.Range = LIGHT_RANGE;
flashlight.Position = at;
flashlight.Direction = look;
flashlight.Phi = LIGHT_CONE_RADIANS_OUTER;
flashlight.Theta = LIGHT_CONE_RADIANS_INNER;
flashlight.Attenuation0 = 0.0f; flashlight.Attenuation1 = 0.01f; flashlight.Attenuation2 = 0.0f; flashlight.Falloff = 1.0f; device->SetLight(0, &flashlight);
device->LightEnable(0, TRUE);
ZeroMemory(&material, sizeof(D3DMATERIAL9));
material.Diffuse.r = material.Ambient.r = 1.0f;
material.Diffuse.g = material.Ambient.g = 1.0f;
material.Diffuse.b = material.Ambient.b = 1.0f;
material.Diffuse.a = material.Ambient.a = 1.0f;
device->SetMaterial(&material);
}
This code begins by defining a direction and location for the spotlight. The light sits in an imaginary location 40.0 units (this is the constant LIGHT_DISTANCE
) in front of the display, and points back at the display.
Next, this code defines two types of color emitted by the light: diffuse and ambient. The difference between these is unimportant, since both colors are pure white (as indicated by setting the red, green, and blue components to the maximum value of 1.0). The light's Range
is set to 60.0 (the constant LIGHT_RANGE
); since the light is 40.0 units away from the center of the cube, this serves to keep the cube within the range of the light.
The Phi
and Theta
parameters define the angle at which the spotlight spreads out from its source. These angles can differ from eachother, allowing for an inner code of brighter illumination. Here, Phi
and Theta
differ only slightly. These are both assigned constants which equal approximately 20 degrees. (These angles are expressed in radians; 2 times Pi (~6.28) radians is equal to 360 degrees.) The Falloff
member of D3DLIGHT9
defines the relative brightness of the outer and inner cone. Here, this is set to the Microsoft-recommended value of 1.0.
This ~20-degree angle allows the light to partially illuminate the cube, which creates a more interesting effect. The exact value of this angle was selected via observation, although in some cases it will be helpful to do trigonometric similar to that already described for the vertical viewfield angle.
Finally, the "attenuation" values for the light are set. The light attenuates (decreases with distance) in three ways: there is a constant attenuation value that is always applied, along with an attenuation value that is multiplied by the distance from the light, and a third value that is multiplied by the distance squared. For our purposes here, only a minimal, linear attenuation is specified. This is mandatory; the documentation states that it is not allowable to specify 0.0 for all three values. Nevertheless, a very low attenuation is appropriate for a spotlight. Higher attenuations create other, interesting effects, like that of a candle.
Once the light is completely set up, it is set to be "light 0" of our D3DDEVICE9
instance, and then "light 0" is enabled.
Finally, a "material" must be supplied to the Direct3D
device. This is a global setting, in that it applies to all faces of all solids in the application. The material specifies how 3D solids, in reflecting light, modify the color of the light. In this demo, the material colors are all set to white (the red, green, blue components all equal 1.0, the maximum value). In conjunction with the white color assigned to the spotlight and the ambient light declared earlier, this results in white light illumination throughout the demo.
At this point, all of the setup functions in MinGW3D.cpp have been explained. Once these have done their work, WinMain()
enters an endless "game loop", which alternately processes any queued Windows messages and then calls a function called render()
. This function, which is shown below this paragraph, does the actual work of rendering each frame.
Recall that the demo uses Direct3D
"immediate mode", in which the content of each frame must be rendered in its entirety. Direct3D
does offer an easier, but less-powerful mode of operation, known as "retained mode". In retained mode, certain information - e.g., the position of 3D solids - persists automatically from one frame to the next. But this is not the case in "retained" mode, which necessitates these repeated calls to render()
, shown below:
void render()
{
static float index = 0.0;
device->Clear(0, NULL, D3DCLEAR_TARGET, D3DCOLOR_XRGB(0, 0, 0), 1.0f, 0);
device->Clear(0, NULL, D3DCLEAR_ZBUFFER, D3DCOLOR_XRGB(0, 0, 0), 1.0f, 0);
device->BeginScene();
static float speed = CUBE_START_SPEED;
speed *= CUBE_ACCEL;
if(speed>CUBE_MAX_SPEED) speed=CUBE_MAX_SPEED;
index+=speed;
D3DXMATRIX matRotateX;
D3DXMatrixRotationYawPitchRoll(&matRotateX,index,index,index);
device->SetTransform(D3DTS_WORLDMATRIX(0), &(matRotateX)); device->SetStreamSource(0, vertex_buffer, 0, sizeof(MYVERTEXTYPE));
device->SetTexture(0, texture_1);
device->DrawPrimitive(D3DPT_TRIANGLESTRIP, 0, 2); device->DrawPrimitive(D3DPT_TRIANGLESTRIP, 4, 2);
device->DrawPrimitive(D3DPT_TRIANGLESTRIP, 8, 2);
device->DrawPrimitive(D3DPT_TRIANGLESTRIP, 12, 2);
device->DrawPrimitive(D3DPT_TRIANGLESTRIP, 16, 2);
device->DrawPrimitive(D3DPT_TRIANGLESTRIP, 20, 2);
device->EndScene();
device->Present(NULL, NULL, NULL, NULL) ;
}
The function begins by clearing the rendering target buffer and the Z-buffer. Then, the BeginScene()
method of the device object is called. This initializes the rendering of a new frame.
At this point, some calculations related to the specific demonstration shown here are made. In order to create some movement, the cube is rotated by an ever-changing angle about all three axes. The same angle is used for all three axes, and this quantity is tracked by the static
variable index
. The initial value, maximum value, and frame-by-frame change of this variable are set by the constants CUBE_START_SPEED
, CUBE_MAX_SPEED
, and CUBE_ACCEL
, respectively. The start speed is 0.02, which represents the rotational velocity of the cube in radians-per-frame. The maximum speed is 0.1 radians/frame. This equates to ~6 degrees/frame, which appears quite fast. CUBE_ACCEL
is set to 1.0025, meaning that the rotational velocity increases by 0.25% each frame, until the maximum velocity is reached.
Once this arithmetic is done, D3DXMatrixRotationYawPitchRoll()
is used to create a matrix to rotate the cube by index
in all three dimensions. This matrix is then passed to the 3D engine as the "world" transformation matrix (more precisely, as world transformation matrix 0, since multiple transformations are supported by Direct3D
). The "world" transformation, quite reasonably, serves to place the next 3D shape in the context of the overall 3D world. This allows the developer to define solids about the origin, as was done for our demo cube here, even if they will be rendered in far-flung locations at run time.
At this point, the actual drawing of the cube is performed. This is done using one call to IDirect3DDevice9::DrawPrimitive()
per face. First, some preliminary setup must be done. In particular, the "stream source" (i.e., the source of solids to be rendered) must be set to the vertex_buffer
array using a call to IDirect3DDevice9::SetStreamSource()
. The texture to be used for rendering must also be set, using a call to IDirect3DDevice9::SetTexture()
.
With these things done, the actual calls to DrawPrimitive()
are made. Recall that, in our array of vertices vertex_buffer
, each group of four elements forms a single triangle strip, which corresponds to a cube face. DrawPrimitive()
takes three parameters: first, we pass the constant D3DPT_TRIANGLESTRIP
; then we pass an index into vertex_buffer
, which will be a multiple of four; finally, we pass the number of triangles in the strip (2).
Now, it is necessary only to execute a couple of calls to signal to the 3D engine that we are done: IDirect3DDevice9::EndScene()
must be called, followed by IDirect3DDevice9::Present()
. This takes four parameters, but since they pertain to features that are not used here, all four parameters are set to NULL
.
At this point, most of the 3D-related code in the demo has been discussed. The major unmentioned portions of the code relate to support for switching between the demo and other applications, e.g., using ALT+TAB. When the user does this, a variety of video-related memory allocations seem to get undone by Windows.
This issue is handled by calling a function restore_surfaces()
in response to the WM_SETFOCUS
message, i.e., whenever the user returns from another application. This, in turn, calls the function cleanup()
, which releases all the COM objects used by the app (the vertex buffer, the texture, and the instances of LPDIRECT3DDEVICE9
and LPDIRECT3D9
), and then calls the init_device()
function of the demo. These actions serve to re-establish the demo's control of the graphics hardware. Note that the demo does not, from the user's perspective, restart. The state of the demo is maintained in the static
variable index
.
Some special steps are also taken to deal with screensaver activation. When the screensaver is activated, this results in the WindowProc()
function receiving the message WM_SYSCOMMAND
with wParam
equal to SC_SCREENSAVE
. The demo simply exits in response to this message. This allows the screensaver to gain access to the graphics hardware.
Another point worth mentioning is that cleanup()
uses a special macro to release the COM objects:
#define RELEASE_COM_OBJECT(i) if(i!=NULL&& i) i->Release();
Because cleanup()
can be called at basically any point in the execution of the demo, these checks-for-null are necessary. Otherwise, Release
could be invoked using a bad pointer, with catastrophic results. This is why the global DirectX objects discussed near the top of the article were all initialized to null
.
In summary, my hope is that this article offers a way to start 3D development as quickly and cheaply as possible. There are many lengthier Direct3D
tutorials available on this site and elsewhere. An understanding of the material presented here should only help the novice's ability to parse those articles. For the experienced 3D developer, I think this article demonstrates the practicality of MinGW for Direct3D
development.
History
This is the third major version of this article.
The second version added a more formal treatment of polygon design and definition, and of the Direct3D
frustum. Also, the article as a whole was made somewhat more detailed and, it is hoped, clearer.
The third version of the article adds improved lighting, and provides a link to the demo executable.