Quantcast
Channel: Greg Dolley's Weblog » DirectX
Viewing all articles
Browse latest Browse all 7

DirectX 9 C++ Graphics Tutorial 1: Getting Started

$
0
0

In this post I’m going to cover writing the most basic DirectX 9 application in native C++. You’ll see how to paint a solid color on a form as well as the minimum amount of code all DirectX 9 applications must have. Note: when I refer to DirectX, unless otherwise specified, I’m strictly referring to the graphics portion of it – Direct3D.

This post is going to be one of a series. I’m going to cover writing simple DirectX 9 applications in C++ and I’ll also cover how to write those same applications with Managed DirectX  (MDX 1.1) in C#. These posts will alternate – one in C++, one in C#, back to C++, and so on. After this, I’ll eventually cover some DirectX 10 stuff in C++ (which can be vastly different from DirectX 9 in certain areas). There is no MDX equivalent of the C++ DX10 API – the XNA Framework is meant to replace MDX since Microsoft dropped support for the MDX 2.0 Beta project. However, I’m not sure if XNA is mature enough yet to be able to do all of things originally included in MDX 2.0. I’ll explore that issue in a future post.

Requirements

The steps and screenshots in this tutorial are based on Visual Studio 2008 Express. However, everything was also tested in Visual Studio 2005 Professional.

You’ll need the DirectX SDK to run the sample programs. You can download it from Microsoft’s site with this link. Note, however, that their latest release (which is November 2007 at the time of this writing) took out the MDX 1.1 samples and documentation. If you still want those, download the August 2007 release or earlier (I use the June release).

Tutorial Source and Project Files

To get the finished result of what this tutorial teaches – project files, binaries, and source – use this link:

If you follow this tutorial’s instructions exactly as specified, you should end up with the same code and output. However, copying and pasting from a web page to Visual Studio sometimes results in double spaces between lines where it wasn’t intended. In almost all cases C++ doesn’t care and your code will still compile perfectly fine (a multi-line macro is one of those rare exceptions).

Setting Up the Project

Start out by performing the following steps:

Create an empty project and add one source file called “main.cpp” (you can actually name it anything you want, but I’m going to refer to this file as “main.cpp” throughout the text).

Go into the Project Properties dialog and add the following include directory, “$(DXSDK_DIR)\Include”:

directx_tutorial_1_cpp_include_dirs

Add the following preprocessor directives, “WIN32;_DEBUG;_WINDOWS”:

directx_tutorial_1_cpp_preprocessor_directives

Add the following library dependency, “d3d9.lib”:

directx_tutorial_1_cpp_lib_dependencies

All the other defaults are fine. Save these settings and move on to the next step.

Creating the Form

Drop the following code snippet into your main.cpp file. This is the smallest amount of code that all C++ Windows form applications must have:

#include <windows.h>

 

LRESULT WINAPI WndProc(HWND hwnd, UINT msg, WPARAM wParam, LPARAM lParam);

 

int WINAPI WinMain(HINSTANCE hInstance, HINSTANCE hPrevInst, LPSTR lpCmdLine,

                   int nShow)
{
   MSG msg;


   WNDCLASSEX wc = {sizeof(WNDCLASSEX), CS_VREDRAW|CS_HREDRAW|CS_OWNDC,
                    WndProc, 0, 0, hInstance, NULL, NULL, (HBRUSH)(COLOR_WINDOW+1),
                    NULL, “DX9_TUTORIAL1_CLASS”, NULL};

   RegisterClassEx(&wc);

   HWND hMainWnd = CreateWindow(“DX9_TUTORIAL1_CLASS”,

                                “DirectX 9 Bare Bones Tutorial 1″,
                                WS_OVERLAPPEDWINDOW, 100, 100, 300, 300,

                                NULL, NULL, hInstance, NULL);
   ShowWindow(hMainWnd, nShow);
   UpdateWindow(hMainWnd);

   while(GetMessage(&msg, NULL, 0, 0))
   {
      TranslateMessage(&msg);
      DispatchMessage(&msg);
   }
  

   return(0);
}

 

LRESULT WINAPI WndProc(HWND hwnd, UINT msg, WPARAM wParam, LPARAM lParam)
{
   switch(msg)
   {
      case WM_DESTROY:
         PostQuitMessage(0);
         return(0);
   }

   return(DefWindowProc(hwnd, msg, wParam, lParam));
}

Since the purpose of this tutorial is to go over DirectX and not basic Win32 programming, I’m going to keep the following description of the code brief. There are many Win32 tutorials on the internet that explain in extreme detail the low-level aspects of Win32.

