Click here to Skip to main content
65,938 articles
CodeProject is changing. Read more.
Articles
(untagged)

A Primer of DirectX Basics and the DirectX3D API

0.00/5 (No votes)
11 Dec 2010 1  
An article that desribes some essentials to help learn the DirectX technology.

What is DirectX?

This focus of this article is to provide the beginner (or any student level of computer programming) with a concept of the DirectX technology. As such, the first section provides some general aspects that are necessary to understand DirectX. They might appear complicated and unrelated. They should, however, tie together to help one move from the general to the specifics. One of the best resources available on the Internet about the DirectX technology (Direct X 9, in particular) is the series of purchasable and downloadable PDFs written by Frank Luna. But in any event, in my limited knowledge, I will begin by first defining what DirectX is, and then provide some examples of basic DirectX applications. The strongest approach to gain a working understanding of the DirectX technology is to obtain a strong grasp of the underlying principles. This means welding together different pieces of subsystem technologies (like the Win32 API, basic COM programming, Windows messaging, the message loop, the Hardware Abstraction Layer (hal.dll), the graphics processing unit) with the applied mathematical subjects of matrices and vectors. The applied math, however, merely explains how movements, motions, rotations, and transformations occur on a user interface. But, nothing ventured, nothing gained, right?

DirectX is an SDK composed of a collection of COM libraries, or a collection of APIs meant for handling tasks related to multimedia, such as game programming and video, on Microsoft platforms. In particular, DirectX is a collection of smaller APIs:

  • Direct3D: A graphics API that simplifies the process of drawing images to the screen. This component handles the visual aspect of games.
  • DirectShow: Responsible for media streaming.
  • DirectSound: Allows developers to play digitized sounds.
  • DirectPlay: An API used to create and design multiplayer games, or games that need to communicate via networks.

We will focus on the Direct3D API. The most recent versions of the SDK include "Direct Computing". In any event, making any Direct3D program requires one to first create a window, a standard window identified by a window handle. You can use the normal Win32 API functions, or other libraries like MFC. The purpose of creating a main window is to receive messages and to provide a canvas upon which Direct3D will render images. Direct3D will draw all of its data onto this window. Anyone who has ever worked on, or studied, Windows UI creation will know that UIs work by processing messages enqueued onto a per-UI thread message queue.

The next step is to create a Direct3D object. Next, we create a Direct3D device object - this is what the Direct3D object creates for us: a Direct3D device. A Direct3D device represents the system's graphics card, and uses it to draw images. That is, we use the Direct3D device to actually draw images in our app's window. Next, we configure the message loop. Once applications create their main window, they usually enter a message pump. When this is accomplished, we then render and display a scene. As the application ends, the message loop finishes and all created objects must be released. If you are familiar with COM, then you might remember the standard Release method of the IUnknown interface.

Stop Right Here: What is an IUnknown Interface?

COM is a specification that describes a strict set of programming steps and practices meant to be independent of the programming language used. This is because COM is a binary standard. When different programming languages define a function that performs the same general purpose differently while using source code text, at the bit level, it will appear the same to the input pin of an integrated circuit chip. COM separates the definition, or interface, from the implementation, by using strict identity rules and extraneous plumbing code that registers a COM component in the system registry. To the Operating System, a structure in memory is meant to appear the same as any other structure. IUnknown is the root interface of a hierarchy of COM interfaces. Here is a reference from the MSDN library:

COM defines one special interface, IUnknown, to implement some essential functionality. All component objects are required to implement the IUnknown interface, and conveniently, all other COM and OLE interfaces derive from IUnknown. IUnknown has three methods: QueryInterface, AddRef, and Release. In C++ syntax, IUnknown looks like this:

interface IUnknown {
   virtual   HRESULT   QueryInterface(IID& iid, void** ppvObj) = 0;
   virtual   ULONG   AddRef() = 0;
   virtual   ULONG   Release() = 0;
}

AddRef and Release are simple reference counting methods. A component object's AddRef method is called when another component object is using the interface; the component object's Release method is called when the other component no longer requires use of that interface. While the component object's reference count is nonzero, it must remain in memory; when the reference count becomes zero, the component object can safely unload itself because no other component holds a reference to it.

A pointer to an interface, which is the only way in which an object user can communicate with an object, can only access the specific member functions of that interface. The user never has a pointer or access to the object as a whole, which is necessary to create a binary standard. However, an object can implement as many interfaces as it wants, providing a different interface pointer - a different function table - for each supported interface. DirectX is actually a series of COM objects, one of which is Direct3D. Direct3D is a COM object that has other COM objects inside it. Ultimately, it contains everything you need to run 2D and 3D graphics using software, hardware, or whatever-ware. So because Direct3D is already stored in classes, don't be surprised when you see Direct3D functions being called like this:

  • device->CreateRenderTargetView()
  • device->Release()

To gain a more powerful insight into the Component Object Model, read "Essential COM", written by Don Box".

Stop Again: Message Loop and Message Pump?

Consider dragging and dropping an icon on your desktop. The icon can be likened to a data source that is dragged and dropped to a destination source. The drag is like a copy/cut of highlighted text. The drop is like a paste. The bitmap location of the icon has an address in video memory. The click of the mouse (or a keyboard event) is an event. For each type of event corresponds a callback procedure. This means that the type of the event must be defined by the Windows OS. The OS, in turn, creates a message which sends it to the concerned application (or destination bitmap location on the desktop upon dropping the icon). That destination can also be considered a target, which upon reception of the data transfer, calls back to the data source to complete the message loop. This is similar to dropping the copied text on a target location which consumes the data. In a windowed application, each window has one thread which waits for messages in a message queue specific to this thread. When a message arrives, the thread executes the corresponding callback procedure. The main code of such a thread is built with a loop which is executed each time a message is received.

DirectX Requires a Knowledge of Graphics Hardware

A surface is a matrix of pixels that Direct3D uses primarily to store 2D image data. While we might view the pixel data to be in a row and column format of a matrix, the pixel data is stored in a linear array. Recall that in memory, a stack is an abstract data structure, but a buffer is like a horizontal array. Direct3D maintains a collection of surfaces, perhaps two or three, called a swap chain. Swap chains, and more specifically, the technique of page flipping, are used to provide a smooth animation between frames. The surface in the front buffer slot is the surface that corresponds to the image presently being displayed on the monitor. The monitor does not display the image represented by the front buffer instantaneously; it takes one-sixtieth of a second on a monitor with a refresh rate of 60 Hz.

A Basic DirectX Program

Assume we have created a window object and we want to paint it blue. This takes four steps:

  1. Create global variables and function prototypes
  2. Create a function to initialize Direct3D and create the Direct3D device
  3. Create a function to render a frame
  4. Create a function to close Direct3D

