sunlight

DirectX Graphics

Introduction

In this part of the tutorial, we will cover:

  • Moving the 2D graphics to DirectX Graphics 8
  • Alpha-blending

Previous View Code Download Code

DirectX Graphics

2D graphics is a fairly simple task - essentially, just drawing rectangular objects with transparency. Almost every DirectDraw program works in just this way - it is the fastest and easiest method to achieve everything. As a result, the DirectDraw API hasn't changed substantially from that shipped with DirectX version 3, back in 1997. DirectX 8 doesn't even bother to update DirectDraw, which remains at version 7. When you compare this with the huge leaps forward in 3D graphics technology, it's natural to wonder if there's a way to take advantage of it.

In this tutorial, we will move our 2D graphics to DirectX Graphics, adding new features as we go. We will introduce mode-switching (between windowed and full-screen modes) and alpha blending, as well as introducing essential techniques for working with a 3D API: vertex buffers and texture mapping. We will also examine the differences between 2D in a 2D buffer-driven API and a 3D triangle-driven API.

We will put our DirectX Graphics code in a new file, since it will be significantly different from the existing DirectDraw code.

Header Files

We will replace the ddraw.h header file in the precompiled header file stdhdr.h with d3d8.h and d3dx8.h:

#include <d3d8.h>
#include <d3dx8.h>

Initialisation

DirectX Graphics has a slightly different and more streamlined initialisation process than previous versions, and it's even easier than DirectDraw. First, we have to create an IDirect3D8 object, with Direct3DCreate8:

// Initialise DirectX Graphics and go to full screen mode.
BOOL InitDirectXGraphics()
{
    g_pD3D = Direct3DCreate8(D3D_SDK_VERSION);
    if (g_pD3D == NULL)
        return FALSE;

    return InitDevice(TRUE);
}

Next, we will create a device object, which represents the 3D card itself - like the primary surface and back buffers in DirectDraw. In order to do this, we must fill in a D3DPRESENT_PARAMETERS structure, which tells DirectX things like the screen resolution, the number of back buffers and the pixel format. Unlike DirectDraw, when we would merely specify 16 bits per pixel, DirectX Graphics wants a specific pixel format. You can enumerate the pixel formats to pick one that is supported; here I have cheated and assumed support for R5G6B5 16-bit colour.

    D3DPRESENT_PARAMETERS d3dpp; 
    ZeroMemory(&d3dpp, sizeof(d3dpp));
    d3dpp.Windowed = FALSE;
    d3dpp.SwapEffect = D3DSWAPEFFECT_FLIP;
    d3dpp.BackBufferCount = 2;
    d3dpp.BackBufferFormat = D3DFMT_R5G6B5;
    d3dpp.hDeviceWindow = hWndMain;
    d3dpp.BackBufferWidth = 640;
    d3dpp.BackBufferHeight = 480;
    d3dpp.FullScreen_RefreshRateInHz = D3DPRESENT_RATE_DEFAULT;
    d3dpp.FullScreen_PresentationInterval = D3DPRESENT_INTERVAL_ONE;
    d3dpp.EnableAutoDepthStencil = TRUE;
    d3dpp.AutoDepthStencilFormat = D3DFMT_D16;
    h = g_pD3D->CreateDevice(D3DADAPTER_DEFAULT, D3DDEVTYPE_HAL, hWndMain,
                            D3DCREATE_SOFTWARE_VERTEXPROCESSING,
                            &d3dpp, &g_pd3dDevice);

