Using Direct3D8: The basics



Click here for larger image

Environment: VC6 SP5 Windows2000 SP2 DirectX8.0a SDK

Introduction (or, #include <windows.h>)

Computer graphics has always been one of the most interesting things in computer programming. In the beginning no one can even imagine a game like the real world. But time was going by, computers' power was growing and now it is really hard to surprise anyone with 3D graphics.

There are two 3D libraries in the Microsoft Windows world: OpenGL and Direct3D(a part of DirectX). They are different, but at the same time they are like each other very much. OpenGL is more consistent, Direct3D is changing permanently: the last release of DirectX was version 8 and I doubt it will be the last.

This article introduces the use of Direct3D 8 for buiding powerful 3D application. As an example task I choose one of the most 3D task: plotting a 3D surface. The sample application and its source code can be freely downloaded from this site and can be freely used in your applications.

  • DirectX 8.0a SDK can be downloaded here.
  • English version of DirectX 8.0a Runtime for Windows 95, Windows 98, Windows 98 SE, Windows ME can be downloaded here.
  • English version of DirectX 8.0a Runtime for Windows 2000 can be downloaded here.
  • For downloading localized version of DirectX 8.0a Runtime visit this page.

About Sample Application

Every author of an article has to face with a difficult question: which framework should be used to demonstrate an idea. There are four possible choices for creating simple Windows application:

  • Pure-API application - Simple, small, easy portable(theoretically).
  • MFC application - Most common choice.
  • WTL application - Great, but not everybody has WTL installed.
  • ATL application - Small code, but Wizard doesn't support non-COM applications.

Every framework has its advantages and drawbacks. For this article I chose the ATL framework because I wanted to make my code as clear as possible, without any overhead. To me, ATL is clearer than MFC. If you hate ATL you can stop reading this article now!

On the following picture you can see class diagram for the sample application (Booch notation).



Click here for larger image

Here is the brief description of the most important classes.

  • CMainDlg - main application class. Inherited from CDialogImpl and is created in WinMain as non-modal dialog. It owns one C3DGraphic object, one C3DGraphFrame object, four non-modal properties windows(CMaterialPropsWindow, CLightPropsWindow, CBackColorWindow and CFunctionTypeWindow), and three 3D functions objects(CSplashFunction, CPlaneFunction and CParabaloidFunction).
  • CPropertyWindow - root class for all properties windows. Inherited from CDialogImpl.
  • C3DFunction - abstract class. Exposes functions that are necessary for getting information about particular 3D function.
  • CPropertyWindowNotify - abstract class-interface for getting notifications from properties windows.
  • CD3D8Application - wrapper class for managing lifetime of IDirect3D8 object.
  • C3DGraphFrame - window where 2D image of 3D function will be displayed, i.e. view class for 3D document.
  • C3DGraphic - the most complex class, it renders given 3D function into C3DGraphFrame window, sets light, materials and do other interesting stuff.

From general point of view the sample application is ATL EXE server without COM support. All COM-related stuff was removed, so it is just a Windows app. I could tell you how to do it from the scratch but that is a topic for another article.

What's the Heck is Direct3D 8, (or Direct3D8=2*(Direct3D7+DirectDraw7))

There is no more DirectDraw--only Direct3D!!! This is the greatest change DirectX programmers have ever seen. Plenty of changes were brought into Direct3D 8/ Many more than were brought into Direct3D 7. There were 48 methods of IDirect3DDevice7 versus 94 methods of IDirect3DDevice8.

Back-buffering is now supported automatically without those nasty flip chains. Initialization of Direct3D has become as simple as i++ and many of low-level details are now inaccesible for programmers. The latter is not always good - for example, with Direct3D 8 you are not allowed to write something on primary surface directly. Moreover, it is not recommended that you read anything directly from a primary surface.

Many programmers will still use DirectDraw 7 for some specific tasks. But what about those people who never dealt neither with Direct3D 7 nor Direct3D? For these people I will try a give a brief explanation of Direct3D 8 and what it is:

  • Direct3D 8 provides hardware-independent way for using 3D capabilities of videocards.
  • Standard 3D transformation pipeline is supported: world matrix, view matrix, projection matrix.
  • Support for rasterizing geometric primitives: points, lines, triangles. high-order primitives are also supported, but only in hardware way - no software emulation.
  • Powerful lighting subsystem: materials and lights.
  • 3D texturing, so you can do amazing things - put a photo of your ex-girlfriend on 3D shape(for example, lavatory pan).
  • For details look DirectX 8.0a SDK

Initialization, (or How to Start)

For the creation of a Direct3D 8 application you must inlude all neccessary header files into your program and link your program with all neccessary libaries. There are two useful header files and two neccessary libraries:

  • d3d8.h     - Header file with core Direct3D8 interfaces declarations.
  • d3d8.lib   - Library file for linking your program with Direct3D8 DLL.
  • d3dx8.h    - Header with some very useful tool functions and interfaces.
  • d3dx8.lib  - Library for d3dx8.h.

In sample project's directory you can find two files: D3D8Include.h and D3DX8Include.h. Just include them into your project for including necessary headers and linking your program with respective libraries.

Now you have all the stuff that you need. What's next???

You now need to create IDirect3D 8 object. This objects provides 3D devices creation, checking devices capabilities, enumerating and retrieving display adapters modes and other useful operations. My sample project uses a wrapper class CD3D8Application for creating an IDirect3D8 object. To using this wrapper you just inherit your application's class from CD3D8Application. In an initialization function (in OnInitDialog, for instance) call function CD3D8Application::Direct3DInitOK to check whether IDirect3D8 was created succefully or not. IDirect3D8 object is created with Direct3DCreate8() method. It is a pretty good function because it takes only one parameterthat must always be D3D_SDK_VERSION. It is enough to do the following:

 pDirect3DObject = Direct3DCreate8(D3D_SDK_VERSION);
 if (!pDirect3DObject) {
    // Do something!!! Error occured!!!
 }

After IDirect3D8 object was created you must somehow get something on which you can draw 3D graphics. This something is called IDirect3DDevice8. You can access IDirect3DDevice8 by using IDirect3D8's method CreateDevice(). The sample project calls this method in C3DGraphic::Create so:

D3DDISPLAYMODE theDisplayMode;
hr = m_p3DApplication->m_pDirect3DObject->GetAdapterDisplayMode(
   D3DADAPTER_DEFAULT, &theDisplayMode);
   if (FAILED(hr)) {
   return hr;
}

D3DPRESENT_PARAMETERS thePresentParams; 
ZeroMemory(&thePresentParams, sizeof(thePresentParams));
thePresentParams.Windowed   = TRUE;
thePresentParams.SwapEffect = D3DSWAPEFFECT_DISCARD;
thePresentParams.BackBufferFormat = theDisplayMode.Format;
thePresentParams.EnableAutoDepthStencil = TRUE;
thePresentParams.AutoDepthStencilFormat = D3DFMT_D16;

hr = m_p3DApplication->m_pDirect3DObject->CreateDevice(
   D3DADAPTER_DEFAULT, D3DDEVTYPE_HAL,
   m_hwndRenderTarget,
   D3DCREATE_SOFTWARE_VERTEXPROCESSING,
   &thePresentParams,
   &m_p3DDevice);

if (FAILED(hr)) {
   return hr;
}

So we created 3D device with the following parameters:

  • 3D device is created for default video adapter(D3DADAPTER_DEFAULT).
  • It has one back buffer, the color format of the back buffer is the same as current display mode.
  • Device is not fullscreen but windowed. All rendering will be performed on m_hwndRenderTarget window (this is handle to our C3DGraphFrame window, if you still remember what it is).
  • Device has automatically created depth buffer. The format of depth buffer is 16 bit per point. Nowadays almost all video adapters support this type of depth buffer. Depth buffer(a.k.a. Z-buffer) is used internally by Direct3D for determining which pixels are visible and which are not.
  • Device should use hardware capabilities for rendering. If a particular capability is not supported by hardware, Direct3D will try to emulate it on a software level. If you change desired device type from D3DDEVTYPE_HAL to D3DDEVTYPE_REF, all operations will be performed on software emulation level. Note that such emulation is VERY slow and, moreover, not any hardware capability can be emulated.
  • Vertexes should be processed on a software level. We choose it with parameter D3DCREATE_SOFTWARE_VERTEXPROCESSING. It also can be D3DCREATE_MIXED_VERTEXPROCESSING or D3DCREATE_HARDWARE_VERTEXPROCESSING, but these modes are supported not by every adapter.

From f(x, y)=x+y+z to 2D picture

Life would be very easy if IDirect3DDevice8 had some function named PleaseDrawPretty3DGraphicOnTheScreen. Alas, we have no such a function. IDirect3DDevice8 can draw some primitives and to draw them you must put their coordinates into vertex buffer. Discussion of vertex buffers is beyond scope of this article so I only can say that vertex buffer is an abstraction of memory block. You can lock it(like a DirectX surface) and write something there. Usually before any rendering we want to clear back buffer surface for drawing new frame. It can be done with IDirect3DDevice8::Clear() method. In sample project you can see it in C3DGraphic::ReRender function:

hr = m_p3DDevice->Clear(0, NULL, 
                        D3DCLEAR_TARGET | D3DCLEAR_ZBUFFER, 
                        m_dwBackColor, 1.0f, 0);
if (FAILED(hr)) {
   return hr;
}

Here we clear entire back buffer by filling it with m_dwBackColor color. Also we clear depth buffer and fill it with 1.0 value. 1.0 is the most far depth value, 0.0 is the nearest. Drawing on 3D device always starts with

m_p3DDevice->BeginScene();

call, and ends with

m_p3DDevice->EndScene();

call. All calls for updating contents of 3D device must be between BeginScene() and EndScene(). Note that drawing will be performed onto back buffer and to blit updated contents you must call IDirect3DDevice8::Present().

The drawing of 3D function is performed with the following code:

  hr = m_p3DDevice->SetStreamSource(0, 
                                    m_pDataVB, 
                                    sizeof(GRAPH3DVERTEXSTRUCT));
  if (FAILED(hr)) {
    return hr;
  }

  hr = m_p3DDevice->SetVertexShader(D3DFVF_GRAPH3DVERTEX);
  if (FAILED(hr)) {
    return hr;
  }
  
  hr = m_p3DDevice->DrawPrimitive(D3DPT_TRIANGLESTRIP, 
                                  0, 
                                  m_dwElementsInVB - 2);
  if (FAILED(hr)) {
    return hr;
  }

m_pDataVB is a member of C3DGraphic class, it contains pointer to IDirect3DVertexBuffer8 interface. In our case contents of vertex buffer is an array of GRAPH3DVERTEXSTRUCT structures:

typedef struct {
  FLOAT x, y, z;
  FLOAT nx, ny, nz;
} GRAPH3DVERTEXSTRUCT;

where x, y and z - coordinates of vertex, nx, ny, nz - components of normal. Normal vector is used in lighting engine of Direct3D for correct shading of surfaces. Before calling DrawPrimitive function we must set vertex shader and data stream. Setting data stream is performed with SetStreamSorce function. We pass in this function stream number(0 if only one stream is used), pointer to vertex buffer, and size of each element in data stream, i.e. sizeof(GRAPH3DVERTEXSTRUCT). Vertex shaders are pretty new capability of Direct3D. Vertex shader is an abstract mechanism used for processing vertexes. They can be either in old FVF format or in custom vertex shader. Programming vertex shaders is very powerful and complex task, but it is enough for us now to use simple fixed vertex shader D3DFVF_GRAPH3DVERTEX, which defined as D3DFVF_XYZ | D3DFVF_NORMAL. It means that vertex engine should treat first 3 float number of each vertex as vertex coordinates, and latter 3 float number - as vertex normal vector.

After setting data stream and vertex shader we call DrawPrimitive method which renders data from vertex buffer onto back buffer's surface. The way in which data will be rendered depends on first parameter of that method - it can be one of D3DPRIMITIVETYPE enumeration type: D3DPT_POINTLIST, D3DPT_LINELIST, D3DPT_LINESTRIP, D3DPT_TRIANGLELIST, D3DPT_TRIANGLESTRIP or D3DPT_TRIANGLEFAN. I decided that the most convenient way for 3D function plotting is to use triangle strips, because it needs low memory. Function triangulating is performed in the way shown on the following picture.



Click here for larger image

This is how strip triangulating works: vertex buffer contains points P1, P2, P3, P4 and so on. When I call DrawPrimitive with D3DPT_TRIANGLESTRIP parameter Direct3D starts to render triangles 1, 2, 3 and so on again. Triangle 1 is defined by points P1, P2, P3, trinagle 2 - by P2, P3, P4. So N points in vertex buffer correspond (N-2) triangles. It is pretty nice but this approach has a drawback: all triangles are linked each with other. That is why even rows are triangulated from the left to the right, odd - from the right to the left(rows numeration start from zero of course!!!).

Implementation of all this stuff you can find in C3DGraphic::RecalculateData method. This method uses tool class CGraphGrid which builds graph grid (X0, Y0, Z0) - (Xn, Yn, Zn).

Light management, or all cats are gray in the dark.

Direct 3D has very powerful lighting engine. It supports several types of light on the scene: directional, point-source light and spotlight. Directional light has no source - only direction. You can think that directional light is like the sun: all its rays are parallel. Spotlight and point-source light has a point from which all rays shine. For more detailed information please read MSDN. Maybe soon I write an article for CodeGuru about Direct3D lighting... But in the mean time I want to face you with a problem: all vertexes that are supposed to be lit must have normal vector included. Normal determination is very easy if you still remember school geometry. So, how to do it for 3D graphic? There are 2(at least) ways:

  • First, we can find analytic expression for normal vector. It will be very accurate result but for any new 3D function we will have to make all calculations from the beginning.
  • Second, we can calculate approximate value of normal vector as we know neighbour points. Though it is an approximate decision, this way doesn't depend of 3D function. This approach is implemented in my sample project and you can find it in C3DGraphic::CalcNormal() function. Here is very brief explanation on finding normal.



Click here for larger image


- Finding 4 vectors to neighbour points:
V01 = P1 - P0;
V02 = P2 - P0;
V03 = P3 - P0;
V04 = P4 - P0;

- Finding 4 normals to faces as cross product of 
  respective vectors:
N1 = [V02, V01];
N2 = [V03, V02];
N3 = [V04, V03];
N4 = [V01, V04];

- Finding resulting normal as average vector of 
  4 faces' normals
N = (N1 + N2 + N3 + N4) / 4;

Material management, or why are they gray?

Everything in the world has color. Color determines how something looks. Apple is red, sky is blue. Direct3D treats properties of objects as materials. Materials are described with D3DMATERIAL8 structure:

typedef struct _D3DMATERIAL8 {
    D3DCOLORVALUE   Diffuse;
    D3DCOLORVALUE   Ambient;
    D3DCOLORVALUE   Specular;
    D3DCOLORVALUE   Emissive;
    float           Power;
} D3DMATERIAL8;

Diffuse, ambient and specular members of this structure describes how a material reflects respective components of light sources. Also you can define power with which specular light is reflected. Non-zero emissive component makes object shining, but note that light which this object emisses will not be reflected by any other object.

To put a material on an object you must call SetMaterial function before calling DrawPrimitive or other drawing function. You can do it in the following way:

  hr = m_p3DDevice->SetMaterial(&m_theGraphMaterial);
  ATLASSERT(SUCCEEDED(hr));
  if (FAILED(hr)) {
    return hr;
  }

Conclusion, or }

In the end I want to say some words about sample application again. It has 4 properties windows which can be activated from "Properties" menu. Here is the brief description of what you can see on those windows:

  • Material properties. This property window allows to edit material parameters: diffuse, ambient, emissive and specular color component and specular power.
  • Light properties. The scene is illuminated with one directional light source. You can change diffuse, ambient and specular color component as well as light direction.
  • Background color. This is a color which is used for clearing each frame in Clear function. You can set red, green and blue components.
  • Function type. You can choose one of 3 3D functions: splash function, plane function and parabaloid function.

All values can be changed with trackbars. 0 is minimum value, 1 is maximum. Minimum value corresponds to the lower position of trackbar, maximum - to the higher. Notes:
DirectX, Direct3D, Windows, Microsoft are trademarks of Microsoft company. All rights reserved. OpenGL is a trademark of Silicon Graphics Inc. All rights reserved.

Downloads

Download D3DSample application - 74 Kb
Download D3DSample source - 46 Kb