Assume that you have installed a copy of Visual Studio 2008 or 2010 to your Windows OS. Upon that installation, you download and install the most recent version of the DirectX SDK. The corresponding DirectX header and library files in the Include and Lib directories should either be transferred to the corresponding C:\Program Files\Microsoft Visual Studio 10.0\VC Include and Lib directories, or the DirectX Bin, Include, and Lib directory paths be properly configured in the Properties section. One might find a compiler error stating something like "dxerr9.h" not found. This is because we have installed the DirectX 11 SDK (June 2010) edition but the dxerr9.h header file shipped with the DirectX 9 SDK. Because we are not examining Managed DirectX at this point, we need to recall that C++ header files contain certain predefined functions that get either deprecated or are not included with the most recent edition.

The code sample below is referenced from http://www.directx.com, a web site that contains a powerful archive of DirectX tutorials. Our intention, however, is to write our own code from scratch once we have a general understanding of how DirectX works and how to use COM C++ syntax effectively.

// include the basic windows header files and the Direct3D header file
#include <windows>
#include <windowsx.h> 
#include <d3d9.h>
// include the Direct3D Library file
#pragma comment (lib, "d3d9.lib")
// global declarations
LPDIRECT3D9 d3d;    // the pointer to our Direct3D interface
LPDIRECT3DDEVICE9 d3ddev;    // the pointer to the device class
// function prototypes
void initD3D(HWND hWnd);    // sets up and initializes Direct3D
void render_frame(void);    // renders a single frame
void cleanD3D(void);    // closes Direct3D and releases memory
// the WindowProc function prototype
LRESULT CALLBACK WindowProc(HWND hWnd, UINT message, WPARAM wParam, LPARAM lParam);

Create a function to initialize Direct3D and create the Direct3D device:

// this function initializes and prepares Direct3D for use
void initD3D(HWND hWnd)
{
    d3d = Direct3DCreate9(D3D_SDK_VERSION);    // create the Direct3D interface

    D3DPRESENT_PARAMETERS d3dpp;    // create a struct to hold various device information

    ZeroMemory(&d3dpp, sizeof(d3dpp));    // clear out the struct for use
    d3dpp.Windowed = TRUE;    // program windowed, not fullscreen
    d3dpp.SwapEffect = D3DSWAPEFFECT_DISCARD;    // discard old frames
    d3dpp.hDeviceWindow = hWnd;    // set the window to be used by Direct3D
    // create a device class using this information and information from the d3dpp stuct
    d3d->CreateDevice(D3DADAPTER_DEFAULT,
                      D3DDEVTYPE_HAL,
                      hWnd,
                      D3DCREATE_SOFTWARE_VERTEXPROCESSING,
                      &d3dpp,
                      &d3ddev);
}

Here is the complete code:

#include <windows.h>
#include <windowsx.h>
#include <d3d9.h>
// include the Direct3D Library file
#pragma comment (lib, "d3d9.lib")
// global declarations
LPDIRECT3D9 d3d;    // the pointer to our Direct3D interface
LPDIRECT3DDEVICE9 d3ddev;    // the pointer to the device class
// function prototypes
void initD3D(HWND hWnd);    // sets up and initializes Direct3D
void render_frame(void);    // renders a single frame
void cleanD3D(void);    // closes Direct3D and releases memory
// the WindowProc function prototype
LRESULT CALLBACK WindowProc(HWND hWnd, UINT message, WPARAM wParam, LPARAM lParam);
// the entry point for any Windows program
int WINAPI WinMain(HINSTANCE hInstance,
                   HINSTANCE hPrevInstance,
                   LPSTR lpCmdLine,
                   int nCmdShow)
{
    HWND hWnd;
    WNDCLASSEX wc;
    ZeroMemory(&wc, sizeof(WNDCLASSEX));
     wc.cbSize = sizeof(WNDCLASSEX);
    wc.style = CS_HREDRAW | CS_VREDRAW;
    wc.lpfnWndProc = WindowProc;
    wc.hInstance = hInstance;
    wc.hCursor = LoadCursor(NULL, IDC_ARROW);
    wc.hbrBackground = (HBRUSH)COLOR_WINDOW;
    wc.lpszClassName = L"WindowClass";
    RegisterClassEx(&wc);
    hWnd = CreateWindowEx(NULL,
                          L"WindowClass",
                          L"Test",
                          WS_OVERLAPPEDWINDOW,
                          300, 300,
                          800, 600,
                          NULL,
                          NULL,
                          hInstance,
                          NULL);

    ShowWindow(hWnd, nCmdShow);
   // set up and initialize Direct3D
     initD3D(hWnd);
    // enter the main loop:
     MSG msg;

    while(TRUE)
    {
        while(PeekMessage(&msg, NULL, 0, 0, PM_REMOVE))
        {
            TranslateMessage(&msg);
            DispatchMessage(&msg);
        }

        if(msg.message == WM_QUIT)
            break;

        render_frame();
    }

    // clean up DirectX and COM
    cleanD3D();

    return msg.wParam;
}
// this is the main message handler for the program
LRESULT CALLBACK WindowProc(HWND hWnd, UINT message, 
                 WPARAM wParam, LPARAM lParam)
{
    switch(message)
    {
    case WM_DESTROY:
            {
                PostQuitMessage(0);
                return 0;
            } break;
    }
   return DefWindowProc (hWnd, message, wParam, lParam);
}

// this function initializes and prepares Direct3D for use
void initD3D(HWND hWnd)
{
    d3d = Direct3DCreate9(D3D_SDK_VERSION);    // create the Direct3D interface
    D3DPRESENT_PARAMETERS d3dpp;    // create a struct to hold various device information
     ZeroMemory(&d3dpp, sizeof(d3dpp));    // clear out the struct for use
    d3dpp.Windowed = TRUE;    // program windowed, not fullscreen
    d3dpp.SwapEffect = D3DSWAPEFFECT_DISCARD;    // discard old frames
    d3dpp.hDeviceWindow = hWnd;    // set the window to be used by Direct3D
    // create a device class using this information
    // and the info from the d3dpp stuct
    d3d->CreateDevice(D3DADAPTER_DEFAULT,
                      D3DDEVTYPE_HAL,
                      hWnd,
                      D3DCREATE_SOFTWARE_VERTEXPROCESSING,
                      &d3dpp,
                      &d3ddev);
}
// this is the function used to render a single frame
void render_frame(void)
{
    // clear the window to a deep blue
    d3ddev->Clear(0, NULL, D3DCLEAR_TARGET, D3DCOLOR_XRGB(0, 40, 100), 1.0f, 0); 
    d3ddev->BeginScene();    // begins the 3D scene
   // do 3D rendering on the back buffer here
    d3ddev->EndScene();    // ends the 3D scene
   d3ddev->Present(NULL, NULL, NULL, NULL);
   // displays the created frame on the screen
}
// this is the function that cleans up Direct3D and COM
void cleanD3D(void)
{
    d3ddev->Release();    // close and release the 3D device
    d3d->Release();    // close and release Direct3D
}

To run this code, fire up Visual Studio C++ 2010 and start a new, empty VC++ Win32 project, and name it Window. Since the project is empty, right-click the source files, select "Add new item", choose C++ code file, and name it main.cpp. Copy and paste the code onto the IDE platform, and run it.

