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


Comments

  • How can i get FullScreen mode

    Posted by Legacy on 05/14/2003 12:00am

    Originally posted by: m@dm@x

    This tutorial is very was help me, but for fullscreen mode, it dos't work.
    d3dpp.Windowed = false;
    and enythingelse propertis?
    Please help ...

    Reply
  • Anyone know how to import a .wmp map file from 3DGS?

    Posted by Legacy on 02/12/2003 12:00am

    Originally posted by: Hapyspaz

    I have been designing levels inside of 3DGameStudio. I have been able to import the models my friend created in Maya, but have had no success importing my map files (*.wmp).

    I hope this is not to big of a question to ask.

    Reply
  • Watcom compiler errors

    Posted by Legacy on 08/09/2002 12:00am

    Originally posted by: xPheRe

    I've got some errors on link my DX8 application under Watcom. Has DX loss its compatibility with WC11.0???

    Reply
  • Resource

    Posted by Legacy on 07/31/2002 12:00am

    Originally posted by: Mike

    Hi!

    How can i convert a resource Bitmap (i.e. IDB_BITMAP1)
    to a Texture? I know, there are functions like
    D3DUtil_... but they doesnt work in my application!

    Please help!

    Reply
  • Can't compile DirectX8 WinCE

    Posted by Legacy on 06/09/2002 12:00am

    Originally posted by: Lander

    When I try to compile a simple program that just initializing the Direct3D device i get an error:
    --------------------Configuration: mywin - Win32 (WCE emulator) Debug--------------------
    Linking...
    mywin.obj : error LNK2019: unresolved external symbol _Direct3DCreate8 referenced in function _WinMain
    emulatorDbg/mywin.exe : fatal error LNK1120: 1 unresolved externals
    Error executing link.exe.

    mywin.exe - 2 error(s), 0 warning(s)

    What to do?

    Reply
  • it's a good

    Posted by Legacy on 05/09/2002 12:00am

    Originally posted by: susuhada

    it's a good ^^

    Reply
  • Failed with D3DDEVTYPE_REF

    Posted by Legacy on 03/09/2002 12:00am

    Originally posted by: Brian

    I changed D3DDEVTYPE_HAL to D3DDEVTYPE_REF because I don't have a 3d card, and it failed. Why? I have the ref set in DirectX settings.

    Reply
  • Checking to see if an object is clicked with the mouse

    Posted by Legacy on 03/03/2002 12:00am

    Originally posted by: Bud Baker

    One of the basics of game programming is checking to see if a 3d object was clicked with the mouse, yet in immediate mode (the obvious choice for games), none of the code I have waded through checks for a mouse click on an object. The lone sdk sample dealing with this (pick) just checks for a primitive pick in a global vertex buffer. Do you just maintain a vertex buffer for every object and check for picks on every buffer when a mouse click happens? I'd really rather use an older version of DirectX (I can hear the grumbling from the size of the DX8 home user download already), but there's even less help for pick testing in the older docs. Better ideas or code, anyone?

    Reply
  • What are the equations that convert a 3D plan into a 2D.

    Posted by Legacy on 02/27/2002 12:00am

    Originally posted by: Liviu Vaduva

    And how can I rotate an 3D object into space.

    Reply
  • I have a question regarding your algorithm here...

    Posted by Legacy on 02/17/2002 12:00am

    Originally posted by: Konstantin Koton

    C3DGraphic::CalcNormal() makes and calculates 4 different normals for every pair of XY-coords on the grid. I'm not very strong on math, but I'd like to know what's the purpose of calculating and averaging 4 different vectors and taking a SERIOUS performance hit on such a critical function?

    Once again, complete newbie here so please don't flame me if you think my question is dumb. After all, I think all of us are here to learn? :)

    Reply
  • Loading, Please Wait ...

Leave a Comment
  • Your email address will not be published. All fields are required.

Top White Papers and Webcasts

Most Popular Programming Stories

More for Developers

Latest Developer Headlines

RSS Feeds