And COM is what?
COM stands for Component Object Model. COM is a method of creating very advanced objects that, well, they act a lot like Legos actually.
Legos, as you know, can be stuck together to create more advanced shapes. No single Lego actually cares about any other Lego in the set. They are all compatible with each other, and all you have to do is stick them together to get them to work. If you want to change pieces, all you have to do is unplug one piece and put another in its place.
And so it is with COM. COM objects are actually C++ classes or groups of classes from which you can call functions and achieve certain aims. No class requires another to operate, and they don't really need to work together to get things done together, but you can plug them in or unplug them as you desire without changing the rest of the program also.
For example, say you had a game distributed broadly and you want to upgrade it. Well, instead of keeping track of and shipping a new copy to every single user who ever bought your game, all you have to do is say "Upgrade! Right Here!". They download the updated COM object, and the new object plugs right in to your program without further hassle. Nice, huh?
I won't get too detailed into COM, because it is far too complex for what we need. It's job is to get all the complex stuff out of the way so that you have an easy time. And if that's its job, what would be the purpose of learning all that complex material?
So why COM? Well, DirectX is actually a series of COM objects, one of which is Direct3D. Direct3D is a rather advanced class that contains everything you need to run 2D and 3D graphics using software, hardware, or whateverware.
So don't be surprised when you see Direct3D functions being called like this:
We use the indirect member access operator here to access the functions CreateDevice() and Release() from the Direct3D interface class. We'll get more into this when we see how it is applied in practice. I'm going to try to avoid unneeded theory from here on out.
For now let's go on to the next topic:
A graphics adapter contains in its memory a pointer to a buffer of pixels that contains the image currently being displayed on the screen. When you need to render something, such as a 3D model or texture, the graphics adapter updates this array and sends the information to the monitor to display. The monitor then redraws the screen from top to bottom, adding in the new part rendered.
However, there is a slight problem with this in that the monitor does not refresh as fast as needed for real-time rendering. Most refresh rates range from 60 Hz (60 fps) to about 100 Hz. If another model were rendered to the graphics adapter while the monitor was refreshing, the image displayed would be cut in two, the top half containing the old image and the bottom half containing the new. This effect is called tearing.
To avoid this, DirectX implements a feature called swapping.
Instead of rendering new images directly to the monitor, Direct3D draws your images onto a secondary buffer of pixels, known as a back buffer. The front buffer would be the buffer currently being displayed. You draw all your images onto the back buffer, and when you are done, Direct3D will update the front buffer with the contents of the back buffer, discarding the old image.
However, doing this can still cause tearing, because the image transfer can still occur while the monitor is refreshing (the CPU is faster than the monitor).
In order to avoid this (and to make the whole thing go much faster), DirectX uses a pointer for each buffer (both front and back) and simply switches their values. The back buffer then becomes the front buffer (and vice versa), and no tearing occurs.
Addresses Swap Instantly
Of course, we could make our game have better performance by adding additional back buffers, like this.
Multiple Back Buffers Gets Better Performance
How can adding additional back buffers get better performance? Well, let's say that every once in a while you finish rendering the back buffer and are ready to swap, but the screen hasn't yet finished drawing the contents of the front buffer. Swapping now would cause more tearing. So what happens instead is your program stops and waits for the screen to finish. You could, of course, be spending this valuable time preparing the next image, and having multiple back buffers allows your program to do just that.
This setup called a swap chain, as it is a chain of buffers, swapping positions each time a new frame is rendered.
We won't be making a "Hello World!" application for Direct3D. We will assume that Direct3D is not a language on its own (which it isn't). Instead, we will start by filling our window with blue. For this we have 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
We need to add a few things to the top of our program before we start working with Direct3D in the first place. Let's take a look at these and see what they are.
// include the basic windows header files and the Direct3D header file
// 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);
This includes the Direct3D 9 header file. This consists of various declarations to the actual methods contained in the Direct3D library.
This includes the Direct3D 9 library file. The #pragma comment directive places a certain piece of information in your project's object file. With our first paramter, lib, we indicate that we want to add a library file to the project. We then specify which file, "d3d9.lib".
This variable is a long pointer to Direct3D. What this means is that we will create a class called iDirect3D9. When COM makes this class, we will ignore it, and access it only indirectly using this pointer. We will cover how the class is created in a moment.
The Direct3D Device interface holds all the information pertaining to the graphics drivers, the video card, and everything else having to do with the hardware side of graphics. This is a pointer to the class that stores all this information.
The first step to actually coding Direct3D is to create the interface and initialize the graphics device. This is done using two functions and a struct containing graphics device information. Let's take a look at this function here, then go over its parts. I didn't bother to bold the new parts, because the entire thing is new.
// 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
If the comments here are good enough for you, excellent. Otherwise, I've described each of these commands below.
This is where the fun begins. This is the first Direct3D function you will run. It's primary purpose is to create the Direct3D interface. The return value is the address of the interface created, and so we will store this address in the pointer we created earlier, d3d.
Then there is the parameter. This parameter is always the same: D3D_SDK_VERSION. Why is this? Well, it really only matters for compatibility on other machines. When a later release of DirectX is published, the users will usually have varying levels of DirectX upgrades. This tells the user's DirectX which version you developed your game for. The user's DirectX can then look back and properly execute your program without implementing the upgrades that have occurred since then. Kinda useful, eh?
In the version of Direct3D 9c, this value returns 32. Previous versions would return different values, causing DirectX to function in different ways. Note that you should not change this value, as it will cause confusion and is not really needed anyway. Just use D3D_SDK_VERSION, and everything will work out all right.
There are certain factors involved in both beginning and advanced game programming which require certain information to be fed into the graphics device from the start. There are plenty of these, but we will only go into three of them here. For now, D3DPRESENT_PARAMETERS is a struct whose members will contain information about the graphics device. We will go over the ones we use here and cover new members as they come up throughout the tutorial.
We use ZeroMemory() to quickly initialize the entire d3dpp struct to NULL. That way we don't have to go through every member of the struct and set them individually.
When we run Direct3D in a window like we are now, this value is set to TRUE. Otherwise, it is set to false. Later, when we make our window full-screen, we will change this value to FALSE.
Note: There are other changes to make when an application goes full-screen. Changing this one value will not make your application full-screen, unfortunately. Patience until the next lesson.
Before, we talked about swap chains as the method Direct3D uses to change images on the screen. However, we get to pick which type of swap chain is used. There are three different kinds.
|D3DSWAPEFFECT_DISCARD||This type of swap chain is used to get the best speed possible. However, if you later want to look at the previous back buffer (which can be useful for various effects), you cannot guarantee the image will still be intact.|
|D3DSWAPEFFECT_FLIP||This type is similar to discarding, but is reasonably slower, because it has to take the time to ensure your previous back buffer(s) are protected and unchanged.|
|D3DSWAPEFFECT_COPY||This last type is the one I least recommend. Instead of switching pointers like the other two, this method copies the image, pixel by pixel, from the back buffer to the front buffer. This is not prefered, although required for some advanced techniques.|
So which one to use? Throughout this tutorial I will be using D3DSWAPEFFECT_DISCARD. The only exception to this will be at some later date when I add additional tutorials on various effects that use the other two methods.
This value sets the handle to the window Direct3D should use. We'll just use the same hWnd we've always been using.
This is a big function, but in actual fact, it is quite simple. Most of the parameters will probably stay the same in every game you write.
What this function does is create a graphics device interface. What this means is a new class will be created from which you will handle all the graphics you need. Notice that this function is part of the d3d class. Most of the functions you use in Direct3D will come from here, so we give the pointer a rather simple name, d3ddev, as seen before.
Here is this function's prototype:
Now let's go into the parameters of this function. They are all simple, so this will go fast.
This is an unsigned integer that stores a value indicating what graphics adapter, or video card, Direct3D should use. We could get detailed here, and try to find the better graphics card, but we'll let Direct3D take care of that for us (because in most cases there's only one). To tell Direct3D that it needs to decide, we put into this parameter the value D3DADAPTER_DEFAULT, indicating the default graphics card.
While there are four possible values for this parameter, we are only going to be concerned with one of them, and stay away from the others until later. The one we will use is D3DDEVTYPE_HAL.
D3DDEVTYPE_HAL tells Direct3D to use what is called the Hardware Abstraction Layer. The Hardware Abstraction Layer, or HAL, is used to indicate that Direct3D should be using hardware to process graphics (and we would use...what else?). If for some reason our graphics device cannot use hardware to handle something, that something will be rendered by software instead. This is done automatically, but probably won't be done, considering the capabilities of modern cards.
This is the handle to our window. We can just put 'hWnd' in here as we passed the value from WinMain().
While there are plenty of values we can store in this parameter, there are only three we will cover just now. They are D3DCREATE_SOFTWARE_VERTEXPROCESSING, D3DCREATE_MIXED_VERTEXPROCESSING and D3DCREATE_HARDWARE_VERTEXPROCESSING. These three values are fairly self-explanatory. The software one indicates that all 3D calculations should be done with software, the hardware one with hardware, and the mixed one using a combination of both, depending on which Direct3D sees fit. We will use the software value for compatibility, but you can experiment with these as you desire. At first you will notice no difference, but as we move into advanced topics, you may start to see some differences appear.
This is a pointer to the d3dpp struct that we filled out earlier. We just fill this with '&d3dpp'.
This is a pointer to a pointer to the graphics device interface (if you can understand that). Earlier in the program we defined this pointer as d3ddev, so we will put '&d3ddev' in this parameter.
And that's it! You have an entire function using Direct3D. However, if you run this program (and you should not), you will find that absolutely nothing different occurs. All we have done at this point is prepare for action. Now let's have some action!
In this function we will render a single frame. The frame will be rather simple, and will consist of a blue background. Of course, you can change the color if you want. Here is the code for this function.
// this is the function used to render a single frame
// 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
There are four functions to rendering a frame, two of them big (but simple) and two of them small. We'll go over them here.
This clears a buffer to a specific color. In this case, we are going to clear our back buffer. It's pretty simple.
Because most of the parameters are irrelevant at this point, we will leave most of them alone. The first two have to do with clearing a specific area. You will rarely need this, but will just leave them at 0 and NULL, indicating the entire back buffer is to be cleared. The third parameter, set to D3DCLEAR_TARGET, indicates that we should clear the back buffer. There are other types of buffers, but we want this type. Next we have color. The parameter is the struct D3DCOLOR, and we will use the function D3DCOLOR_XRGB() to build the color. The next two parameters we will get into in later lessons. For now, just set them to '1.0f' and 0.
Next we call the function BeginScene() which tells Direct3D you are ready to start rendering. This function needs to be called for two reasons. First, you need to tell Direct3D that you are in control of the memory. Second, this function does something called locking, where the buffer in the video RAM is 'locked', granting you exclusive access to this memory. There is no guarantee that your memory will stay put and not be moved around while you are trying to work on it. Having your memory suddenly appear somewhere else can be quite a hassle for sure.
Next we call the function EndScene(). While BeginScene() locked the video memory we wanted, EndScene() unlocks it, making it available by other processes that need access to it.
Note: Locking video RAM is slow, but required. You should call BeginScene() and EndScene() only once per frame to save your valuable CPU ticks.
And finally we call the Present() function. The four parameters, all set to NULL, are not going to be used in this tutorial. Three of them have to do with the slowest method of handling the swap chain (which we don't use) and the other one has to do with presenting to a different window (which we won't need and won't go into).
This is the last (and easiest) step to creating a Direct3D program. We just have two steps here:
// this is the function that cleans up Direct3D and COM
d3ddev->Release(); // close and release the 3D device
d3d->Release(); // close and release Direct3D
Here, we call the Release() function from each of the two interfaces we created, d3ddev and d3d. No parameters, nothing spectacular. Just cleans everything up.
Why? Well, let's say it would be a bad thing to do otherwise. Basically, if you create Direct3D, but never close it, it will just keep on running in the background of the computer until your next reboot, even after the program itself closes. Bad. Especially bad if you have a lot of resources in your game. Releasing these two interfaces let's everything off the hook and allows Windows to take back it's memory.