First we must define a WinMain() function and include <windows.h>. The windows.h header file includes most of the definitions needed to access the Win32 API. In order to run any Win32 program, the following three steps must be performed:

  1. Create a windows class.
  2. Create a window object.
  3. Show the window.

In addition to these steps, all Windows programs must have something called a “message pump.” This is the mechanism in which the Win32 subsystem sends your application events about what is happening on the outside (i.e., what the user is doing in your app, global events of the operating system – such as starting to reboot, etc.). Your application must either respond to these events or must pass them to the default Win32 event handler.

For the first step, create a Windows class – this doesn’t have anything to do with a C++ class. It is a Win32 specific term that tells Windows about your program. It needs some minimal information in order to setup your application to run in the Windows environment. It needs to know:

  1. How GDI should handle your application’s main window.
  2. A function pointer to your program’s message handler.
  3. The instance handle of your program.
  4. A background brush (this represents the background color, but technically it’s not required – your window will appear transparent if omitted).
  5. Class string (a unique string that identifies your Win32 class object to the rest of Windows).

The WNDCLASSEX structure is used to hold this information. While there are more than five members in WNDCLASSEX, the code above specifies NULL or “0″ for the properties in which Windows will take the default. The RegisterClassEx() function is used to send this information to the Win32 subsystem.

The next step is to create the actual window object. This object turns into the application’s main form. The CreateWindow() function, like its name implies, is used to create a window object. This function needs at least the following information:

  1. The same class string used when calling RegisterClassEx().
  2. The title of the form.
  3. The style of the form (dialog, tool window, etc.).
  4. The (x, y) position of the form and its size.
  5. The instance handle of your program (this should match the instance handle used when creating the class object).

As long as no error is generated, the CreateWindow() function returns a handle to your window. NULL will be returned if an error occurred. The next step is take that window handle and call ShowWindow(). The first parameter specifies the window handle and the second parameter represents how the window is to be shown by default. We used the “nShow” variable as the second parameter – the forth argument to WinMain(). This allows Windows to tell us how the window should be shown based on how the application was launched. The next line calls UpdateWindow(), which is technically not required by a program this simple. It allows any post-updates to occur after the window is first drawn.

We now get to the while() loop at the end of the main function. This loop is part of the “message pump” discussed earlier. If this loop wasn’t there your application would simply start and stop right away. There needs to be something that keeps your program running until it’s time to close. The “message pump” loop does exactly this. It continually checks for events from the Win32 subsystem by calling GetMessage() and subsequently passes them down to TranslateMessage() and DispatchMessage(). These two functions eventually route the events to WndProc() – your callback function specified in the windows class object. This function allows your program to execute whatever code is necessary in response to these events. If your application doesn’t need to respond to an event, it should pass it down to the default Win32 handler by calling DefWindowProc().

Creating the DirectX Object and Device

Now we must create two objects: the main DirectX object that allows us to interface with the API and the “device” object which allows interaction with the 3D hardware. Begin doing this, you’ll need to include the Direct3D header file and declare a couple global pointers to the DirectX object and device object:

#include <d3d9.h>

 

// globals
LPDIRECT3D9       g_pDirect3D = NULL;
LPDIRECT3DDEVICE9 g_pDirect3D_Device = NULL;

In order to create the DirectX object and device object, we add the following code directly under the call to CreateWindow() right above ShowWindow():

g_pDirect3D = Direct3DCreate9(D3D_SDK_VERSION);

D3DPRESENT_PARAMETERS PresentParams;
memset(&PresentParams, 0, sizeof(D3DPRESENT_PARAMETERS));

PresentParams.Windowed = TRUE;
PresentParams.SwapEffect = D3DSWAPEFFECT_DISCARD;

g_pDirect3D->CreateDevice(D3DADAPTER_DEFAULT, D3DDEVTYPE_HAL, hMainWnd,

                          D3DCREATE_SOFTWARE_VERTEXPROCESSING, &PresentParams,

                          &g_pDirect3D_Device);

Let’s go through this line by line. First we create the DirectX object by calling Direct3DCreate9(). The first parameter, D3D_SDK_VERSION, is a special number that ensures an application was built against the correct header files. If the number doesn’t match, the call will fail.

Next we declare and fill in a D3DPRESENT_PARAMETERS structure. This object holds properties about how an application should behave. In our sample, we want it to run in windowed mode and we want the video hardware to handle the back buffer instead of handling it ourselves (the full meaning of D3DSWAPEFFECT_DISCARD is beyond the scope of this tutorial, but basically it tells the device driver to select the most efficient way of handling swap chains).

