[ 32bits.co.uk ] Coding Direct3D for real people

32Bits - Coding for real people!

.:[ Please always use http://www.32bits.co.uk to access the site. Thanks! ]:.

32Bits main page Introduction to the site The tutorials index Our releases The license for content on this site Public forum for questions and feedback We provide professional services Contact us

 

Last updated: 10:55 AM Monday October 2008

welcome.gif (1805 bytes)

So, you made it through part 1 of this tutorial, and you're on the path to becoming a code ninja. But this is part 2, where the unwary fall, and the impure of heart give up and use Visual Basic. This is where we start DirectX.

When you started this tutorial, you hopefully read all of the first page. If you didn't, go read it now. In that page is the following quote:

"Lets clear up some jargon. DirectX is the API - the application programming interface - that encompasses a range of technologies. It's the generic product name, in the same way that "Windows" or "MS Office" is a product name. We are using v8.1, however there is very little difference (that matters to us) between 8.0 and 8.1. Therefore, when you read "DirectX", "DirectX 8" or "DirectX 8.1", it all means the same thing. As we use the product name so much, it's become shortened in popular use to "DX". So, "DX" and "DX8" mean exactly the same as "DirectX 8.1". But, it gets worse. DirectX is actually the name for a group of technologies, just like "Office" represents a group of applications - Word, Excel, Powerpoint etc. Under the DirectX banner we have Direct3D (now known as DirectX Graphics), DirectSound, DirectInput, DirectShow, DirectPlay and DirectSetup."

It's time we expanded on that. There are a lot of different schools of thought on what DX "is", and how it measures up. You only need to flip through some of the forums on Flipcode, Gamedev or CfxWeb to see the controversy. The problem is, game & demo coders have big egos, and they like to shout their opinions from the rooftops to force other people to think like them. This is bad for one simple reason - people need to find out for themselves what they like or dislike. The main argument around DX is it's capability as a graphics API, and many HTML pages have been written discussing it versus OpenGL - it's main "competitor". There are other APIs in the wings such as GLIDE, but the signal to noise ratio mainly surrounds DX -v- OGL. An in depth review of the differences between the 2 APIs would take more than this single tutorial section to complete, and it's not something I will attempt to write up. The reason is simple - I looked at both OGL and DirectX, and I picked DirectX as my API of preference. If you are at this site and reading this tutorial, I hope you have either picked DirectX too, or you are reading to help you make your decision. If you are after a rundown of the differences & history of both DirectX and OGL, I suggest you read this article at GameDev. It's a moderate but well written discussion of DX -v- OGL, and whilst I disagree with some of the points raised (and the reviewer is OGL biased), it makes for good reading. In fact, I suggest you read it anyway, it does go through some of the basic points regarding DX.

Whilst the above quote has DX8 plastered all over it, the same problem applies to DX9 - in fact, every recent version of DX has been paired against it's OGL counterpart in the never ending api-war!


Before continuing, realise that DirectX is not just an uphill struggle to learn, it's a struggle against other people! During your learning phase, you will no doubt ask people for assistance - on irc, via email etc. As a lot of the game developer, demo and enthusiast community is OGL-biased, I can pretty much garantee your decision to learn DX will be questioned and laughed at (from an OGL -v- DX perspective). Ignore these people! In my (and a lot of other people's) eyes DirectX is equal to OGL in every way, and thats without taking into account the non-graphics aspects of DX. Ok, I keep going on about the components of DirectX, so lets discuss them. DirectX 8 is made up of the following:

Direct3D Also known as DirectX Graphics, this is your API for graphics coding
DirectSound Also known as DirectX Audio, this API provides for audio programming
DirectInput The API for abstracted input device programming - force feedback joysticks, mice, keyboard etc...
DirectPlay The API for multiplayer network games
DirectShow The API for streaming media (audio/video streams etc)
DirectSetup A one call API for easy installation of DirectX runtimes

 

As you can imagine, there is a large amount of code behind these API's. The 3 most useful ones are Direct3D (of course!), DirectInput and DirectShow, however in this tutorial series we'll be focusing on Direct3D. Out of interest, in case you are wondering why I don't rate DirectSound it's because there are better 3rd party shareware/freeware sound API's available such as fmod and Bass.