blank.JPG

Let's try an application that demonstrates initializing DirectX by rendering this background and drawing some text. The image shown below has flickering text against a lighter blue background. We can write a C++ class file to handle the steps mentioned (amongst others) and be referenced from a main source code file. When you run this application, you will see that the text flickers. Let's also make the canvas a lighter blue:

2.JPG

There are two header files, a C++ object file, and a C++ main source file: shown below is the main file. The downloadable zip is called Direct3D.zip. Download and extract the files into a newly-made folder in your Visual Studio Projects directory. Double-click the project file and run the application:

#include "d3dApp.h"
#include <tchar.h>
#include <crtdbg.h>

class HelloD3DApp : public D3DApp
{
public:
    HelloD3DApp(HINSTANCE hInstance, std::string winCaption,
         D3DDEVTYPE devType, DWORD requestedVP);
    ~HelloD3DApp();

    bool checkDeviceCaps();
    void onLostDevice();
    void onResetDevice();
    void updateScene(float dt);
    void drawScene();

private:

    ID3DXFont* mFont;
};

int WINAPI WinMain(HINSTANCE hInstance, HINSTANCE prevInstance,
                   PSTR cmdLine, int showCmd)
{
    // Enable run-time memory check for debug builds.
    #if defined(DEBUG) | defined(_DEBUG)
        
     _CrtSetDbgFlag( _CRTDBG_ALLOC_MEM_DF |
      _CRTDBG_LEAK_CHECK_DF );
    #endif


    HelloD3DApp app(hInstance, "Here is Direct3D",          

         D3DDEVTYPE_HAL, 
        D3DCREATE_HARDWARE_VERTEXPROCESSING);
    gd3dApp = &app;
    
    return gd3dApp->run();
}

HelloD3DApp::HelloD3DApp(HINSTANCE hInstance, 
     std::string winCaption, D3DDEVTYPE devType, 
     DWORD requestedVP)
: D3DApp(hInstance, winCaption, devType, requestedVP)
{
    srand(time_t(0));

    if(!checkDeviceCaps())
    {
      MessageBox(0, "checkDeviceCaps() Failed", 0, 0);
      PostQuitMessage(0);
    }

    D3DXFONT_DESC fontDesc;
    fontDesc.Height          = 80;
    fontDesc.Width           = 40;
    fontDesc.Weight          = FW_BOLD;
    fontDesc.MipLevels       = 0;
    fontDesc.Italic          = true;
    fontDesc.CharSet         = DEFAULT_CHARSET;
    fontDesc.OutputPrecision = OUT_DEFAULT_PRECIS;
    fontDesc.Quality         = DEFAULT_QUALITY;
    fontDesc.PitchAndFamily  = DEFAULT_PITCH | FF_DONTCARE;
    _tcscpy(fontDesc.FaceName, _T("Ariel"));

    HR(D3DXCreateFontIndirect(gd3dDevice, &fontDesc, &mFont));
}

HelloD3DApp::~HelloD3DApp()
{
    ReleaseCOM(mFont);
}

bool HelloD3DApp::checkDeviceCaps()
{
    // Nothing to check.
    return true;
}

void HelloD3DApp::onLostDevice()
{
    HR(mFont->OnLostDevice());
}

void HelloD3DApp::onResetDevice()
{
    HR(mFont->OnResetDevice());
}

void HelloD3DApp::updateScene(float dt)
{
}

void HelloD3DApp::drawScene()
{
    HR(gd3dDevice->Clear(0, 0,
        D3DCLEAR_TARGET | D3DCLEAR_ZBUFFER, 
        D3DCOLOR_XRGB(0, 0, 255), 1.0f, 0));

    RECT formatRect;
    GetClientRect(mhMainWnd, &formatRect);

    HR(gd3dDevice->BeginScene());

    mFont->DrawText(0, _T("Here is Direct3D"), -1, 
        &formatRect, DT_CENTER | DT_VCENTER, 
        D3DCOLOR_XRGB(rand() % 256, rand() % 256, rand() % 256));

    HR(gd3dDevice->EndScene());
    HR(gd3dDevice->Present(0, 0, 0, 0));
}

We derive a new class from D3DApp (enclosed in the zip file) and override the functions comprised in the unit structure:

class HelloD3DApp : public D3DApp
{
public:
      HelloD3DApp(HINSTANCE hInstance,
                  std::string winCaption,
                  D3DDEVTYPE devType, DWORD requestedVP);
      ~HelloD3DApp();

      bool checkDeviceCaps();
      void onLostDevice();
      void onResetDevice();
      void updateScene(float dt);
      void drawScene();

private:

      ID3DXFont* mFont; };
};

This child class has one data member, an ID3DXFont interface from which text output in Direct3D is done. To get the application up and running, we instantiate an instance of our child class and enter the message loop:

int WINAPI WinMain(HINSTANCE hInstance, HINSTANCE prevInstance,
                     PSTR cmdLine, int showCmd)
{
      // Enable run-time memory check for debug builds.
      #if defined(DEBUG) | defined(_DEBUG)
            _CrtSetDbgFlag( _CRTDBG_ALLOC_MEM_DF | 
                  _CRTDBG_LEAK_CHECK_DF );
      #endif


      HelloD3DApp app(hInstance);
      gd3dApp = &app;

      return gd3dApp->run();
}

Now, let's examine the implementation of the constructor, destructor, and framework methods.

HelloD3DApp::HelloD3DApp(HINSTANCE hInstance,
                         std::string winCaption,
                         D3DDEVTYPE devType, DWORD requestedVP)
: D3DApp(hInstance, winCaption, devType, requestedVP)
{

      srand(time_t(0));

      if(!checkDeviceCaps())
      {
            MessageBox(0, "checkDeviceCaps() Failed", 0, 0);
            PostQuitMessage(0);
      }

      D3DXFONT_DESC fontDesc;
      fontDesc.Height          = 80;
      fontDesc.Width           = 40;
      fontDesc.Weight          = FW_BOLD;
      fontDesc.MipLevels       = 0;
      fontDesc.Italic          = true;
      fontDesc.CharSet         = DEFAULT_CHARSET;
      fontDesc.OutputPrecision = OUT_DEFAULT_PRECIS;
      fontDesc.Quality         = DEFAULT_QUALITY;
      fontDesc.PitchAndFamily  = DEFAULT_PITCH | FF_DONTCARE;
      _tcscpy(fontDesc.FaceName,_T("Ariel"));
      
      HR(D3DXCreateFontIndirect(gd3dDevice, &fontDesc, &mFont));
}

The constructor first constructs its parent part in the initialization list. Then, it seeds the random number generator. Next, it checks the device capabilities with checkDeviceCaps. It then fills out a D3DXFONT_DESC structure, which describes the attributes of the font we use for text drawing. Finally, it invokes the D3DXCreateFontIndirect function, which returns a pointer to an ID3DXFont interface (via the third parameter) based on the specified font description (parameter two). Note that the function also requires that we pass in a copy of a valid Direct3D device pointer (parameter one), since the font will need the device for drawing the text (all drawing is done through the Direct3D device). Because the constructor created an ID3DXFont object, the destructor must destroy it. C++ is powerful, but it can lead to memory leaks. So we destroy with a destructor, and we clean up COM by Release().

