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

Use Direct3D 8 To Fly Through the Munsell Color Solid

0.00/5 (No votes)
19 Jul 2004 1  
A tutorial on Direct3D 8

Introduction

Direct3D is the component of DirectX that can render immersive 3D worlds. Direct3D is ideally suited for programs that allow the operator to fly through complex models in 3D space. The specific model I tackled was a model of the Munsell color solid. The Munsell color solid is a model for the range of colors perceivable by the human eye. It was developed back in 1905 by Albert H. Munsell. It is called a color solid because the color varies continuously throughout the volume of the model. You usually see the Munsell color solid displayed in 2D space in one of the following manners:

A vertical slice through the Munsell color solid would result in a 2D image such as the following:

The darkest, least-colorful colors reside near the center of the Munsell color solid. In fact, the central axis is the range of black and white colors known as the gray scale. The outer perimeter of the model is lumpy, just because that's how Munsell decided to distribute the colors. Munsell was an artist. Most scientists would prefer a color model that displays some form of geometric symmetry, such as a sphere or pyramid. But there is no reason to believe that cleaner geometry better depicts human color perception.

Background On DirectX

DirectX is the name of the Microsoft technology that allows developers to write computer games that achieve a high frame rate. DirectX allows your program to write to the display much faster than Microsoft's original technology, the GDI ("graphics device interface"). Direct3D is that portion of DirectX which can draw three dimensional shapes with smooth Gouraud shading and scientifically correct lighting. With Direct3D, we can create a program that allows the operator to dynamically adjust his position with respect to a 3D object, to zoom in and out, and to rotate the object about its central axis. You can even fly right through the object to view its interior.

I decided to employ Direct3D 8, which is the version that ships as part of the Windows XP operating system. Microsoft has since moved on to Version 9. DirectX version 9 can be installed on Windows 98, Windows Me, Windows 2k, and Windows XP.

To develop Direct3D programs you need to obtain the DirectX SDK (software development kit) from Microsoft. This is over 200 Mbytes so it's a pain to download (Microsoft used to offer the SDK on a $10 CD-ROM but they seem to have stopped doing this).

To merely execute a program (as opposed to build a program) that employs Direct3D you only need the much smaller DirectX run-time which you can again download from Microsoft. It is this run-time that was included in Windows XP but not in earlier Microsoft operating systems. Alternatively, most computer games install some version of the DirectX run-time and hence your computer may already have the version 8 or 9 run-time if you have purchased and installed a fairly recent game. Once you have installed the DirectX 9 run-time, you should be able to execute any program that employs DirectX 9, DirectX 8, DirectX 7, etc.

Be warned that once you install any version of any of the DirectX technologies, you cannot easily get it back off your computer. Microsoft does not bother to provide an uninstall capability. Therefore Microsoft recommends that Windows Me and Windows XP users create a "System Restore" point before they install a new version of DirectX. Windows 9x and Windows 2k users will have to re-install their operating system to revert to an earlier version of DirectX.

My program has no other dependencies other than DirectX. It employs the standard WIN32 API described in Charles Petzold's famous book "Programming Windows". I make no use of MFC, ATL, STL, WTL, etc.

The Code

My goal was to use Direct3D to display 20 radial slices through the Munsell color solid and to allow the operator to select an arbitrary rotation, tilt, and zoom factor from which to view this representation of the color solid. The operator can use the r and R keys to rotate the model, the e and E keys to elevate his viewpoint above or below the centerline, and the z and Z keys to select the viewing distance. A typical view presented by the finished program is shown below:

By the time I had the program working I realized it was an excellent illustration of both transparent textures and the importance of the alpha test.

A texture is just a .BMP file used to provide the surface coloring for a 3D model. This particular program uses 20 .BMP files holding the radial slices through the Munsell color solid. A single one of these .BMP files is shown below:

You can see that this .BMP file has a medium gray background. Microsoft's D3DXCreateTextureFromResourceEx() function provides a way to declare one particular color as the color key color which means that it won't be copied to the screen. The way you accomplish this in Direct3D8 is by enabling alpha blending via a statement such as:

pd3dDevice->SetRenderState( D3DRS_ALPHABLENDENABLE, true );

The color key color will then be transparent wherever it appears in your textures. While running the program you can type the 't' key to toggle whether the medium gray color is treated as being transparent. When the color is not treated as transparent, the Munsell color solid looks like:

Obviously, that's a mess. But even after I got the transparent texture working correctly, I still observed the following, smaller mess:

You really need to download the .EXE executable (or build it yourself from the Visual C++ 6 Project that I provide) and then rotate the model for yourself to observe the dynamic characteristic of the image distortion pictured above.

At first it seemed that the distortion visible in this last image only occurred on the left-hand side of the Munsell color solid. And it so happens that you are viewing the back face of the Direct3D surfaces on that side of the Munsell color model. You can verify this by typing the 'b' key to toggle Direct3D's back face culling mode. If the distortion only existed on the left-hand side of the color solid then the problem might be an interaction between transparent textures and back faces.