So lets talk about Direct3D. D3D is a HAL. That one simple abbreviation covers up the essence of DirectX, and it stands for Hardware Abstraction Layer. If you've not heard of this abbreviation before, consider this. This website is in english, and I am english. If I travelled to Germany, everybody would be speaking german and unable to understand me. To fix this, I can learn german, and can therefore communicate with the natives with ease. But what happens if I then go to Spain? I'll need to learn Spanish. But then I move onto Italy, and have to learn italian. This could go on and on - you get the picture. But what if there was a way round this? What if, just like in Star Trek, I could get a universal translator that would allow me to speak in english, but everyone else to hear it in their native language? Well, it would be impossible, but this is exactly what DirectX does. DirectX, combined with the drivers for the graphics card, allows you to address every single supported card with the same set of commands. That's not to say every card supports every feature, just the access mode is the same. This is unbelievably important - back in the DOS days when there was no uniform driver standard or HAL, if you wanted to support different types of graphics card you had to write the code yourself. And often you would need completely different code for different cards, a real problem for both development time and support. How does this work? As an overview it's simple, but the actual mechanisms and code behind the abstraction layer is extremely complex. But, because of the HAL we don't need to know about it!


Fig 2.1: Sort of what the HAL does


For the end-coders like us, it's as simple as that. We pump in a function call such as CreateImageSurface (which, unsurprising, creates a surface for an image by allocating memory) into D3D, the HAL translates it into custom code for whichever supported graphics card is currently in use, then returns us a sanitised pointer to the memory location of the surface we just created. It makes our life much easier, as we no longer need to worry about writing hardware-specific code as the HAL will translate one function call to work on all possible graphics cards. Well, thats true to an extent - we still have to consider what we will do with the graphics cards that don't support the features we require. D3D has an answer to that too, and will allow you to query it for specific features and find out if the currently used card supports them. Stunning stuff.

What happens with cards that don't support modes or functions we want to use? Well, we have the option of using the Software Reference Rasteriser. This is like a little graphics card that is 100% software based and internal to D3D. It supports absolutely every single function D3D does, so you can be garanteed you will see the same display on every system, regardless of graphics card. "Surely this can't work?", you think. You're nearly right. Because this adapter is implemented 100% in software it obviously can't have any of the hardware accelleration of modern graphics cards, and has to rely on processor and system memory to render graphics. This means that pretty much everything you do that uses the reference rasteriser runs as slow as a snail in treacle. Therefore if you are testing an effect for a card you don't have, or you'd like to see what people with better gfx cards can see, it's useful. For anything else, forget it.

The HAL is effectively the lowest layer of D3D. Above from that, closer to the interface we will use to code are 2 further "modes". These are "Immediate Mode" and "Retained Mode". Immediate mode is as close to actually talking to the graphics card as you can get, without bypassing the HAL. It's low level and extremely fast. Of course, this comes at a price. Equate Immediate Mode to programming in assembly language, where you must tell the hardware to do everything in explicit terms. If you use Immediate Mode you supply all the maths, all the data and all the little bits that glue it together in explicit detail. The advantage to this is that you get total control over what you draw to the screen. Retained Mode however is a totally different beast. It's a little bit like the Visual Basic of the D3D world, and Microsoft are doing their best to remove all references to it. In fact, if you look in the DX8 or DX9 SDK docs, you probably wont find any mention of it. Thats simply because it's crap. Like Visual Basic, it was created for rapid development of structured 3D applications, and unlike Visual Basic it's totally crap. It's slow, bloated, and full of stuff you'll never need. It is easy to use though, if you want to be very restricted in what you create. So, suffice to say, we'll be ignoring Retained Mode and focussing completely on Immediate Mode. Out of interest, Sol_HSA informs us that the last time Microsoft updated Retained mode was in DX6.