HelloD3DApp::~HelloD3DApp()
{
      ReleaseCOM(mFont);
}

The ReleaseCOM macro is defined in d3dUtil.h, and simply calls the Release method and sets the pointer to null:

#define ReleaseCOM(x) { if(x){ x->Release();x = 0; } }

For this simple demo, there are no device capabilities to check; thus our checkDeviceCaps implementation simply returns true. In the utilities directory of the DirectX SDK installation, there is a DirectX Caps Viewer to allow you to see the capabilities of your graphics.

bool Hel1oD3DApp::checkDeviceCaps()
{
      // Nothing to check.
      return true;
}

There is nothing to update in this demo, so our updateScene function does nothing:

void HelloD3DApp::updateScene(float dt)
{
}

A good DirectX series is written by Frank D. Luna. Because the most recent version of the DirectX SDK was released a few months ago (at the date of writing this article), there might not yet be a lot of good textbooks to learn from. The texts on DirectX 9 and 10 should explain things a lot more effectively than an article. But in any event, certain resources need to be released before resetting the device, and certain resources and device states need to be restored after resetting the device. To handle these situations, our framework provides the onLostDevice and onResetDevice methods, which are called before and after a device is reset, respectively. The mFont object contains Direct3D resources internally, and needs to do some work before a reset and after; thus we have:

void HelloD3DApp::onLostDevice()
{
      HR(mFont->OnLostDevice());
}
void HelloD3DApp::onResetDevice()

{
      HR(mFont->OnResetDevice());
}

Here, mFont->OnLostDevice invokes whatever code ID3DXFont needs to execute before a reset, and mFont->OnResetDevice invokes whatever code ID3DXFont needs to execute after a reset. Finally, we implement the drawScene method and output the text:

void HelloD3DApp::drawScene()
{
      HR(gd3dDevice->Clear(0, 0, D3DCLEAR_TARGET | D3DCLEAR_ZBUFFER,
         D3DCOLOR_XRGB(255, 255, 255), 1.0f, 0));

      RECT formatRect;
      GetClientRect(mhMainWnd, &formatRect);

      HR(gd3dDevice->BeginScene());

      mFont->DrawText(0, _T("Here is Direct3D"), -1,
            &formatRect, DT_CENTER | DT_VCENTER,
            D3DCOLOR_XRSB(rand() % 256, rand() % 256, rand() % 256));

      HR(gd3dDevice->EndScene());
      HR(gd3dDevice->Present(0, 0, 0, 0));
}

Observe first that we call the IDirect3DDevice9::Clear method, which clears the back buffer (target) and depth buffer to D3DCOLOR_XRSB(0, 0, 255) (blue) and 1.0, respectively. The declaration of IDirect3DDevice9::Clear is:

HRESULT IDirect3DDevice9::Clear(
      DWORD Count,
      const D3DRECT* pRects,
      DWORD Flags,
      D3DCOLOR Color,
      float Z,
      DWORD Stencil);

In the C language, when a function is called, it has a return value. If there is an error upon the function's return, it can be obtained by studying the errno.h header file. In COM, method results return an error number of type HRESULT. HRESULTs are 32 bit integers that provide information about the error to the caller. The remainder of the code can be itemized as follows:

  • Count: Number of rectangles in the pRects array
  • pRects: An array of screen rectangles to clear. This allows us to only clear parts of a surface.
  • Flags: Specifies which surfaces to clear. We can clear one or more (combined by a bitwise OR) of the following surfaces:

The surfaces:

  1. D3DCLEAR_TARGET: The render target surface, usually the back buffer.
  2. D3DCLEAR_ZBUFFER: The depth buffer.
  3. D3DCLEAR_STENCIL: The stencil buffer.

After we have cleared the back buffer and depth buffer, we get the dimensions of the client area rectangle, which will be used to format the text in the window. But what does that mean? Count and clear rectangles? Let's take a brief look at some geometric descriptions.

The Rendering PipeLine

The rendering pipeline involves the process of taking a geometric description of a 3D scene and generating a 2D image from it. Now think of model representation. That scene is a collection of objects or models (those objects collectively comprise a scene). An object is represented as a triangle mesh approximation. A mesh is basically a surface. The triangles of the mesh are the building blocks of the object that we are modeling. Moreover, triangles are the basic building blocks of 3D objects. To construct an object, we create a triangle list that describes the shape and contours of the object. A triangle list contains the data for each individual triangle that we want to draw. To draw a rectangle, we break it into two triangles. In fact, a mesh is also basically a representation of a surface. The mesh represents the surface through a system of points and lines. The points describe the high and low areas of the surface, and the lines connect the points to establish how you get from one point to the next.

Despite the above definition, in the simplest sense, a surface is a flat plane. A flat plane needs three points to define it. Mathematically, a single point is one dimensional, a line segment is two dimensional, and a shape like a circle is three dimensional. Thus, the simplest surface that can be described in a mesh is a single triangle. This means that meshes can only be described with triangles. That is because a triangle is the simplest, most granular way to define a surface. A large, complex surface obviously can't be accurately described by one triangle. Instead, it can be approximated by many smaller triangles. You could argue that you could use a rectangle to define a surface, but it's not as granular as a triangle. A triangle is defined by its three points, also called vertices. Since a rectangle can be broken into two triangles, two triangles can much more accurately describe a surface than a single rectangle. Note the expression below:

vertex rect[6] = {v0, v1, v2, // triangle0
                  v0, v2, v3}; // triangle1

A possible problem lies in the fact that often the triangles for a 3D object share many of the same vertices as the rectangle. Although only two vertices are duplicated in the rectangle example, the number of duplicate vertices can increase as the detail and the complexity of the model increases. To solve this problem, we introduce the concept of indices. It works like this: We create a vertex list and an index list. The vertex list consists of all the unique vertices, and the index list contains values that index into the vertex list to define how they are to be put together to form triangles. Returning to the rectangle example, the vertex list would be constructed as follows:

Vertex vertexList[4] = {v0, v1, v2, v3}; 
// two triangle corners share the same corners as the rectangle (4)

Then the index list needs to define how the vertices in the vertex list are to be put together to form the two triangles.

WORD indexList[6] = {0, 1, 2, // triangle0
                     0, 2, 3}; // triangle1

A Virtual Camera