Now we have created our device, we can set up some essential parts to make sure our 2D graphics will display correctly. We will turn off culling and the z-buffer (since we don't want DirectX deciding not to draw things), and lighting (which makes no sense in 2D).

    // Turn off culling
    g_pd3dDevice->SetRenderState(D3DRS_CULLMODE, D3DCULL_NONE);
    // Turn off D3D lighting
    g_pd3dDevice->SetRenderState(D3DRS_LIGHTING, FALSE);
    // Turn off the zbuffer
    g_pd3dDevice->SetRenderState(D3DRS_ZENABLE, FALSE);

We will just release the device and Direct3D object when we're done with them.

Textures

Images in 3D applications are manipulated in the form of textures. These textures can then be applied to 3D objects in a variety of ways. We want a very simple texture loader, that does no filtering, resizing or changes of any kind. 

IDirect3DTexture8 *LoadTexture(LPCTSTR lpFilename)
{
    IDirect3DTexture8   *pTexture;

    // Create an unfiltered, unsized texture
    HRESULT h = D3DXCreateTextureFromFileEx(g_pd3dDevice, lpFilename, 
        D3DX_DEFAULT, D3DX_DEFAULT, D3DX_DEFAULT, 
        0, D3DFMT_UNKNOWN, D3DPOOL_DEFAULT, D3DX_FILTER_NONE, D3DX_DEFAULT,
        0, NULL, NULL, &pTexture);
    if (FAILED(h))
        return NULL;
    return pTexture;
}

D3DXCreateTextureFromFileEx takes many parameters, but most of them can be set to default values, making it actually very simple to call. Interestingly, the ANSI and Unicode versions of this function are documented separately.

Objects

As hinted at beforehand, 3D APIs have a different focus from 2D APIs - they are specialised in drawing triangles. This is essentially all that most 3D graphics cards do; admittedly they do a lot with triangles, and they do it very quickly, but they're just triangle-drawing engines at the bottom.

We can, of course, create a rectangle from two triangles, and we can then wrap a texture around this rectangle, resulting in a rectangular object. Since these objects are going to be central to our 2D graphics, we will have a simple management system for them. We will be able to create these objects, move them about and set their texture.

These objects will have to become triangles at some point, of course. Each of these triangles is defined by three vertices. Since we're only interested in absolute screen position - i.e. we're not interested in an abstract 'world space' - we will work with ready-transformed-and-lit vertices. Each of these vertices requires an x, y and z value, a value called rhw (which, for our purposes, will remain 1.0), a colour (which we will eventually ignore), and texture co-ordinates - positions to map points on the texture.

struct CCustomVertex
{
    float   x, y, z, rhw;   // The transformed position for the vertex.
    DWORD   dwColor;        // The vertex colour.
    float   tu, tv;         // Texture co-ordinates.
};

#define D3DFVF_CUSTOMVERTEX (D3DFVF_XYZRHW|D3DFVF_DIFFUSE|D3DFVF_TEX1)

The FVF code tells DirectX what to expect in each of its vertices.

We have a simple fixed-size array for the object buffer, which is not terribly exciting. The meat of the objects begins with the CreateRectangularObject function:

// Initialise an object in the buffer
int CreateRectangularObject(int x, int y, int cx, int cy,
                            IDirect3DTexture8 *pTexture, int xSrc, int ySrc)
{
    CRectangularObject  *pObj = &g_pObjects[g_nNewObjectPointer];

    D3DSURFACE_DESC desc;
    pTexture->GetLevelDesc(0, &desc);

    pObj->vertices[0].x = pObj->vertices[2].x = (float)x - 0.5f;
    pObj->vertices[1].x = pObj->vertices[3].x = (float)(x + cx) - 0.5f;
    pObj->vertices[0].y = pObj->vertices[1].y = (float)y - 0.5f;
    pObj->vertices[2].y = pObj->vertices[3].y = (float)(y + cy) - 0.5f;
    
    // z and rhw co-ordinates are fixed at 0.5 and 1.0 respectively
    pObj->vertices[0].z = pObj->vertices[1].z = 
    pObj->vertices[2].z = pObj->vertices[3].z = 0.5f;
    
    pObj->vertices[0].rhw = pObj->vertices[1].rhw = 
    pObj->vertices[2].rhw = pObj->vertices[3].rhw = 1.0f;
    
    // White colour to avoid clashing with the texture
    pObj->vertices[0].dwColor = pObj->vertices[1].dwColor = 
    pObj->vertices[2].dwColor = pObj->vertices[3].dwColor = 0xFFFFFFFF;

    // Texture co-ordinates
    pObj->vertices[0].tu = 
	pObj->vertices[2].tu = (float)xSrc / (float)desc.Width;
    pObj->vertices[1].tu = 
	pObj->vertices[3].tu = (float)(xSrc + cx) / (float)desc.Width;
    
    pObj->vertices[0].tv = 
	pObj->vertices[1].tv = (float)ySrc / (float)desc.Height;
    pObj->vertices[2].tv = 
	pObj->vertices[3].tv = (float)(ySrc + cy) / (float)desc.Height;

    pObj->pTexture = pTexture;
    
    g_nNewObjectPointer++;
    return g_nNewObjectPointer - 1;
}

This creates four vertices for each of the corners of our rectangle. We will draw these as a triangle strip, which uses the first three to draw a triangle and the 2nd, 3rd and 4th to draw the next triangle, conveniently drawing a rectangle. The texture co-ordinates, tu and tv are normalised between 0.0 and 1.0 (0.0 being the left or top, 1.0 being the right or bottom).

There are a couple of simple functions to modify the objects, which are not interesting. Next, we must get these vertices to the card. We do this by loading them into a vertex buffer - a video-memory buffer which holds our vertices.

// Create the vertex buffer which holds the sprite co-ordinates
BOOL CreateVertexBuffer()
{
    HRESULT h;

    if (g_pVB != NULL)
        return FALSE;

    h = g_pd3dDevice->CreateVertexBuffer(
	g_nNewObjectPointer * 4 * sizeof(CCustomVertex),
        D3DUSAGE_WRITEONLY, 
        D3DFVF_CUSTOMVERTEX,
        D3DPOOL_DEFAULT, &g_pVB);
    if (FAILED(h))
        return FALSE;
    // Lock the vertex buffer and put our co-ordinates in it
    CCustomVertex   *pVertices;

    h = g_pVB->Lock(0, 0, (BYTE **)&pVertices, D3DLOCK_DISCARD);
    if (FAILED(h))
        return FALSE;
    for (UINT i = 0; i < g_nNewObjectPointer; i++)
    {
        memcpy(pVertices, g_pObjects[i].vertices, 4 * sizeof(CCustomVertex));
        pVertices += 4;
    }

    g_pVB->Unlock();
    return TRUE;
}

We create a write-only vertex buffer, which allows DirectX to optimise it (reading from video memory can be very, very slow indeed). We then simply copy the vertices to it.

Next, we need a function to begin drawing these objects from the vertex buffer to the screen.

void BeginDrawingObjects()
{
    g_pd3dDevice->SetStreamSource(0, g_pVB, sizeof(CCustomVertex));
    g_pd3dDevice->SetVertexShader(D3DFVF_CUSTOMVERTEX);

    // Get the colour information solely from the texture.
    g_pd3dDevice->SetTextureStageState(0, D3DTSS_COLOROP,   D3DTOP_SELECTARG1);
    g_pd3dDevice->SetTextureStageState(0, D3DTSS_COLORARG1, D3DTA_TEXTURE);
}

Here, we tell DirectX to ignore the colour information specified in the vertex, and to use the texture. We tell the device to get triangles from the vertex buffer we created, and tell it what format to expect the vertices. Now we can draw the objects:

void DrawObject(int nIndex)
{
    g_pd3dDevice->SetTexture(0, g_pObjects[nIndex].pTexture);

    g_pd3dDevice->DrawPrimitive(D3DPT_TRIANGLESTRIP, nIndex * 4, 2);
}

All that remains is to load the objects during initialisation:

    // Initialise sprites.
    SetObjectBufferSize(4);

    // Load our textures.
    pBackgroundTexture = LoadTexture(_T("a.bmp"));
    if (pBackgroundTexture == NULL)
    {
        ExitDirectXGraphics();
        
        DISPLAYERROR("Could not load background image.");
        return FALSE;
    }
    pSpriteTexture = LoadTexture(_T("sprites.bmp"));
    if (pSpriteTexture == NULL)
    {
        ExitDirectXGraphics();
        
        DISPLAYERROR("Could not load sprites.");
        return FALSE;
    }

    nBackground = CreateRectangularObject(0, 0, 640, 480, 
	pBackgroundTexture, 0, 0);
    nFixedSprite = CreateRectangularObject(288, 208, 64, 64, 
	pSpriteTexture, 128, 0);
    nMovingSprite = CreateRectangularObject(288, 100, 64, 64, 
	pSpriteTexture, 0, 0);
    nPlayerSprite = CreateRectangularObject(288, 150, 64, 64, 
	pSpriteTexture, 64, 0);

    CreateVertexBuffer();

Now we draw these during DrawFrame:

void DrawFrame()
{
    DEBUG_TIMING_START();
    MoveObject(nMovingSprite, 288 + xAutomaticSprite, 100);
    MoveObject(nPlayerSprite, 288 + xPlayer, 150);
    RefreshVertexBuffer();

    BeginScene();
    BeginDrawingObjects();

    DrawObject(nBackground);
    DrawObject(nFixedSprite);
    DrawObject(nMovingSprite);
    DrawObject(nPlayerSprite);
    
    EndScene();
    DEBUG_TIMING_END("Scene draw");
    Flip();
}

We now need to turn briefly to the DirectInput code, which flips to the GDI surface before displaying its input boxes. With DirectX Graphics, this is no longer necessary, and you can delete these lines.

That's it - a little clearing up code and you're away with DirectX Graphics!

Actually, there are a few things missing from this picture. Chief among them is the lack of colour key support - indeed, there is no colour key support in DirectX Graphics. I also promised some advantages, and we'll get to those now.

Alpha Blending

In place of colour key support, DirectX Graphics offers alpha blending. This is basically variable transparency - you can use it to provide colour-key-type effects, or better still some very cool transparency effects.

To manage these, the DirectX 8 SDK provides a sample application called DXTex, which allows you to load the colour and alpha parts of a texture and merge them into a single file, with the extension DDS. I have provided an alpha bitmap:

As you can see, black is transparent, white is opaque - and the shades of grey are in between. I just loaded the sprites bitmap into DXTex, loaded this bitmap as the alpha channel and saved it out as 'sprites.dds'.

We must now initialise alpha-blending in our application. First, we must turn it on:

    // Turn on alpha-blending
    g_pd3dDevice->SetRenderState(D3DRS_ALPHABLENDENABLE, TRUE);

Next, we need to tell DirectX what to do with the alpha values.

    // Set the render state up for source alpha blending.
    g_pd3dDevice->SetRenderState(D3DRS_SRCBLEND, D3DBLEND_SRCALPHA);
    g_pd3dDevice->SetRenderState(D3DRS_DESTBLEND, D3DBLEND_INVSRCALPHA);

This tells DirectX that the source should come through with increasing alpha values, and the destination with decreasing alpha values. Now we need to tell DirectX where to get its alpha information from:

    // Get the alpha information solely from the texture.
    g_pd3dDevice->SetTextureStageState(0, D3DTSS_ALPHAOP,   D3DTOP_SELECTARG1);
    g_pd3dDevice->SetTextureStageState(0, D3DTSS_ALPHAARG1, D3DTA_TEXTURE);

We're done, and you should now have alpha-blended textures with very little effort.

Mode Switching

One more feature before we finish: mode switching. The ease of initialising DirectX Graphics means this is very easy indeed. We add an action to our table:

    { ACTIONS_MODESWITCH, DIKEYBOARD_M, DIA_APPFIXED, "Full screen/Windowed" },

When this is pressed, we just close the device and reopen it in windowed mode, and reload our textures.

    case ACTIONS_MODESWITCH:
        if (dwData != 0)
        {
            // Switch between full-screen and windowed mode
            ReleaseVertexBuffer();
            CloseDevice();
            
            g_bFullScreen = !g_bFullScreen;

            InitDevice(g_bFullScreen);
            
            // Reload our textures.
            pBackgroundTexture = LoadTexture(_T("a.bmp"));
            SetObjectTexture(nBackground, pBackgroundTexture);
            pSpriteTexture = LoadTexture(_T("sprites.dds"));
            SetObjectTexture(nFixedSprite, pSpriteTexture);
            SetObjectTexture(nMovingSprite, pSpriteTexture);
            SetObjectTexture(nPlayerSprite, pSpriteTexture);
            
            CreateVertexBuffer();
        }
        break;

We need to change InitDevice to handle windowed mode. First, it will set up the window with a title bar and border:

BOOL InitDevice(BOOL bFullScreen)
{
    HRESULT h;

    ShowWindow(hWndMain, SW_HIDE);
    if (bFullScreen)
    {
        // Set up main window to cover the screen
        SetWindowLong(hWndMain, GWL_STYLE, WS_POPUP);
        SetWindowPos(hWndMain, NULL, 0, 0, 
            GetSystemMetrics(SM_CXSCREEN), 
            GetSystemMetrics(SM_CYSCREEN), SWP_NOZORDER);
    }
    else
    {
        // Set up main window to be not sizable
        SetWindowLong(hWndMain, GWL_STYLE, 
		WS_POPUP|WS_CAPTION|WS_BORDER|WS_SYSMENU|WS_MINIMIZEBOX);
    }
    ShowWindow(hWndMain, SW_SHOW);

Next, we need to fill in the D3DPRESENT_PARAMETERS structure differently:

        // Get the current desktop display mode
        D3DDISPLAYMODE d3ddm;
        if (FAILED(g_pD3D->GetAdapterDisplayMode(D3DADAPTER_DEFAULT, &d3ddm)))
            return FALSE;
        d3dpp.Windowed = TRUE;
        d3dpp.SwapEffect = D3DSWAPEFFECT_DISCARD;
        d3dpp.BackBufferFormat = d3ddm.Format;

Afterwards, we need to move the window to our final location:

    if (!bFullScreen)
    {
        // Set up main window to be 640x480, not sizable
        int cxScreen = GetSystemMetrics(SM_CXSCREEN);
        int cyScreen = GetSystemMetrics(SM_CYSCREEN);

        RECT    r;
        
        r.left = (cxScreen - 640) / 2;
        r.top = (cyScreen - 480) / 2;
        r.right = r.left + 640;
        r.bottom = r.top + 480;
        AdjustWindowRect(&r, 
		WS_POPUP|WS_CAPTION|WS_BORDER|WS_SYSMENU|WS_MINIMIZEBOX, FALSE);
        SetWindowPos(hWndMain, NULL, r.left, r.top, 
		r.right - r.left, r.bottom - r.top, SWP_NOZORDER);
    }

That's it. We now have mode switching support and alpha blending, all at roughly the same speed as our DirectDraw application; and it's not a lot more difficult, either.

You now have everything you need to make your own 2-D DirectX-based application, be it game or multi-media presentation. Have fun - and watch this space!

 

Copyright © David McCabe, 1998 - 2001. All rights reserved.

You will need to download and install the m-math control to display any equations on this Web site. Without this control, you will not see most of the equations. Please do not e-mail me asking why the equations do not display!