We then use the main DirectX object to call CreateDevice(). This function does the work of allocating the device object and associating that object with the 3D hardware (or software – depending on what parameters are specified). The first parameter, D3DADAPTER_DEFAULT, tells DirectX to take whatever graphics card is the default (typically the one and only piece of active video hardware connected to your motherboard). The second parameter, D3DDEVTYPE_HAL, tells DirectX to use 3D hardware, instead of a software emulator, for rendering. A software emulator becomes useful only for low-level debugging – don’t worry about it for now. The third parameter specifies the window handle that will receive the rendered output. D3DCREATE_SOFTWARE_VERTEXPROCESSING tells DirectX to use software for transformation calculations instead of hardware HTL (Hardware Transform & Lighting). If you had specified hardware transformation calculations instead, and the video card didn’t support HTL, then this function would fail. All new cards on the market today support HTL, and most have for quite a while. So unless your card is really old you can select hardware processing without any worries – just replace the existing constant with D3DCREATE_HARDWARE_VERTEXPROCESSING. The fifth parameter is a pointer to the D3DPRESENT_PARAMETERS object that we filled in right above the call. Lastly, the sixth parameter holds a pointer to the device object that will be created and returned by the function.

Now that the device object is created, we can call its functions to control the video card! The next section will explain exactly how to do this.

Draw on the Window

In this section we’ll see where the magic happens. While it’s not much – drawing a solid color in the window – you’ll see how to do more advanced stuff in the next few tutorials.

Before we begin, we must first handle the WM_PAINT event inside our WndProc() message handler function. The WM_PAINT event is fired whenever the interior of a window needs its contents redrawn. We capture the event by enumerating the WM_PAINT constant inside WndProc’s switch statement:

switch(msg)
{
   case WM_DESTROY:
      PostQuitMessage(0);
      return(0);
   case WM_PAINT: // <— ADD THIS BLOCK
      // drawing code goes here…
      ValidateRect(hwnd, NULL);
      return(0);
}

ValidateRect() is called in order to tell Win32 that we’ve handled all of the rendering ourselves.

Now for the fun part – filling in the drawing code. Add the following two lines directly below the “case WM_PAINT” statement (where it says, “drawing code goes here…”):

g_pDirect3D_Device->Clear(0, NULL, D3DCLEAR_TARGET, D3DCOLOR_XRGB(0, 0, 255),

                          1.0f, 0);
g_pDirect3D_Device->Present(NULL, NULL, NULL, NULL);

The first call to Clear() tells DirectX to fill a specific rectangle, or set of rectangles, with a certain color. The first two parameters represent the number of rectangles and the address of where those rectangles are stored. If “0″ and “NULL” are specified instead, DirectX will fill the entire rendering target. The third parameter, D3DCLEAR_TARGET, means we want to clear the actual pixel buffer as opposed to the depth buffer or stencil buffer. This parameter could also be used to clear a combination of those buffers at the same time. The last three parameters tell DirectX what values to use for clearing the pixel buffer, depth buffer, and stencil buffer. Since we’re not clearing the depth buffer or stencil buffer, the values for the last two parameters have no effect.

Once everything has been rendered to the back buffer you display it on the screen by calling Present(). Parameters one and two represent the source and destination rectangles, respectively. Since we want the entire back buffer to get copied to the entire screen, we pass in NULL. The third parameter specifies which window to use for the display. When this parameter is NULL, Present() looks at what was originally set when creating the D3DPRESENT_PARAMETERS object (specifically the hDeviceWindow member). Since we didn’t specify anything in the beginning of our program, the memset() call would have set this member to NULL. When this happens, and windowed mode is set, Present() takes the focus window as its destination.

Shutting Down

We allocated two DirectX objects in our program – the main interface object and the device object. These objects must eventually be freed. Since we aren’t doing anything fancy that requires destruction and reallocation of these objects, we can simply put the clean up code right above the “return” statement in WinMain():

g_pDirect3D_Device->Release();
g_pDirect3D->Release();

We call Release() because these objects were given to us by DirectX via COM as opposed to being allocated with the “new” keyword or created with some memory allocation function. Therefore we must delete them through COM’s Release() function.

Run the Program!

You can now compile and run the program. Here’s what the output looks like:

 directx_tutorial_1_cpp_output

OK, it’s not much at this point, but in the next few tutorials we’ll get into more exciting things.

Conclusion

OK, I admit, the final output of this tutorial wasn’t too exciting, but it only strived to cover the absolute minimum – nothing more, nothing less. All 3D DirectX applications start off this way. If someone made a DirectX project template for Visual Studio, the wizard would more than likely generate very similar code to what you see here.

In the next tutorial, we’ll see how to do the same thing in managed DirectX (MDX 1.1).

-Greg Dolley

*Get new posts automatically! Grab the RSS feed here. Want email updates instead? Click here.



Viewing all articles
Browse latest Browse all 7

Trending Articles