Stated crudely, a camera allows us to view the stuff that comprises the model. Light helps us see it. When you see an object, it is actually the light reflecting off of that object and entering your eye. The camera specifies what part of the world the viewer can see, and thus what part of the world for which we need to generate a 2D image. The camera is positioned and oriented in the world, and defines the volume of space that is visible. The projection window is the 2D area that the 3D geometry inside the frustum gets projected onto to create the 2D image representation of the 3D scene. It is important to know that we define the projection window with the dimensions min = (-1, -1) and max = (1, 1). Therefore, given a geometric description of a 3D scene, and a positioned and aimed virtual camera in that scene, the rendering pipeline refers to the entire sequence of steps necessary to generate a 2D image that can be displayed on a monitor screen based on what the virtual camera sees. Now consider this notion. When constructing 3D objects, rather than building that object's geometry with coordinates relative to a global scene (or world space), you would specify them relative to a local coordinate system (local space), where the object is the center of the coordinate system.

When the entire 3D model is finished in local space, it is placed in that global scene. But how is this done? It is done by specifying where we want the origin of axes of the local space coordinate system relative to the global scene coordinate system, and executing a change of coordinates transformation. In any computer 3D graphics topic, the subject of matrices serves to clarify the reasoning. Now, we must recall that (roughly) the objects that make up a scene are comprised of triangles. That a triangle is defined by its three points, called vertices. A set of three vertices with unique positions define a unique triangle. In order for a graphics processing unit to render a triangle, we must tell it about the position of the triangle's three vertices. For a 2D example, let's say we wish to render a triangle. We would pass the three vertices with the positions (say) (0, 0), (0, 1), and (1, 0) to the Graphics Processing Unit, or GPU, and the GPU has enough information to render the triangle that we want.

So Where Are We?

As a flat TV screen is two dimensional, we can simulate 3D scenes on 2D planes. We notice that the size of an object diminishes with depth: objects obscure the objects behind them, lighting and shading depict the solid form and volume of 3D objects, and shadows imply the location of light sources and indicate the positions of objects relative to other objects in the scene. Now, the points where two edges meet on a polygon is a vertex. A vertex in Direct3D includes a 3D spatial location, but can also couple other data components to the vertex. In order for Direct3D to know what to do with our vertex data, we must define a vertex declaration, which provides Direct3D with a description of our vertex structure. We approximate objects with a triangle mesh. We can define each triangle by specifying its three vertices. The virtual camera is modeled as a frustum. The volume of space inside the frustum is what the camera sees. So, finally, given a geometric description of a 3D scene and a positioned and aimed virtual camera in that scene, the rendering pipeline refers to the entire sequence of steps necessary to generate a 2D image that can be displayed on a monitor screen based on what the virtual camera sees.

The vertices of each 3D object are defined relative to the object's local coordinate system. Next, the associated world transform (a matrix equation) of each object changes the object's vertex coordinates so that they are relative to the world coordinate system. After all of this, the vertices in the scene are relative to the same coordinate system, and should be positioned, oriented, and scaled correctly in relation to each other. The camera is an object in the world as well, and we can choose to make the camera our frame of reference instead of the world coordinate system so that it is relative to the camera coordinate system (view space). The projection matrix modifies each vertex. After the projection matrix modification, back facing triangles are culled, and geometry outside the frustum is discarded or clipped. Next, the homogenous divide occurs, which finishes the projection process and transforms vertices to normalized device coordinates. At this point, the normalized device coordinates are mapped to a portion of the back buffer (or the entire back buffer) as defined by the viewport transformation. Finally, rasterization takes places.

An Example of Transformation Using Matrices

The next step renders the 3D geometry. To deal with 3D geometry, we need to introduce the use of 4x4 matrices to transform the geometry with translations, rotations, scaling, and setting up our camera. Geometry is defined in model space. We can move it (translation), rotate it (rotation), or stretch it (scaling) using a world transform.

The geometry is then said to be in world space. Next, we need to position the camera, or eye point, somewhere to look at the geometry. Another transform, via the view matrix, is used to position and rotate our view. With the geometry then in view space, our last transform is the projection transform, which "projects" the 3D scene into our 2D viewport.

4.JPG

Here is the main source code file. The other header and object files are available for download at the top of this article:

#include "d3dApp.h"
#include "DirectInput.h"
#include <crtdbg.h>
#include "GfxStats.h"
#include <list> 
#include "Vertex.h"

class ColoredWavesDemo : public D3DApp
{
public:
    ColoredWavesDemo(HINSTANCE hInstance, std::string winCaption, 
                     D3DDEVTYPE devType, DWORD requestedVP);
    ~ColoredWavesDemo();

    bool checkDeviceCaps();
    void onLostDevice();
    void onResetDevice();
    void updateScene(float dt);
    void drawScene();

    // Helper methods
    void buildGeoBuffers();
    void buildFX();
    void buildViewMtx();
    void buildProjMtx();

private:
    GfxStats* mGfxStats;

    DWORD mNumVertices;
    DWORD mNumTriangles;

    IDirect3DVertexBuffer9* mVB;
    IDirect3DIndexBuffer9*  mIB;
    ID3DXEffect*            mFX;
    D3DXHANDLE              mhTech;
    D3DXHANDLE              mhWVP;
    D3DXHANDLE              mhTime;

    float mTime;

    float mCameraRotationY;
    float mCameraRadius;
    float mCameraHeight;

    D3DXMATRIX mView;
    D3DXMATRIX mProj;
};


int WINAPI WinMain(HINSTANCE hInstance, HINSTANCE prevInstance,
                   PSTR cmdLine, int showCmd)
{
    // Enable run-time memory check for debug builds.
    #if defined(DEBUG) | defined(_DEBUG)
   _CrtSetDbgFlag( _CRTDBG_ALLOC_MEM_DF | _CRTDBG_LEAK_CHECK_DF );
    #endif

    ColoredWavesDemo app(hInstance, "Surface Waves", 
                         D3DDEVTYPE_HAL, 
                         D3DCREATE_HARDWARE_VERTEXPROCESSING);
    gd3dApp = &app;

    DirectInput di(DISCL_NONEXCLUSIVE
        |DISCL_FOREGROUND, DISCL_NONEXCLUSIVE
         |DISCL_FOREGROUND);
    gDInput = &di;

    return gd3dApp->run();
}

ColoredWavesDemo::ColoredWavesDemo(HINSTANCE hInstance, 
    std::string winCaption, D3DDEVTYPE devType, DWORD requestedVP)
    : D3DApp(hInstance, winCaption, devType, requestedVP)
{
    if(!checkDeviceCaps())
    {
        MessageBox(0, "checkDeviceCaps() Failed", 0, 0);
        PostQuitMessage(0);
    }

    mGfxStats = new GfxStats();

    mCameraRadius    = 25.0f;
    mCameraRotationY = 1.2 * D3DX_PI;
    mCameraHeight    = 15.0f;
    mTime            = 0.0f;

    buildGeoBuffers();
    buildFX();

    onResetDevice();

    InitAllVertexDeclarations();
}

ColoredWavesDemo::~ColoredWavesDemo()
{
    delete mGfxStats;
    ReleaseCOM(mVB);
    ReleaseCOM(mIB);
    ReleaseCOM(mFX);

    DestroyAllVertexDeclarations();
}

