Direct3D Notes from a Beginner
by Jim Adam - posted 11/21/2000
If you're thinking about learning Direct3D, or you already know it but need a good laugh, here are some experiences I recently had trying to learn this technology. Most of the head-banging described here-in is probably covered in a FAQ somewhere. But, hey, I don't even read help files, so don't flame me too hard on that account.
All of these lessons were learned using Direct3D version 7. If you're already using DirectX 8, some of these lessons may no longer apply. (Hope springs eternal.) And keep in mind that I'm only reporting lessons learned, not attempting to evaluate the fitness of Direct3D. Actually, I've been reasonably happy with it.
As a bit of background, I took a 3D graphics course in college (about 9 years ago), and I've played around a teensy bit with OpenGL and also Direct3D Retained Mode in my spare time.
Getting it to Draw
One of the goals of 3D programming is to have something show up on screen. When you're working with Direct3D, it takes about 5,000 lines of initialization code to get so much as a single triangle to draw. So, after you've written the 5,000 lines, when nothing shows up, it can be a real bone-crunching experience. --That's exactly what happened to me.
Well, I had actually used the framework classes that ship with the sample programs as a way of getting up and running faster. This left me with about 1,000 lines of code that I'd written, 4,000 lines of code that they'd written, and no clear idea of what had been left undone. Heck, with Direct3D, I could have done everything, just not done it in exactly the right order....
After about three days of agony, it turned out that I wasn't short one thing. I was actually short three things. (a) A light. (b) A material for the triangle. (c) A viable view matrix. --Lack of any one of these three produces nada, zilch, the set of all triangles tq such that for every q, tq = the empty set.
Luckily, the D3D samples came with precisely the example I needed: a program that drew a single triangle: "Samples/MultiMedia/D3DIM/Src/Tutorials/Triangle". That program didn't use the D3D Framework, but after several days stepping through the debugger, I was finally able to piece together the three missing pieces and--ta da--I was the happy father of a baby triangle! It wasn't exactly a bouncing triangle, but I was still proud.
Having a triangle under my belt, among other places, it was time to work with textures. No problem, as the framework does all the dirty work for you. Just load a texture, set it into the device, and then draw your primitive. I did all this. No texture! My primitive did change color, however, so I remained calm.
Time went by. I banged my head around a little. I inspected sample applications. I turned device render states on and off. I tried different textures. I left cookies out for the elves. Then! At last I saw it! I was building my primitives by hand, and all the texture coordinates in my vertex structures were set to zero. After all, who needs a TV when you have a computer? And TU was just some over-priced university south of the Mason-Dixon line as far as I could tell. But finally it was clear that I needed a 1.0f somewhere--anywhere. And two of them would be even better (one for TU, one for TV). Cha-ching! My TV started receiving transmissions again, and TU was #1 in its division.
As soon as I had textures in place, I decided I didn't need no stinking materials. Why spend CPU cycles coloring an object that's just going to get wraped by a texture? Well, one reason is that if you don't assign a material to your objects, your texture will not show up. Your objects will all just be black. After you have spent 4 days downloading free textures off the web (or carefully hand-crafting them pixel by pixel, like I do...), this is not what you want. You can try to fix it any way you want. You can turn your TV as high as you want, and send huge floating point donations to TU until they turn off your electricity. It will not help. It's material you need (but don't ask me why, because I have no idea).
I had a Z-Buffer. It was a very nice one, too. But it was lazy. It did not want to do any work. As a result, I had some *very* weird scenes, where objects that were obviously further away were being drawn in front of objects that were nearer. In the documentation they referred to this as seeing "hidden surface artifacts in distant objects," like part of a face or something would occasionally peak through. Forget it. I was seeing entire objects popping through the middle of other objects. It was like looking down a tunnel.
So I said to my Z-Buffer, "Get off your lazy tooqas and go filter out some hidden surfaces for me." It gave me the finger. That made me mad, so I went looking for a way to make it work.
According to the SDK documentation, D3DRENDERSTATE_ZENABLE by default is TRUE. For a while, I believed the documentation, and I went looking elsewhere for the problem. Eventually, I started running out of things to try. I started trying things even if they didn't make sense. One time, I made a pathetic change, setting D3DRENDERSTATE_ZENABLE to TRUE. I felt very low, ashamed, but I was deperate. Then I pressed F5 and discovered I had been lied to, misrepresented, falsified, and misappropriated. Suddenly, my hidden surfaces were as if they had never existed.
With D3D version 8, I will buy the documentation in book form. That will be much better than the on-line version, because you can throw a book against the wall and it will still mostly work (with a little tape, maybe). Try saying the same thing for a monitor though....
The framework is great and it will save hundreds of thousands of C++ developers from having to key in the same 5000 lines of initialization and cleanup code: a global savings of a billion lines of duplicate code. If the rest of the Microsoft teams provided this type of support, the world would be a better place to live, at least for developers.
Here I'll list some of my lessons learned, most of which are probably obsolete now that D3D version 8 is available.
Alt+Tab. The framework doesn't respond to the WM_ACTIVATEAPP message, which it can receive when the user presses Alt+Tab. This happens a lot when I am the user, because I often want to switch from my application to the debugger. The engineers at Mircrosoft probably missed this step because they don't write bugs, and so don't need the debugger.
But, debugger or no, it's important for the framework to pause and unpause when it receives this message. One way to achieve this is to call CD3DApplication::Pause( true/false ) in response to WM_ACTIVATEAPP. If you use this approach, though, be careful about just calling CD3DApplication::Pause( FALSE ) when you receive the first WM_ACTIVATEAPP (which happens when your application starts up for the first time). Doing so will set the "paused count" to negative one, which is a bad thing and will bite you when the user actually tries to pause the application the first time.
The Quaternion Ate my Matrix. Beware the framework's support functions. Most are no doubt correct, but not all. For example, D3DMath_QuaternionFromMatrix() has a horrible copy-paste bug, where the input matrix is modified following the (partial) calculation of the quaternion. Thus, not only is the resulting quaternion questionable (try saying that three times fast!), but the input matrix will have been corrupted as well. Here is a perfect example where use of the C++ const keyword would have saved the day. Unfortunately, almost none of the support functions in the framework are const correct.
A questionable Quaternion from Quadtree
Decided to go out and P.
But it ran into trouble with X, Y, and Z.
Now it rotates in eternity.
Initialization and Cleanup. The D3D Framework has a definite philosophy about when and where certain types of operations can or must be performed. For example, you must invalidate any textures you own in your DeleteDeviceObjects method (called by the framework whenever the screen is resized, etc.). Failure to do this will cause the Framework to exit (as it will detect that the reference count on the DirectDraw interface isn't zero). And if you're holding onto a DirectDraw reference for other reasons, you will have to release it in DeleteDeviceObjects and then reacquire it in the InitDeviceObjects method, which is where you'll also want to "restore" all of your textures.
Full Screen Mode. The framework starts up in windowed mode by default. Actually, it starts off in a fairly small window of hard-coded size. Getting it to start up as a maximized window isn't quite so bad. You can just add a "::ShowWindow( m_hWnd, SW_MAXIMIZED)" call in the framework function that creates the window. Or, if you're hesitant to change the framework directly, you can override the method that creates the window and do that call yourself (at the expense of some copy-pasting). But getting the application to start up in full-screen mode isn't as easy as it might seem.
(a) The primary method that implements this mode switching (CD3DApplication::Change3DEnvironment) assumes that the application started life in windowed mode. When switching to full-screen mode, it saves the current window size and state in a local static variable. When switching "back" to windowed mode, it restores the window size and state using this local static. This means that if the application started in full-screen mode, this call will set the window size and state to some garbage values. --This may or may not be a bad thing, as I was too chicken to try it.
(b) The logic for the initial windowed state is spread throughout the framework. Both the application and the framework classes participate in this assumption, as does a "device enumeration" helper function (D3DEnum_DefaultDevice) that is used during initialization.
Having the source code to the framework means there are several ways to get around this problem, from modifying the framework code itself to overriding (via extensive copy and paste) various methods implemented there-in. Still, fixing this isn't as easy as saying "start in full-screen mode." For myself, I've decided to wait, cross fingers, and pray that D3D 8 has a better implementation for this bit, rather than try to mess with it myself.
A key point to remember is that light positions and directions are specified in world coordinates. This means the current world transformation is not applied to these values when you call the SetLight method. Thus, if you are trying to attach a light to an object that has a unique world transform, or have a light move in concert with an moving object, you need to perform transformations on the light's position and direction vectors yourself.
As with other bits in Direct3D, don't forget to turn lighting on at the global level via the SetRenderState( D3DRENDERSTATE_LIGHTING, TRUE ) method. And, then, remember to turn on each individual light using the D3dDevice's LightEnable() method. If your light still doesn't show up, pray that you are using a spotlight and that it's entirely contained inside a single polygon, because that would explain everything. Of course you know enough not to expect the light to show up in that situation, right?
Render State Retained
The Direct3D device retains a lot of state information between calls to the Render method. I learned this for sure when I was playing with alpha blending, and decided to draw just the last object in the scene with alpha blending enabled. Woops. That was a good approach for the first frame. But every other frame had blending enabled all the time, because I hadn't turned it off when I was done. This lent my scene a nice trippy '60s feel. Just not the effect I was aiming for.
Well, that's all for now. I hope you had a good laugh.
Contact me here: Jim Adam.
And visit my home page to read some of my game reviews.