Lets take one more step back. Not content with making you worry about hardware abstraction layers and Immediate or Retained mode, Microsoft threw another obstacle in the way of the newbie D3D coder. It's called the Common Files Framework. Much like MFC is an Object Oriented wrapper around the Win32 API, the Framework is an OO wrapper around the D3D API. Unlike MFC, it's a pain in the ass and makes life much more complicated. Even worse, the examples that Microsoft so kindly supply as a tutorial with the DirectX SDK are all based on this Framework. So, my first piece of advice is to totally ignore it. By all means dip into the examples to find a specific piece of code, but don't ever try to implement your own code using the framework. It will cause you far more trouble than it's worth, not least because when you're learning it adds yet another layer of complexity to DX. Besides, as we progress through the tutorials we'll be implementing our own wrapper and class system which you will understand far better. Why? Because you'll be writing it!

So, we've got the HAL, Immediate -v- Retained mode and the Framework -v- the plain API. Guess what - there's more! If you've been reading through these tutorials and noticed a lot of "3D" related words, or you used DirectX7 and know all about DirectDraw and you're wondering where it's gone from the above table of API components, you're no doubt wondering about 2D in Direct3D. The bad news is, there is no single 2D API in DirectX that you can use for simple (or complex) 2D graphics. The good news is that it is very easy to implement 2D graphics code in D3D, and it's very fast. We'll be looking into 2D graphics first, simply because it's easier to pick up than 3D concepts, and a good understanding of 2D techniques in D3D will put you in a good position to begin learning the interesting 3D stuff!

COM. You've heard of it, the DirectX API is accessed via it, but do you understand it? COM is an abbreviation of Component Object Model, and is a clever way of allowing code written in one language to be accessed and re-used in another. It's a fundamental part of Windows, all those DLL files (and some EXE files) are all COM components, and because you will be using DirectX's COM interfaces to access the API, it's time we had a brief discussion. To be perfectly honest, there are 2 ways to approach COM. The first way is to learn absolutely everything about it, understand it to it's lowest level, then leave your home wondering what happened to the last 10 years. The second way is to understand what it does, understand how to use it and how it benefits us, and then smile and nod whenever you see any further talk of COM. We'll be taking the second option. So what is COM, and what does it do? COM is a lot like the C++ class system, and in fact it is "binary compatible" with C++ classes. This means you can use COM objects just like you'd use C++ classes. A COM object is structured in a tiered system. At the top you have the object itself - OurApp. Below that you have an OurApp Interface, called IOurApp1. That interface publishes a method GetAppInfo(), which you access via a pointer to the interface. Think of an interface as a logical collection of similar methods. For example, if we created an IMaths interface, it may publish Add(), Subtract() and Multiply() methods, whilst an IEnglishTools interface might publish CheckGrammar(), CheckSpelling() etc. Above this, both IMaths and IEnglish could belong to the same COM object - UsefulTools for example. Grouping methods together in this way allows for better object-oriented code.


Fig 2.2: COM Heirarchy

 

All very simple, yes? OurApp is roughly equivalent to an application name, IOurApp1 is roughly equivalent to a class within OurApp, and GetAppInfo is roughly equivalent to a method (or member function) of IOurApp1. In practice, you create a pointer to the IOurApp1 interface, and access it's methods just as you would for a class - IOurApp1->GetAppInfo(). Thats not all though. One of the benefits of COM is that objects are re-usable, and are "self aware". This means that they are not just bits of code in "static" functions, COM objects can retain data and serve multiple requests. They also destroy themselves when they are no longer in use. This is extremely useful when you get the hang of it, and a pain in the ass while you're learning. Basically, COM objects keep a count of the number of times an object is referenced, and when this reference count reaches zero they terminate themselves. This is nicely memory efficient, because if handled correctly a COM object will never use more memory than is absolutely required. How does it do this? Simple. Every COM object must publish a standard IUnknown interface, in addition to any other interfaces. The IUnknown interface only has 3 methods: AddRef(), QueryInterface() and Release(). AddRef() and Release() are the 2 you'll be using most. As we just discussed, COM objects keep a reference count. So, when you create a new pointer to an existing object, you must call it's AddRef() method. Similarly, when you're done using the pointer and wish to destroy it, you must call it's Release() method first. If you forget to do these things, you cannot be guaranteed that the COM object you created will be available when you wish to use it, or that it will be unloaded from memory when it is no longer in use. Finally, the QueryInterface() method returns a pointer to another interface. So, our heirarchy diagram now looks as follows:



Fig 2.3: A logical COM heirarchy diagram


All fairly straightforward. Just a little bit more information is required! COM has certain rules, and one of these rules states that you can call any IUnknown method from any interface pointer. This means that even though I may only have a pointer to IOurApp1, I am still able to call IOurApp1->AddRef(). This links directly back to using AddRef() and Release() to enable a COM object to keep track of it's use, and gives us two simple rules:

1. When creating a new pointer to an existing interface, always call AddRef()

    Eg: LPDIRECT3D9 g_pD3D;
     g_pD3D = Direct3DCreate9(D3D_SDK_VERSION);
     LPDIRECT3D9 g_pD3DCopy = g_pD3D;

     g_pD3DCopy->AddRef();

2. When you are finished with a pointer to an interface, always call Release()

    Eg: g_pD3D->Release();
     g_pD3DCopy->Release();


2 important rules, never forget them and you will have a happy life. One important point: Notice how I do not call AddRef() for g_pD3D. This is done automatically for me when I create the pointer using the function Direct3DCreate9(). That goes for all interfaces for all COM objects, DirectX or not. But, very importantly, notice that I still call Release() for both pointers. So, remember the following:

When making a copy of an interface pointer, always use AddRef(). When you're finished with any pointer, call Release().

It's exceptionally important you follow the rules for adding reference counters and releasing interfaces. If you don't do this, your code will behave extremely erratically. The problem is that your code is not necessarily the only piece of code running on the machine that is using a COM interface. In fact, there is nothing to say someone on another machine entirely is using your COM object remotely. So, if you don't increase the reference count every time you make a copy of a pointer, the COM object won't know how many copies you have (and are using). So, when it's internal reference count reaches zero, it will destroy itself. You can see the problem - if the object is destroyed whilst you're still using it, many bad things will happen. So, stick to the rules religiously. Every time you make a copy of a pointer, or create a new pointer to a new interface, go to the end of the function or the class destructor and add a call to Release() in. Forget it at your peril.

A few more facts about COM before you can feel happy and safe. Whilst we use easy-to-remember names for our COM interfaces (such as IOurApp1 or IDirect3D8), in reality these interfaces are defined by unique 128bit numbers called IIDs - Interface Identifiers. You may have heard of GUIDs in Windows, especially in the registry. GUIDS, or Globally Unique Identifiers, is a generic name for any unique 128bit value. An IID is just a specific type of GUID. If you're not creating your own COM objects, you won't need to know much more about this.

DirectX does it's absolute best to keep you away from COM where it can. Skipping ahead of ourselves slightly, D3D even provides library functions to get a pointer to it's interfaces. Taking the above example, with the library function we only have to do this to create a pointer to the IDirect3D9 interface:

LPDIRECT3D9 g_pD3D = Direct3DCreate9(D3D_SDK_VERSION);

Without the function, we'd be doing something along the lines of this:

IDirect3D9* g_pD3D;
CoCreateInstance(CLSID_OBJECT, NULL, CLSCTX_SERVER, IID_IDirect3D9, (void**)&g_pD3D);


And thats assuming CLSID_OBJECT was a var of type CLSID that was set to the IID of the IDirect3D8 interface. See, Microsoft do try to help. Occasionally. One last item before we leave all this behind. Fig 2.2 and 2.3 are logical diagrams of what a COM object looks like, however they are incorrect. I drew them in that way for clarity, but if you look elsewhere at official documentation, this is how you will find a COM object diagrammed:

 


Fig 2.4: A correct COM diagram

The big grey box represents the object itself, and the legs with circles coming out of it represent it's published interfaces. So now you know how to read a COM object diagram! Don't worry if some of this has passed you by, you only need to remember the important rules above, and know that this section is here if you ever need to come back to it. I won't be going any deeper into COM as there are far better resources about, and besides - this is a DirectX tutorial, lets draw some graphics!