bool ColoredWavesDemo::checkDeviceCaps()
{
    D3DCAPS9 caps;
    HR(gd3dDevice->GetDeviceCaps(&caps));

    // Check for vertex shader version 2.0 support.
    if( caps.VertexShaderVersion < D3DVS_VERSION(2, 0) )
        return false;

    // Check for pixel shader version 2.0 support.
    if( caps.PixelShaderVersion < D3DPS_VERSION(2, 0) )
        return false;

    return true;
}

void ColoredWavesDemo::onLostDevice()
{
    mGfxStats->onLostDevice();
    HR(mFX->OnLostDevice());
}

void ColoredWavesDemo::onResetDevice()
{
    mGfxStats->onResetDevice();
    HR(mFX->OnResetDevice());


    // The aspect ratio depends on the backbuffer dimensions, which can 
    // possibly change after a reset. So rebuild the projection matrix.
    buildProjMtx();
}

void ColoredWavesDemo::updateScene(float dt)
{
    mGfxStats->setVertexCount(mNumVertices);
    mGfxStats->setTriCount(mNumTriangles);
    mGfxStats->update(dt);

    // Get snapshot of input devices.
    gDInput->poll();

    // Check input.
    if( gDInput->keyDown(DIK_W) )     
        mCameraHeight   += 25.0f * dt;
    if( gDInput->keyDown(DIK_S) )     
        mCameraHeight   -= 25.0f * dt;

    // Divide to make mouse less sensitive. 
    mCameraRotationY += gDInput->mouseDX() / 100.0f;
    mCameraRadius    += gDInput->mouseDY() / 25.0f;

    // If we rotate over 360 degrees, just roll back to 0
    if( fabsf(mCameraRotationY) >= 2.0f * D3DX_PI ) 
        mCameraRotationY = 0.0f;

    // Don't let radius get too small.
    if( mCameraRadius < 5.0f )
        mCameraRadius = 5.0f;

    // Accumulate time for simulation. 
    mTime += dt;

    // The camera position/orientation relative to world space can 
    // change every frame based on input, so we need to rebuild the
    // view matrix every frame with the latest changes.
    buildViewMtx();
}

void ColoredWavesDemo::drawScene()
{
    // Clear the backbuffer and depth buffer.
    HR(gd3dDevice->Clear(0, 0, 
       D3DCLEAR_TARGET | D3DCLEAR_ZBUFFER, 
       0x00000000, 1.0f, 0));

    HR(gd3dDevice->BeginScene());

    // Let Direct3D know the vertex buffer, index buffer and vertex 
    // declaration we are using.
    HR(gd3dDevice->SetStreamSource(0, mVB, 0, sizeof(VertexPos)));
    HR(gd3dDevice->SetIndices(mIB));
    HR(gd3dDevice->SetVertexDeclaration(VertexPos::Decl));

    // Setup the rendering FX
    HR(mFX->SetTechnique(mhTech));
    HR(mFX->SetMatrix(mhWVP, &(mView*mProj)));
    HR(mFX->SetFloat(mhTime, mTime));

    // Begin passes.
    UINT numPasses = 0;
    HR(mFX->Begin(&numPasses, 0));
    for(UINT i = 0; i < numPasses; ++i)
    {
        HR(mFX->BeginPass(i));
        HR(gd3dDevice->DrawIndexedPrimitive
                (D3DPT_TRIANGLELIST, 
                 0, 0, mNumVertices, 0, mNumTriangles));
        HR(mFX->EndPass());
    }
    HR(mFX->End());

    
    mGfxStats->display();

    HR(gd3dDevice->EndScene());

    // Present the backbuffer.
    HR(gd3dDevice->Present(0, 0, 0, 0));
}
 
void ColoredWavesDemo::buildGeoBuffers()
{
    std::vector<d3dxvector3 /> verts;
    std::vector<dword /> indices;

    GenTriGrid(100, 100, 0.5f, 0.5f, 
        D3DXVECTOR3(0.0f, 0.0f, 0.0f), verts, indices);

    // Save vertex count and triangle count for DrawIndexedPrimitive arguments.
    mNumVertices  = 100*100;
    mNumTriangles = 99*99*2;

    // Obtain a pointer to a new vertex buffer.
    HR(gd3dDevice->CreateVertexBuffer(mNumVertices * sizeof(VertexPos), 
        D3DUSAGE_WRITEONLY,
    0, D3DPOOL_MANAGED, &mVB, 0));

    // Now lock it to obtain a pointer to its internal data, and write the
    // grid's vertex data.
    VertexPos* v = 0;
    HR(mVB->Lock(0, 0, (void**)&v, 0));

    for(DWORD i = 0; i < mNumVertices; ++i)
        v[i] = verts[i];

    HR(mVB->Unlock());


    // Obtain a pointer to a new index buffer.
    HR(gd3dDevice->CreateIndexBuffer(mNumTriangles*3*sizeof(WORD),   

                 D3DUSAGE_WRITEONLY,
                 D3DFMT_INDEX16, 
                 D3DPOOL_MANAGED, &mIB, 0));

    // Now lock it to obtain a pointer to its internal data, and write the
    // grid's index data.

    WORD* k = 0;
    HR(mIB->Lock(0, 0, (void**)&k, 0));

    for(DWORD i = 0; i < mNumTriangles*3; ++i)
        k[i] = (WORD)indices[i];

    HR(mIB->Unlock());
}

void ColoredWavesDemo::buildFX()
{
    // Create the FX from a .fx file.
    ID3DXBuffer* errors = 0;
    HR(D3DXCreateEffectFromFile(gd3dDevice, "heightcolor.fx", 
        0, 0, D3DXSHADER_DEBUG, 0, &mFX, &errors));
    if( errors )
        MessageBox(0, (char*)errors->GetBufferPointer(), 0, 0);

    // Obtain handles.
    mhTech = mFX->GetTechniqueByName("HeightColorTech");
    mhWVP  = mFX->GetParameterByName(0, "gWVP");
    mhTime = mFX->GetParameterByName(0, "gTime");
}

void ColoredWavesDemo::buildViewMtx()
{
    float x = mCameraRadius * cosf(mCameraRotationY);
    float z = mCameraRadius * sinf(mCameraRotationY);
    D3DXVECTOR3 pos(x, mCameraHeight, z);
    D3DXVECTOR3 target(0.0f, 0.0f, 0.0f);
    D3DXVECTOR3 up(0.0f, 1.0f, 0.0f);
    D3DXMatrixLookAtLH(&mView, &pos, &target, &up);
}

void ColoredWavesDemo::buildProjMtx()
{
    float w = (float)md3dPP.BackBufferWidth;
    float h = (float)md3dPP.BackBufferHeight;
    D3DXMatrixPerspectiveFovLH(&mProj, D3DX_PI * 0.25f, w/h, 1.0f, 5000.0f);
}

Triangles and Meshes

Below is an image of a mesh demonstration.

5.JPG

Here is the main source file:

// use W and S keys to alter the height of the application