But this was a red herring, as I realized when I saw that if I rotated the model just so then there was a similar type of image distortion that would appear on the right hand side of the Munsell color solid. You can see this in the picture below:

I realized that what I was looking at was the gray "transparent" portion of the very first radial slice that my algorithm was painting to the screen. All the other radial slices would look great on the right-hand side of the Munsell color solid except for this one particular slice, and this bad slice was the very first slice I painted.

I was able to solve this problem and eliminate the visual distortion by adopting a more complicated rendering algorithm. My original program always painted slice #0 first, then slice #1, etc. irregardless of the current rotation of the model. The visual distortion disappeared when I took the trouble to paint the radial slices in an order dictated by the current rotation of the model. As long as the slices were rendered in a back-to-front order the final picture was perfect. You can see the difference this makes by using the 'p' keystroke to toggle between the original algorithm and the smarter algorithm that orders the slices. I chose the letter 'p' for this keystroke because this is often called the painter's algorithm, meaning that the last color an oil paint artist places on the canvas is the color you observe.

After I made this code available from my web site www.computersciencelab.com/Direct3DTut1.htm I was informed of an even simpler solution by Henrik Rydg�rd of Sweden. If the particular video card in the computer supports an alpha test then the visual distortion can be avoided even if the program continues to use the original algorithm where it always starts painting with slice #0.

To understand the alpha test, you first need to understand the mechanism that was causing the visual distortion. Let's suppose that slice #0 is the red slice (which it is) and the current rotation of the model means that you should be able to see some of the pink slice appearing behind the red slice (look again at the last picture). You need to be able to see the pink slice in those areas of the screen where the gray "transparent" border of the red slice would normally be painted. You prevent the gray pixels that make up the border of slice #0's texture from being copied to the display surface by declaring that gray should be treated as the color key. BUT EVEN THOUGH THE GRAY PIXELS ARE NOT COPIED TO THE DISPLAY SURFACE, THEIR POSITIONS IN 3D SPACE ARE WRITTEN TO THE DEPTH BUFFER !! Because of this, when you later attempt to create the pink pixels of slice #1 that need to appear behind slice #0, Direct3D will NOT paint these pixels because their z distance from the viewer (i.e., their depth) is further than the distance recorded in the z buffer (the depth buffer) for that spot on the display, leading Direct3D to believe these pixels are obscured by something closer to the viewer. But that something is transparent, so this is not the behavior we want.

At first I didn't think Direct3D offered a solution to this problem. But it does, as I learned from Henrik. You just need to add the following statements:

pd3dDevice->SetRenderState( D3DRS_ALPHATESTENABLE, true );

pd3dDevice->SetRenderState( D3DRS_ALPHAREF, 0x01 );

pd3dDevice->SetRenderState( D3DRS_ALPHAFUNC, D3DCMP_GREATEREQUAL );

These 3 statements ask Direct3D to NOT write a transparent pixel to the z buffer (depth buffer) unless its alpha value exceeds a certain value. The particular value 0x01 is sufficient for my textures. If the transparent pixels from slice #0 are not written to the depth buffer then the (non-transparent) pixels from later slices can still qualify for rendering and thereby appear UNDERNEATH the transparent areas of slices that were drawn earlier. After adding these 3 statements I was able to toss the complexity of sorting the slices in a back-to-front order and instead I could always render slice #0 first, then slice #1, etc. But again, this worked on my computer because my video card offered this capability. Your video card may or may not support this particular DirectX feature.

You can interrogate your video card to learn if it has this capability in the following manner:

D3DCAPS8   d3dCaps;
pd3dDevice->GetDeviceCaps( &d3dCaps );
if ( d3dCaps.AlphaCmpCaps & D3DPCMPCAPS_GREATEREQUAL )
{

   pd3dDevice->SetRenderState( D3DRS_ALPHATESTENABLE, true );
   pd3dDevice->SetRenderState( D3DRS_ALPHAREF, 0x01 );
   pd3dDevice->SetRenderState( D3DRS_ALPHAFUNC, D3DCMP_GREATEREQUAL );
}
else
{

   //<use the painter's algorithm>


}