Do you use Visual Studio 2003? Please read the note at the end of this DirectX Installation Tutorial!

Before we can begin to draw stuff to screen, we have to set up D3D. To do that, we need to create a new project in Visual C++, and add our base Win32 framework code that we wrote in part 1 of this series. At this point I'm assuming you've downloaded the DirectX 9 SDK from Microsoft, and it's been sucessfully installed. So, load up VC++, and select File - New - Win32 Application. Select your source code folder (I'll be using c:\code\directx as the base folder for these tutorials). Ok the New Project window, and you will be asked what type of Win32 project you wish to create. Select "An Empty Project", and click Finish. You will be presented with a blank workspace. Next, open an Explorer window and copy "win32creation.cpp" from Tutorial 1 into your source code folder, and rename it to Main.cpp. Finally, back in VC++ click the FileView tab in the Workspace window. Right click Source Files, and click Add.


Fig 2.5: Adding the source file to your new project

Select the Main.cpp file you just copied into the project folder, and click OK. You will see Main.cpp appear in the source code file list, as above. If you double click Main.cpp, the source code will appear in the code window. Now we're almost ready to begin. If you were to compile the project now, it would work perfectly and a window would appear on-screen (just as in the 1st tutorial). However, our project is not yet ready to work with DirectX. To do that, we need to link in the D3D libraries. In VC++ click the Project menu, and then click Settings (or press ALT-F7). This will bring up the Project Settings window, where you can change the compiler and linker options, amongst other things. Make sure All Configurations is selected in the "Settings For" listbox, and click the "Link" tab. At the very end of the "Object/Library modules" line, add the following: dxerr9.lib d3d9.lib d3dx9.lib dxguid.lib winmm.lib.


Fig 2.6: Setting the DX libraries as part of your project

Your Project Settings dialog box should look like figure 2.6. Click OK, and you will be returned to the main VC++ screen. Make a note of those library names, as you will always need to add them to every new D3D project you create. We've actually included more than we need for this tutorial, but we will use all of these libraries eventually - better to add them now so you have a one stop guide to doing it! Here is one final tip. Exit VC++, and make a copy of your project folder. In the future, whenever you want to create a new DirectX project, just make a copy of the original project folder and use that. That way you'll never have to do this again. Why? Because in their infinite wisdom, if there is a setting to allow you to store the project settings globally for use later, Microsoft have hidden it so well I (and a lot of other people) can't find it. Plus it will save you time - as we build up our framework you can add the working & tested code to your master project folder, ready for instant use.

Here's a little bit of useful information for you. d3d9.lib contains all the core D3D9 interfaces that we'll use. d3dx9.lib is the D3DX utility library, which provides us with lots of handy functions to make our life easier. dxguid.lib is required for backwards compatability when accessing interfaces of older DirectX versions. dxerr9.lib contains the error parsing tools that we can programatically convert error codes to text strings describing the error. Finally winmm.lib is nothing to do with DirectX, but it's the Windows Multimedia system, which contains handy functions such as timeGetTime() which we'll be using a lot later.


One last thing. If you use Visual Assist, you need to do the following. If you don't, VA won't recognise the DirectX libraries and will underline most of your code in red! So, in VC++ on the Visual Assist toolbar, you'll see a little icon that looks like some cogs with a checkbox. This is the VA options dialog. Click it, and you will be presented with the dialog in fig2.7. In the top (headers) and bottom (source) listboxes on the Directories tab, add the path to your DXSDK\include folder. On my system I installed the SDK to C:\Program Files\DXSDK, so you can see what I've added. When you're done, click both Reparse buttons. Wait for the first reparsing cycle to finish (you can see it at the bottom of your VC++ screen), then quit VC++ and reload it. VA will now recognise DirectX!


Fig 2.7: Setting VA to recognise DX.

 

Well, thats it for this tutorial. You've read all the boring but important bits, and we're now ready to get on and start coding. Good luck!

 

Click here to send feedback, bug reports, comments and ideas etc!


Article proofed by Sol^Trauma