#include "d3dApp.h"
#include "DirectInput.h"
#include <crtdbg.h>
#include "GfxStats.h"
#include <list>
#include "Vertex.h"

class MeshDemo : public D3DApp
{
public:
    MeshDemo(HINSTANCE hInstance,
             std::string winCaption, 
             D3DDEVTYPE devType, DWORD requestedVP);
    ~MeshDemo();

    bool checkDeviceCaps();
    void onLostDevice();
    void onResetDevice();
    void updateScene(float dt);
    void drawScene();

    // Helper methods
    void buildGeoBuffers();
    void buildFX();
    void buildViewMtx();
    void buildProjMtx();

    void drawCylinders();
    void drawSpheres();

private:
    GfxStats* mGfxStats;

    DWORD mNumGridVertices;
    DWORD mNumGridTriangles;

    ID3DXMesh* mCylinder;
    ID3DXMesh* mSphere;

    IDirect3DVertexBuffer9* mVB;
    IDirect3DIndexBuffer9*  mIB;
    ID3DXEffect*            mFX;
    D3DXHANDLE              mhTech;
    D3DXHANDLE              mhWVP;

    float mCameraRotationY;
    float mCameraRadius;
    float mCameraHeight;

    D3DXMATRIX mView;
    D3DXMATRIX mProj;
};


int WINAPI WinMain(HINSTANCE hInstance, HINSTANCE prevInstance,
                   PSTR cmdLine, int showCmd)
{
    // Enable run-time memory check for debug builds.
   #if defined(DEBUG) 
   | defined(_DEBUG)
   _CrtSetDbgFlag( _CRTDBG_ALLOC_MEM_DF 
   | _CRTDBG_LEAK_CHECK_DF );
    #endif

    MeshDemo app(hInstance, 
      "Mesh Demo", D3DDEVTYPE_HAL,   

      D3DCREATE_HARDWARE_VERTEXPROCESSING);
    gd3dApp = &app;

    DirectInput di(DISCL_NONEXCLUSIVE|DISCL_FOREGROUND,   
                   DISCL_NONEXCLUSIVE|DISCL_FOREGROUND);
    gDInput = &di;

    if(!gd3dApp->checkDeviceCaps())
        return 0;
    else
        return gd3dApp->run();
}

MeshDemo::MeshDemo(HINSTANCE hInstance, 
          std::string winCaption, 
          D3DDEVTYPE devType, DWORD requestedVP)
        : D3DApp(hInstance, winCaption, devType, requestedVP)
{
    if(!checkDeviceCaps())
    {
        MessageBox(0, "checkDeviceCaps() Failed", 0, 0);
        PostQuitMessage(0);
    }

    mGfxStats = new GfxStats();

    mCameraRadius    = 10.0f;
    mCameraRotationY = 1.2 * D3DX_PI;
    mCameraHeight    = 5.0f;

    HR(D3DXCreateCylinder(gd3dDevice, 1.0f, 1.0f, 6.0f, 20, 20, &mCylinder, 0));
    HR(D3DXCreateSphere(gd3dDevice, 1.0f, 20, 20, &mSphere, 0));

    buildGeoBuffers();
    buildFX();

    // If you look at the drawCylinders and drawSpheres functions, you see
    // that we draw 14 cylinders and 14 spheres.
    int numCylVerts    = mCylinder->GetNumVertices() * 14;
    int numSphereVerts = mSphere->GetNumVertices()   * 14;
    int numCylTris     = mCylinder->GetNumFaces()    * 14;
    int numSphereTris  = mSphere->GetNumFaces()      * 14;

    mGfxStats->addVertices(mNumGridVertices);
    mGfxStats->addVertices(numCylVerts);
    mGfxStats->addVertices(numSphereVerts);
    mGfxStats->addTriangles(mNumGridTriangles);
    mGfxStats->addTriangles(numCylTris);
    mGfxStats->addTriangles(numSphereTris);

    onResetDevice();

    InitAllVertexDeclarations();
}

MeshDemo::~MeshDemo()
{
    delete mGfxStats;
    ReleaseCOM(mVB);
    ReleaseCOM(mIB);
    ReleaseCOM(mFX);
    ReleaseCOM(mCylinder);
    ReleaseCOM(mSphere);

    DestroyAllVertexDeclarations();
}

bool MeshDemo::checkDeviceCaps()
{
    D3DCAPS9 caps;
    HR(gd3dDevice->GetDeviceCaps(&caps));

    // Check for vertex shader version 2.0 support.
    if( caps.VertexShaderVersion < D3DVS_VERSION(2, 0) )
        return false;

    // Check for pixel shader version 2.0 support.
    if( caps.PixelShaderVersion < D3DPS_VERSION(2, 0) )
        return false;

    return true;
}

void MeshDemo::onLostDevice()
{
    mGfxStats->onLostDevice();
    HR(mFX->OnLostDevice());
}

void MeshDemo::onResetDevice()
{
    mGfxStats->onResetDevice();
    HR(mFX->OnResetDevice());

    // The aspect ratio depends on the backbuffer dimensions, which can 
    // possibly change after a reset. So rebuild the projection matrix.
    buildProjMtx();
}

void MeshDemo::updateScene(float dt)
{
    mGfxStats->update(dt);

    // Get snapshot of input devices.
    gDInput->poll();

    // Check input.
    if( gDInput->keyDown(DIK_W) )     
        mCameraHeight   += 25.0f * dt;
    if( gDInput->keyDown(DIK_S) )     
        mCameraHeight   -= 25.0f * dt;

    // Divide to make mouse less sensitive. 
    mCameraRotationY += gDInput->mouseDX() / 100.0f;
    mCameraRadius    += gDInput->mouseDY() / 25.0f;

    // If we rotate over 360 degrees, just roll back to 0
    if( fabsf(mCameraRotationY) >= 2.0f * D3DX_PI ) 
        mCameraRotationY = 0.0f;

    // Don't let radius get too small.
    if( mCameraRadius < 5.0f )
        mCameraRadius = 5.0f;

    // The camera position/orientation relative to world space can 
    // change every frame based on input, so we need to rebuild the
    // view matrix every frame with the latest changes.
    buildViewMtx();
}


void MeshDemo::drawScene()
{
    // Clear the backbuffer and depth buffer.
    HR(gd3dDevice->Clear(0, 0,
        D3DCLEAR_TARGET 
        | D3DCLEAR_ZBUFFER, 0xffff0000, 1.0f, 0));

    HR(gd3dDevice->BeginScene());

    // Let Direct3D know the vertex buffer, index buffer and vertex 
    // declaration we are using.
    HR(gd3dDevice->SetStreamSource(0, mVB, 0, sizeof(VertexPos)));
    HR(gd3dDevice->SetIndices(mIB));
    HR(gd3dDevice->SetVertexDeclaration(VertexPos::Decl));

    // Setup the rendering FX
    HR(mFX->SetTechnique(mhTech));
    

    // Begin passes.
    UINT numPasses = 0;
    HR(mFX->Begin(&numPasses, 0));
    for(UINT i = 0; i < numPasses; ++i)
    {
        HR(mFX->BeginPass(i));

        HR(mFX->SetMatrix(mhWVP, &(mView*mProj)));
        HR(mFX->CommitChanges());
        HR(gd3dDevice->
                   DrawIndexedPrimitive(D3DPT_TRIANGLELIST,
                    0, 0, mNumGridVertices, 
                    0, mNumGridTriangles));
        
        drawCylinders();
        drawSpheres();

        HR(mFX->EndPass());
    }
    HR(mFX->End());

    
    mGfxStats->display();

    HR(gd3dDevice->EndScene());

    // Present the backbuffer.
    HR(gd3dDevice->Present(0, 0, 0, 0));
}
 