Because of this hardware dependency, it is safer for this program to render the slices in a back-to-front order because this will work with all video cards. You can find the SetRenderState( D3DRS_ALPHATESTENABLE, true ) statement in the appropriate place in my source code but it is commented out. This way you can use the 'p' keystroke to alternate between the original render algorithm (which always starts with slice #0) and the smarter back-to-front algorithm (the "painter's algorithm") and you'll be able to observe the difference regardless of which video card you have.

This program is written in C++ and is very heavily commented. For example, here is the SetRenderStates() member function from my cMunsell class:

HRESULT cMunsell::SetRenderStates()
{
    HRESULT     hr;
    IDirect3DDevice8 * pd3dDevice = g_pd3dDevice;

    // The D3DRENDERSTATETYPE enumerated type is used with

    // IDirect3DDevice8::SetRenderState() to specify all possible

    // rendering states.


    // By default, Direct3D performs lighting calculations on all

    // vertices.  And consequently, if a vertex has no normal vector

    // it will receive zero light.  An application that specifies

    // vertex colors probably will not specify normal vectors and

    // hence must be sure to disable lighting or else everything

    // will be black.


    // If you plan to cover all surfaces with textures then you

    // probably want to disable Direct3D lighting.  Otherwise,

    // you will use IDirect3DDevice8::SetLight() and

    // IDirect3DDevice8::SetMaterial().


    hr = pd3dDevice->SetRenderState( D3DRS_LIGHTING, false );

    if ( FAILED( hr ) )
        return hr;

    // Depth buffering is a method of removing hidden lines and surfaces.

    // By default, Direct3D does not use depth buffering but it can

    // be enabled using the D3DRS_ZENABLE state.


    // Enable the z-buffer.


    hr = pd3dDevice->SetRenderState( D3DRS_ZENABLE, D3DZB_TRUE );

    if ( FAILED( hr ) )
        return hr;

    // To convince Direct3D to render the inside of an object in

    // addition to its outside you will need to turn off back-face

    // culling.


    hr = pd3dDevice->SetRenderState( D3DRS_CULLMODE, D3DCULL_NONE );

    if ( FAILED( hr ) )
        return hr;

    // Since we want to employ color keying when we load our textures,

    // we need to enable alpha blending.


    hr = pd3dDevice->SetRenderState( D3DRS_ALPHABLENDENABLE, true );

    if ( FAILED( hr ) )
        return hr;

    // Alpha blending means combining the color value of the new pixel

    // with the pixel already stored at that location in the frame buffer.

    // There are a number of ways of doing this, described by the eq.:


    // FinalColor = TexelColor � SourceBlendFactor

    //            + PixelColor � DestBlendFactor


    // To achieve complete transparency you would request:


    // hr = pd3dDevice->SetRenderState( D3DRS_SRCBLEND,  D3DBLEND_ZERO );

    // hr = pd3dDevice->SetRenderState( D3DRS_DESTBLEND, D3DBLEND_ONE );


    // But we want the alpha blending formula to vary on a pixel by pixel

    // basis, as described by the alpha channel of the source.  This

    // is accomplished by:


    hr = pd3dDevice->SetRenderState( D3DRS_SRCBLEND, D3DBLEND_SRCALPHA );

    if ( FAILED( hr ) )
        return hr;

    hr = pd3dDevice->SetRenderState( D3DRS_DESTBLEND, 
D3DBLEND_INVSRCALPHA );

    return hr;

} // cMunsell::SetRenderStates()

Compiling

I have included my executable (Munsell.exe) so you can experiment with the program without needing to compile code yourself.

If you do want to compile the code then you will need to first install Microsoft's DirectX 9 SDK. Microsoft provides this as a free download. To download DirectX from Microsoft go to the URL:

www.microsoft.com/downloads

and then click in the left-hand column where it says "DirectX". This will bring up a list of hyperlinks including one for the "End-user Runtime" (about 20 MBytes) and another for the "Software Developer Kit" (or SDK, which is over 200 MBytes). These hyperlinks will all mention the current release of DirectX (as of this writing, that's DirectX 9.0b) and that version is compatible with the software described here. Microsoft does not bother to continue distributing the older versions of DirectX. Again, if you are running the Windows XP operating system then your computer came with the DirectX 8 run-time, which is sufficient to execute my demonstration program. But to compile the code yourself you will have to install the full SDK.

I have included my Visual C++ 6 project file (Munsell.dsp). The only customization that I had to perform is to add the library files d3d8.lib, d3dx8.lib, and dxerr8.lib to the list of "Object/Library modules" that you find on the "Link" tab of the "Project Settings" dialog (which appears when you select "Project/Settings" from the Visual C++ 6 menu). This change is already incorporated in the Visual C++ 6 project that I provide so you will only need to repeat it if you create a new project.

Because Visual C++ 6 is older than DirectX 8, you have to force the compiler to employ the DirectX .H and .LIB files from the DirectX SDK rather than those similarly named but older files that shipped with Visual C++ 6. This customization you each will need to perform because this setting is not stored in the .DSP or .DSW project files. Select "Tools/Options" from the Visual C++ 6 menu which causes the "Options" dialog to appear. Activate the "Directories" tab on this dialog and then select "Include files" from the combo box labeled "Show directories for". Click on the "New" icon and then type in the directory path where you chose to install the DirectX SDK. Assuming you accepted the defaults offered by the DirectX SDK installer, then this directory path would be:

  • C:\mssdk\include

Then use the Up arrow icon to bring this new entry to the top of the list so it will be searched before the other members of the list.

Next select "Library files" from the combo box labeled "Show directories for". Click on the "New" icon and then type in the directory path where you chose to install the DirectX 8 SDK. Assuming you accepted the defaults offered by the DirectX SDK installer, then this directory path would be:

  • C:\mssdk\lib

Then use the Up arrow icon to bring this new entry to the top of the list.

History

This is my first submission to CodeProject, which is a web site I turn to often. My thanks to all who have contributed! You can read more about me and my other software efforts at http://www.computersciencelab.com/.

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