void MeshDemo::buildGeoBuffers()
{
    std::vector<d3dxvector3 /> verts;
    std::vector<dword /> indices;

    GenTriGrid(100, 100, 1.0f, 1.0f, 
        D3DXVECTOR3(0.0f, 0.0f, 0.0f), verts, indices);

    // Save vertex count and triangle count for DrawIndexedPrimitive arguments.
    mNumGridVertices  = 100*100;
    mNumGridTriangles = 99*99*2;

    // Obtain a pointer to a new vertex buffer.
    HR(gd3dDevice->CreateVertexBuffer(mNumGridVertices * sizeof(VertexPos), 
        D3DUSAGE_WRITEONLY,
                 0, D3DPOOL_MANAGED, &mVB, 0));

    // Now lock it to obtain a pointer to its internal data, and write the
    // grid's vertex data.
    VertexPos* v = 0;
    HR(mVB->Lock(0, 0, (void**)&v, 0));

    for(DWORD i = 0; i < mNumGridVertices; ++i)
        v[i] = verts[i];

    HR(mVB->Unlock());


    // Obtain a pointer to a new index buffer.
    HR(gd3dDevice->CreateIndexBuffer(mNumGridTriangles*3*sizeof(WORD), 
       D3DUSAGE_WRITEONLY, D3DFMT_INDEX16, D3DPOOL_MANAGED, &mIB, 0));

    // Now lock it to obtain a pointer to its internal data, and write the
    // grid's index data.

    WORD* k = 0;
    HR(mIB->Lock(0, 0, (void**)&k, 0));

    for(DWORD i = 0; i < mNumGridTriangles*3; ++i)
        k[i] = (WORD)indices[i];

    HR(mIB->Unlock());
}

void MeshDemo::buildFX()
{
    // Create the FX from a .fx file.
    ID3DXBuffer* errors = 0;
    HR(D3DXCreateEffectFromFile(gd3dDevice, "transform.fx", 
        0, 0, D3DXSHADER_DEBUG, 0, &mFX, &errors));
    if( errors )
        MessageBox(0, (char*)errors->GetBufferPointer(), 0, 0);

    // Obtain handles.
    mhTech = mFX->GetTechniqueByName("TransformTech");
    mhWVP  = mFX->GetParameterByName(0, "gWVP");
}

void MeshDemo::buildViewMtx()
{
    float x = mCameraRadius * cosf(mCameraRotationY);
    float z = mCameraRadius * sinf(mCameraRotationY);
    D3DXVECTOR3 pos(x, mCameraHeight, z);
    D3DXVECTOR3 target(0.0f, 0.0f, 0.0f);
    D3DXVECTOR3 up(0.0f, 1.0f, 0.0f);
    D3DXMatrixLookAtLH(&mView, &pos, &target, &up);
}

void MeshDemo::buildProjMtx()
{
    float w = (float)md3dPP.BackBufferWidth;
    float h = (float)md3dPP.BackBufferHeight;
    D3DXMatrixPerspectiveFovLH(&mProj, D3DX_PI * 0.25f, w/h, 1.0f, 5000.0f);
}

void MeshDemo::drawCylinders()
{
    D3DXMATRIX T, R;

    D3DXMatrixRotationX(&R, D3DX_PI*0.5f);

    for(int z = -30; z <= 30; z+= 10)
    {
        D3DXMatrixTranslation(&T, -10.0f, 3.0f, (float)z);
        HR(mFX->SetMatrix(mhWVP, &(R*T*mView*mProj)));
        HR(mFX->CommitChanges());
        HR(mCylinder->DrawSubset(0));

        D3DXMatrixTranslation(&T, 10.0f, 3.0f, (float)z);
        HR(mFX->SetMatrix(mhWVP, &(R*T*mView*mProj)));
        HR(mFX->CommitChanges());
        HR(mCylinder->DrawSubset(0));
    }
}

void MeshDemo::drawSpheres()
{
    D3DXMATRIX T;

    for(int z = -30; z <= 30; z+= 10)
    {
        D3DXMatrixTranslation(&T, -10.0f, 7.5f, (float)z);
        HR(mFX->SetMatrix(mhWVP, &(T*mView*mProj)));
        HR(mFX->CommitChanges());
        HR(mSphere->DrawSubset(0));

        D3DXMatrixTranslation(&T, 10.0f, 7.5f, (float)z);
        HR(mFX->SetMatrix(mhWVP, &(T*mView*mProj)));
        HR(mFX->CommitChanges());
        HR(mSphere->DrawSubset(0));
    }
}

Download the MeshDemo.zip file located at the top of this article. Again, extract the files into a newly-made subfolder in your Visual Studio Projects directory. Note that this source contains header files from the DirectX 9 edition. So now we should be getting the idea behind modeling a scene. All of the objects that comprise that scene are small triangles. Each object was designed in a local coordinate system, and then placed in a world coordinate system where its vertices had to positioned, oriented, and scaled through a matrix transformation process. Upon creating a window and showing that window, we create a Direct3D object, which creates a Direct3D device. Now let's look at a deeper, more complicated topic.

Particle Systems

When you download the DirectX SDK, you will get a sample browser that includes many advanced DirectX examples, some of which use "shaders". We will not touch on shaders in this article, but there is plenty of information out there on them. Suffice it to know that a shader file normally has an .fx file extension, and the file itself contains a data structure. Particle Systems, however, require a deeper understanding. Examine the image below:

fires.JPG

Download the fires.zip file and run it. The flames flicker, as the waves sparkle and the text flickers, due to the nature of the DirectX technology, and in particular, the vertex buffer. A particle is a very small object that is usually modeled as a point mathematically. It follows then that a point primitive (D3DPT_POINTLIST of D3DPRIMITIVETYPE) would be a good candidate to display particles. However, point primitives are rasterized as a single pixel. This does not give us much flexibility, as we would like to have particles of various sizes and even map entire textures to these particles.

When studying flames, begin by examining how particles are created for this system, and then look at the effect file.

References

  • The MSDN Library
  • Introduction to 3D Game Programming, written by Frank Luna

License

This article has no explicit license attached to it but may contain usage terms in the article text or the download files themselves. If in doubt please contact the author via the discussion board below.

A list of licenses authors might use can be found here