[ 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)

This tutorial is going to demonstrate 2 totally unrelated techniques; Dot 3 Bumpmapping, and a DirectInput mouse device. I decided to combine these tutorials as a mouse tutorial on its own is pretty boring, and a Dot3 tutorial without any user interaction is also pretty boring. Because these topics are unrelated, this tutorial is split into halves. The first half will cover the Dot3 bumpmapping technique, the second will cover the DirectInput mouse. If you're dipping into this tutorial to only look for info on one of these topics, that's not a problem. The tutorial code keeps the bumpmapping and the mouse handling completely separate so you can split them apart without any effort.

That said, here's what we'll be coding today:


Fig 5.1: That's bumpmapped. Really. Honest, guv.


Click the mouse to change the background colour.

Bumpmapping is a little hard to describe if you've never seen it before, so here are 2 more screenshots. The idea behind this technique is to make a flat image appear bumpy ("perturbed" or "embossed"), and for the shadows/light to move as the light source shining upon it moves. These images are taken with the lightsource placed at 2 different positions (signified by the yellow arrow). Look closely at the "b", you should be able to see the difference.


Fig 5.2: Bumpmapped by light source


Admittedly it's a little hard to see, especially in a static jpeg. The Dot3 technique demonstrated in this tutorial is not the only method of bumpmapping - it's the simplest, but also the least visually effective. It's also limited to greyscale images. The advantage of it is that all the hard work is done for you by D3D, and almost all modern gfx cards support it. The "proper", more common bumpmapping technique is not nearly as widely supported. Just try the BumpEarth sample in the DirectX SDK; that one doesn't even work on my older GeForce 2.

Just to give you a comparison, this is the source image used to create the bumpmapping in Fig 5.1:


Fig 5.3: Source texture

Major difference, huh! When you build and run the tutorial, you'll notice that whilst the transformation from Fig 5.3 (above) to Fig 5.1 is quite major, the actual bumpmapping by light source that occurs is not so significant. In fact, although it is real bumpmapping (look at the curves on the letters to prove it), it does look more like simple modulation of the texture. That's because that is precisely what is happening. For that reason I prefer to use this technique for "embossing", and not for dynamic bump mapping that changes as the light source moves. That said, it's still an interesting technique to know.

To implement it, all we need to do is to create a vector that represents the light source (just as we did when implementing lighting in DX Basics Series 3), normalize the vector, then use the x, y and z components of the vector as the red, green and blue values for texture modulation. D3D then provides us with a specific Dot3 render state that takes care of all the calculations. One very important point is that the bumpmapping is dependant on the relative RGB difference between neighbouring pixels - the bigger the difference, the more noticeable the bumpmap.

If you haven't realised yet, this is very similar to the technique we used to create a heightmap for the terrain engine in Tutorial 1 of this series. There we used differences in the RGB values of a source texture to determine the height of vertices on screen. Here we are using those differences to alter the amount of perceived light that falls on each pixel.


For this tutorial we're going to use a DirectInput mouse device as the light source vector. The first question is, "What is DirectInput, and why do we need it?". Taking the answer directly from the SDK:

"Microsoft® DirectInput® is an application programming interface (API) for input devices including the mouse, keyboard, joystick, and other game controllers, as well as for force-feedback (input/output) devices. Apart from providing services for devices not supported by the Microsoft Win32® API, DirectInput gives faster access to input data by communicating directly with the hardware drivers rather than relying on Microsoft Windows® messages. DirectInput enables an application to retrieve data from input devices even when the application is in the background. It also provides full support for any type of input device, as well as for force feedback."


That's pretty clear. In plain English, DirectInput allows us to abstract the user input system so that we can handle pretty much any device available, and to get data from that device fast enough so that it's nice and responsive when controlling objects in a game. As you'll remember from the introduction to the Win32 messageloop all the way back in DirectX Basics Series 1 tutorial 1, WM_ messages take a (relatively) long time to make their way through the message loop. That's not ideal when you're hammering the arrow keys to escape some guy with a AWM in Counterstrike.

The DirectX SDK also says you should understand the following definitions:

DirectInput object: The root DirectInput interface.
Device: A keyboard, mouse, joystick, or other input device.
DirectInputDevice object: Code representing a keyboard, mouse, joystick, or other input device.
Device object: Code representing a key, button, trigger, and so on found on a DirectInput device object.


Remembering that is as clear as the old Microsoft "printer" definition (if you've ever done a Windows MCP exam, you'll know that a printer is what we call a driver, a driver is what we call a spooler, and a printing device is what we call a printer), so don't worry too much. To work with basic DirectInput you only need to remember 2 things - the DirectInput object is the COM object we create, and the DirectInput device is the physical hardware object we want to get input from.


At this point I'll go out on a limb and assume you know what a mouse is, and how to use it. The only thing we need to do next is to cover the steps to creating a DirectInput object, and using it to access a device.

The first task, just as with Direct3D, is to create a pointer to the DirectInput interface. Once we have a valid pointer, we create a device object to represent the device we want to receive input from (ie: the mouse). Next we tell DirectInput the "format" of the device and how the device should co-operate with the rest of Windows (in other words, whether other applications should be allowed to use the mouse whilst our application is using it). Once the device has been set up it is "acquired", which locks it to our application. We can then retrieve further properties from the device (in reality, from the drivers) and set further options such as scrolling sensitivity and cursor speed.

Before we look at the code I want to explain why we need to tell DirectInput the format of the device. It's very important to remember that Microsoft created DInput to be a generic abstraction layer that could provide an interface between our code and nearly every input device available on the market. That's a bigger job than it sounds - DInput doesn't only need to handle keyboards, mice, trackballs and joysticks, it needs to handle steering wheels, light guns, gloves and even movement sensitive 3D glasses. To acheive this, Microsoft had to keep the abstraction layer as, well, abstract as possible. By allowing the developer to specify the format of each DInput device, Microsoft ensured DInput can support any input device that comes with Windows drivers. This is a blessing and a curse rolled into one. It's great because the issue of compatibility is placed on the developers, not Microsoft. You can guarantee that if a game needed to support a full size bodysuit the developers would be much more eager to implement it than Microsoft would! The flipside of that flexibility is that creating data formats for a unique device can become a complex task, requiring the help of the hardware manufacturer.

Don't worry about all that though (unless you've somehow acquired a neoprene bodysuit with Windows drivers for you next project). DInput comes with a couple of pre-configured formats for generic keyboards, mice and joysticks to speed up development.

Time to look at the code!

[ Download complete VC6 workspace as .zip ]

Dot3DInput.cpp
[ View source code in this window ] [ View source code in a new window ] [ Source code in plain ascii .cpp file ]

DIMouse.cpp [ View source code in this window ] [ View source code in a new window ] [ Source code in plain ascii .cpp file ]

DIMouse.h [ View source code in this window ] [ View source code in a new window ] [ Source code in plain ascii .cpp file ]


First up we're going to look at how the Dot3 bumpmapping is done - we'll cover the DInput mouse later. Towards the bottom of the AppInit() function we create a vertex buffer and place a quad into it for texturemapping. This is the quad we will use for displaying the bumpmap on. I'm not going to recap the steps to code this - you should be very used to it by now (need a recap? Look at DirectX Basics Series 3 Tutorial 1). Run your eye over the code so you know which variables we're using.

Directly below the vertex buffer and quad creation code you'll see some texture loading code (I've removed the error checking code):

rslt=D3DXCreateTextureFromFileEx(g_pDevice, "bumpmap.png", 256, 256, 1, 0, D3DFMT_UNKNOWN,
                                 D3DPOOL_MANAGED, D3DX_DEFAULT, D3DX_DEFAULT, 0, NULL, NULL,
                                 &g_pBumpMapTexture);


// Create a brand new texture to receive our normal map

rslt=D3DXCreateTexture(g_pDevice, 256, 256, 1, D3DX_DEFAULT, D3DFMT_A8R8G8B8,
                       D3DPOOL_DEFAULT, &g_pNormalMapTexture);


// Calculate the normal map from the source png image, and write it to the normal map texture

rslt = D3DXComputeNormalMap(g_pNormalMapTexture, g_pBumpMapTexture, NULL, 0, D3DX_CHANNEL_RED, 1.0f);


Believe it or not, this is half the bumpmapping work completed. First we load a texture in the normal manner using D3DXCreateTextureFromFileEx(). We then create a second, new texture surface of the same dimensions (256*256) and bit depth (A8R8G8B8) as the texture we loaded from disk. The loaded texture contains the source of the bumpmap - the image in Fig 5.3 above - and the second texture will receive the normal map calculated from it.

To calculate the normal map, we simply call D3DXComputeNormalMap() and pass it the source texture for the normals and the destination texture to write the normal map to. This gives us 3 interesting questions - what is a normal map, why do we need it and why do we use D3DX_CHANNEL_RED?

I'll answer those questions in reverse. Here's the prototype for D3DXComputeNormalMap():

HRESULT D3DXComputeNormalMap( LPDIRECT3DTEXTURE8 pTexture, LPDIRECT3DTEXTURE8 pSrcTexture, const PALETTEENTRY *pSrcPalette, DWORD Flags, DWORD Channel, FLOAT Amplitude);


The purpose of this function is to create a normal map from a height map. I'll come back to what a normal map is in a second, but you should already know what a height map is - a greyscale bitmap where the RGB values of each pixel represent the "height" of a unit in object space (confused and need a refresher? DirectX Techniques Tutorial 2 explains it in detail). We use D3DX_CHANNEL_RED to specify which channel (images are made up of a combination of red, green, blue, alpha and luminance channels, depending on the file format used) we should take the height information from. Because our source bitmap is greyscale, the red, green and blue channels all contain the same information. Thus the answer is, it really makes no difference which channel we pick, I just chose red at random.

That explains the D3DX_CHANNEL_RED parameter, but why do we need a normal map? The entire basis of bumpmapping is lighting effects. Think about the real world for a second. The only time you see shadows on a surface is when a light is shining on different parts of the surface with varying intensities. The reason these intensities vary is because the angle the light hits the surface is not the same in all places. To produce a bumpmapping effect from our source texture, we need some way of telling D3D where the imperfections are on our surface. To do this, we create a normal map from the heightmap.

So now you should understand what a normal map is. It's a bitmap that contains the normal vectors for every pixel on the heightmap, which allows D3D to calculate exactly how light falls on each pixel of the source texture. At this point if you're a bit confused about how normals work, what they are and how they affect lighting, have a thorough read of DirectX Basics Series 3 Tutorial 5.

The final piece of the puzzle is how a normal map works. You may be wondering how D3D stores the normal vector for every heightmap pixel inside a standard texture. If you've read all the tutorials here on 32Bits, you already know the answer (we discussed it in the Terrain tutorial!). A vector consists of an X, Y and Z value (not sure about vectors either? No problem, the 32Bits Cheaters Guide to Maths will sort you out) which defines where it points in 3D space. An X, Y and Z value. Interesting, because every pixel of a texture has a red, green and blue value. Aha! All we need to do is to store the X value of the vector in the Red channel of the normal texture, the Y value in the Green channel and the Z value in the Blue channel. Hey presto, one texture containing the normals for every pixel in the heightmap.

You may be wondering why we don't just create a 256*256 element array, and store all the vectors in there. Good question, and we might if we did the calculations ourself. In this case D3D will be doing all the work for us via texture stages, which you'll see in a second.


The rest of the bumpmapping takes place in the Render() function. After we've set the vertex stream source and the FVF, we get on with the bumpmapping:

D3DXVECTOR3 vLight;
POINT kPoint;
g_pDIMouse->GetAbsolutePos(&kPoint);

// Create a vector from the cursor position (no Z axis for this, the mouse is in 2D)
vLight.x = (((float)kPoint.x / (float)g_D3DSettings.m_nDeviceWidth) - 1);
vLight.y = (((float)kPoint.y / (float)g_D3DSettings.m_nDeviceHeight) - 1);
vLight.z = 1.0f;


Don't worry about the call to GetAbsolutePos() at the moment - it simply retrieves the X and Y position of the cursor into a POINT struct, which is just a convenient way of storing an X & Y pixel position. By dividing the cursor position by the screen width and height (subtracting 1 because the screen dimensions are 800*600, starting at 0,0 which gives an extent of 799, 599) we create a vector from the cursor position. The Z value is set to 1.0 explicitly - the mouse only moves in 2 dimensions and we're using it for the light vector, but we still need to put a value in the Z component of the vector.

D3DXVec3Normalize( &vLight, &vLight );

DWORD r = (DWORD)(127.0f * vLight.x + 128.0f);
DWORD g = (DWORD)(127.0f * vLight.y + 128.0f);
DWORD b = (DWORD)(127.0f * vLight.z + 128.0f);

DWORD dwFactor = D3DCOLOR_XRGB(r, g, b);


I've removed the comments from the above code to save space. Next, we need to normalize the vector as it represents our lighting vector. For a recap of normalization and unit vectors, take another look at the DirectX Basics Series 3 Tutorial 5 and 32Bits Cheaters Guide to Maths. Once we've normalized it, we have to turn it back into a greyscale RGB value packed into a DWORD - the reason for this will become clear in a second. Because the vector is normalized the X & Y components will have a value between 0.0 and 1.0 (Z will always be 1.0, we set that explicitly earlier). To turn them into a 0 to 255 RGB value, we multiply by 127 and add 128 - try it yourself if you don't see why this works. When we have the individual RGB values, we pack them into a DWORD using the normal D3DCOLOR_XRGB macro.

g_pDevice->SetRenderState(D3DRS_TEXTUREFACTOR, dwFactor);

// Modulate the texture (the normal map) with the light vector (stored
// above in the texture factor). From the SDK: "Modulate the components of each argument
// as signed components, add their products; then replicate the sum to all color
// channels, including alpha".

g_pDevice->SetTextureStageState( 0, D3DTSS_COLORARG1, D3DTA_TEXTURE );
g_pDevice->SetTextureStageState( 0, D3DTSS_COLOROP, D3DTOP_DOTPRODUCT3 );
g_pDevice->SetTextureStageState( 0, D3DTSS_COLORARG2, D3DTA_TFACTOR );


Next, we set the texture factor value to the RGB value we just placed in the dwFactor variable. Finally we set a couple of texture stages. The input for colour argument 1 is the texture (which we set in a second), the input for colour argument 2 is the texture factor we just set in the previous call to SetRenderState() - in other words, the DWORD containing the light vector. The colour operation is D3DTOP_DOTPRODUCT3, which is what performs all the magic here. If you're unsure about how render and texture stage states work, have a read of DirectX Basics Series 3 Tutorial 4.

// Set the texture to be the normal map
g_pDevice->SetTexture( 0, g_pNormalMapTexture );

g_pDevice->DrawPrimitive(D3DPT_TRIANGLESTRIP, 0, 2);


We set the normal map texture (the one D3D generated for us when we called D3DXComputeNormalMap()) as the texture for stage 0, and then render our quad. That is all there is to Dot3 bumpmapping using Direct3D!

Okay, right now I'm going to bet that you are extremely confused (if you understood it, great - skip on down to the DInput mouse). You probably don't understand why we kept putting vectors into RGB values, and where all the actual bumpmapping happens.

The principle behind this is very, very simple. Just accept that D3D does all of the work for us, without exception. So the question is actually, "how does D3D get all the information it needs from that bit of code?". Look at the bumpmapping code in the Render() function again.

What we're actually doing throughout the code is setting up variables for D3D to use, we're not actually doing any bumpmapping ourself. Because Dot3 bumpmapping is actually based on per-pixel lighting, D3D has to perform the calculations as it processes each pixel of the normal map. This means the actual bumpmapping work has to take place within the transformation and lighting pipeline. The problem is that although normals and lights are represented by vectors, the D3D interface to the pipeline (texture stages) only accepts DWORDs that contain RGB values. So most of the work that we do is to somehow cram a bunch of vectors into the pipeline, rather than actually perform any calculations on them.

Here's the texture stage code excerpt again, with some other added comments.

Here we set dwFactor as an RGB value that will be used in the texture stage calculation below. Remember that dwFactor is not actually an RGB value, it's the normalized lighting vector packed into an RGB-type value.
g_pDevice->SetRenderState(D3DRS_TEXTUREFACTOR, dwFactor);

Next, the normal map texture is set as one of the 2 inputs for the texture stage calculation. The normal map contains all the normal vectors, one per pixel (because we've also used the RGB values of each pixel in it as a place to store the normal vectors). Remember that texture stage operations are done on EVERY pixel of a source texture, which is how every vector stored as an RGB value is processed.

g_pDevice->SetTextureStageState( 0, D3DTSS_COLORARG1, D3DTA_TEXTURE );

Next we set the colour stage operation to be a DOTPRODUCT3 calculation. In other words, the calculation that will be performed on the 2 inputs of this texture stage is a Dot3.

g_pDevice->SetTextureStageState( 0, D3DTSS_COLOROP, D3DTOP_DOTPRODUCT3 );

Finally the second argument/input for this texture stage is the texture factor value. We set this value in the call to SetRenderState() above - it's the dwFactor variable, our lighting vector packed into a DWORD.
g_pDevice->SetTextureStageState( 0, D3DTSS_COLORARG2, D3DTA_TFACTOR );


Hopefully now you can see that we're simply telling the T&L pipeline to perform a Dot3 operation on the lighting vector and every vector stored in the pixels of the normal map. D3D takes care of all the details for us, and a nicely bumpmapped texture pops out the other end of the pipeline!

 

That's the bumpmapping covered, so let's take a look at how the DirectInput mouse is handled. Here's the class definition:

class CDIMouse
{
public:
    CDIMouse();
    ~CDIMouse();

    HRESULT Initialise(HWND hWnd, int iWindowWidth, int iWindowHeight);
    BOOL Update();                            
    HRESULT Render(LPDIRECT3DDEVICE8 pDevice);

    int GetRelativeX();
    int GetRelativeY();
    int GetAbsoluteX();
    int GetAbsoluteY();
    void GetAbsolutePos(POINT* kMousePos=NULL);
    void SetHotSpot(int iX, int iY);

    BOOL IsMouseButtonDown(int iButton, POINT* kClickPoint=NULL);

private:
    LPDIRECTINPUT8 m_pDIObject;
    LPDIRECTINPUTDEVICE8 m_pDIMouseDevice;

    int m_iWindowWidth;
    int m_iWindowHeight;

    DIMOUSESTATE m_kMouseState;

    int m_iCursorX;
    int m_iCursorY;
    int m_iHotSpotX;
    int m_iHotSpotY;
};


It's a simple class. Before we look at the actual code, one piece of mouse theory to cover. In the code above you can see a few references to a "hotspot". A hotspot is the actual pixel offset from the top left of the cursor position at which a mouse action occurs.


Fig 5.4: Bounding rects & hotspots


Fig 5.4 shows 2 standard Windows cursors. The top 2 cursors have red "bounding boxes" around them, showing the complete size of the cursor. The bottom 2 cursors have red circles where their hotspots are. As you can see, the bounding rects are roughly the same size but the hotspots are in different places. Why is this an issue? You've probably guessed.


Fig 5.5: It's Micheal Barrymore's hotspot!


In Fig 5.5 the bounding box is clearly intersecting the icon, but the hotspot (the tip of the finger) is not. Therefore when you click the mouse, you would not expect to have clicked on the icon - initutively, the pointy part of the cursor (the finger in this case) is not over the icon. If we tracked cursor location purely on bounding boxes, our code would wrongly see this as a click on the icon. Therefore we need a way of allowing the hotspot to be located somewhere other than the top left pixel of the bounding box. Simple stuff, but important.

On the off chance that you're using the DX9 SDK to code DX8 apps, you need to be aware of 2 points. First, you must create the following define in a header file somewhere: #define DIRECTINPUT_VERSION 0x0800. Second, you must link your app against dxguid.lib. For more information on this, take a look at this section of the DX9 version of the tutorial.


Okay, time to look at the CDIMouse::Initialise() method. If you remember back to DirectX Basics Series 1 Tutorial 2, we looked at how the Direct3D COM object was initialized using the Direct3DCreate9() function. You'll be pleased to know that Microsoft decided not to help out anyone using DirectInput, so we have to create the COM object manually. This isn't as bad as it sounds. First we need to initialise DInput.

rslt = DirectInput8Create(GetModuleHandle(NULL), DIRECTINPUT_VERSION, IID_IDirectInput8, (void**)&m_pDIObject, NULL);


To create a DInput COM object & retrieve a pointer to the interface, we use the DirectInput8Create() function. This produces exactly the same result as calling the Direct3DCreate8() function for D3D, except it's a slightly more manual process. I'm not going to explain the parameters or use of this function, because you will always call it in the exact same way every time you want to initialise DInput. The only parameter you need to change is the 4th one, which is the variable that will receive the pointer to the DInput interface.

Once the DInput COM object is initialised, we need to create a DInput device for the mouse.

rslt = m_pDIObject->CreateDevice(GUID_SysMouse, &m_pDIMouseDevice, NULL);
if(FAILED(rslt)) { return D3DError(rslt, __LINE__, __FILE__, "Could not create DInput device"); }


For DInput control over the keyboard and mouse, this function is nice and easy. Parameter 1 is a REFGUID object representing the piece of hardware to receive input from. Parameter 2 is a pointer to an LPDIRECTINPUTDEVICE variable that will receive the pointer to the created IDirectInputDevice8 object. We use that interface used to interrogate the hardware device (scroll back up to the start of the tutorial for a quick reminder about the difference between the DirectInput object and a DirectInput device). Parameter 3 is always going to be set to NULL as it's used for COM aggregation, which is a topic I guarantee you do not want to get into.

A little more about parameter 1. If you're only interested in using the keyboard or mouse, you can supply the predefined GUID_SysMouse and GUID_SysKeyboard vars to this function. However if you want to use any device other than those, you'll need to enumerate all the DInput compatible devices on the system. There are 2 parts to doing this. First, you must create a callback function that DInput will call for every device found on the system. The callback function is prototyped as follows:

BOOL CALLBACK DIEnumDevicesCallback(LPCDIDEVICEINSTANCE lpddi, LPVOID pvRef);

Each time DInput calls this function, the lpddi var will be filled out with the details of one DInput compatible device. If you search the SDK for LPCDIDEVICEINSTANCE you'll see that this is actually a struct with a lot of useful members. The most important member is guidInstance, which is the GUID you need to supply to CreateDevice().

If you're unsure what a callback function is, Google is your best bet. Basically it is a function that your code never calls - its only purpose is to be called by another component of the libraries (or Windows itself) you are using, and to receive data. Windows uses callback functions a lot, especially when enumerating items. Normally the callback function will contain code to store the data passed to it (in an array, or stl::vector<> object), which the rest of the application can use.


To initialise the callback and enumeration process, IDirectInput8::EnumDevices() is called. The prototype is as follows:

HRESULT EnumDevices(DWORD dwDevType, LPDIENUMDEVICESCALLBACK lpCallback, LPVOID pvRef, DWORD dwFlags);

As we're slowly drifting further and further off-topic, take a look in the SDK for further information. In brief, you supply the type of devices you want to enumerate to parameter 1, a pointer to your callback function as parameter 2, parameter 3 is application defined and can be safely left as NULL, and parameter 4 is the state of the devices you want to enumerate (for example, you can enumerate either all DInput devices that have drivers installed, or only those that are physically connected & installed).

We only need the mouse for this tutorial though, so let's get back to the tutorial code.

rslt = m_pDIMouseDevice->SetDataFormat(&c_dfDIMouse);


Once the DInput device has been successfully created, we need to set the "format". This simply tells DInput how to handle a few characteristics of the device (acceleration, axis movement, sensitivity etc), and unless you're back to coding for that neoprene bodysuit you can use one of the predefined structs: c_dfDIMouse, c_dfDIKeyboard or c_dfDIJoystick. The SDK itself says that applications do not typically need to worry about creating new structures - just pick the appropriate one for the device you're using.

rslt = m_pDIMouseDevice->SetCooperativeLevel(hWnd, DISCL_EXCLUSIVE | DISCL_FOREGROUND);


Nearly done! Before actually using the device, we need to tell DInput how it should cooperate with the rest of Windows by calling SetCooperativeLevel(). This is pretty important as it affects how an application can get data from the device, and when it can get it. Parameter 1 is the HWND of the application using the DInput device - this is extremely important, and we can't just specify NULL here as we sometimes do for other functions that need an HWND. If the HWND supplied is destroyed before DInput is properly shut down, bad things happen! Parameter 2 is a combination of bitwise flags that specify the cooperation level between this device and the rest of Windows. The normal setting here is DISCL_EXCLUSIVE | DISCL_FOREGROUND, which makes sure we have access to the device whilst the application window is in the foreground & has the input focus. Not an issue in full-screen mode, but important in windowed mode. Incidentally you can also disable the Windows hotkey on a keyboard device by including the DISCL_NOWINKEY flag here.

rslt = m_pDIMouseDevice->Acquire();


We've finished setting up the device, so all that's left is to acquire it for our own use. Acquiring is the action of telling DInput that we want to start receiving input from it - if you've created a DInput device and you're not getting any data, make sure you've acquired it.

The remaining code in the Initialise() method is just to remember the size of the screen/window, to allow us to keep the mouse cursor within its boundaries. Next, let's look at the Update() method.

The reason we need an Update() method is because DInput works fundamentally differently to how Windows processes user input. Whereas with Windows and a normal message loop you must wait for a WM_ message (such as WM_MOUSEMOVE or WM_CLICK) to know that the mouse has moved, DInput doesn't tell you this has happened - you must ask DInput if it has. Therefore at every frame (ie: iteration of the code) in the application the code must interrogate DInput and ask if the mouse position has changed. If it has, the code must update the mouse position on screen accordingly - yes, we're responsible for tracking and drawing the cursor too!

The first task in the method is to check we still have access to the device. If the application window has been minimized or another window has gained the focus, we will have lost the device. This is pretty much the same principle as lost devices in D3D, and the same reason we call the ValidateDevice() function in the Render() function.

if(m_pDIObject == NULL || m_pDIMouseDevice == NULL)
    return FALSE;

// If we can't get the device state, we've lost the device so we need to try to get it back.
// This is very similar to a lost D3D device.

if(FAILED(m_pDIMouseDevice->GetDeviceState(sizeof(DIMOUSESTATE),(LPVOID)&m_kMouseState)))
{
    if(FAILED(m_pDIMouseDevice->Acquire()))
    {
        return FALSE;
    }
    else
    {
        if(FAILED(m_pDIMouseDevice->GetDeviceState(sizeof(DIMOUSESTATE),(LPVOID)&m_kMouseState)))
            return FALSE;
    }
}


First we check that the DInput device and object pointers are actually valid, in case the method has been called before DInput has been initialised. Next we check to see if we can get the device state of the DInput device. If GetDeviceState() succeeds, we can move on. If not, we need to try and reacquire the device. If we fail to acquire the device by calling Acquire(), we've definately lost it so we can abort the rest of the code and return from the function. If Acquire() succeeds, what probably happened was the app lost focus & then regained it so we just need to double check everything is OK by calling GetDeviceState() again.

Now that we've made sure we have the input device focus, we need to handle the actual movement updates of the mouse.

At this point I'm very slowly going to explain something that confuses a lot of people (especially some people on the 32Bits forums!). DInput does not provide absolute coordinates for the mouse position. DInput will only provide relative coordinates. In other words, when you ask DInput if the mouse has moved, it will say "The mouse moved left 10 units and down 1 unit", not "The mouse is now at 200, 320".

Note that I said "units", not "pixels". DInput doesn't give you any absolute measurements, so you can assume 1 unit is 10 pixels if you like. This would cause the mouse to jump across the screen, but you get the idea.

So if we can't obtain the absolute coordinates of the mouse, how do we know where it is at any point in time? It's easier than you think. Because we control the drawing of the cursor, the mouse is wherever we say it is! When we initialise the mouse we decide where it will start (for example, the exact center of the window - 400, 300). Next we ask DInput for the current cursor position information, which will be provided as "It's moved right 1 unit and up 4 units". We then add 1 to the X position of the mouse and subtract 4 from the Y position, and we have our absolute coordinates for the cursor - (401, 296)! We then use those coordinates to render the cursor as we need.

Okay, I said that DInput will only provide relative coordinates which isn't exactly true. You can get it to provide axis specific absolute coordinates, but then you have to play with the device settings which is all very pointless. For a couple of extra lines of code you get far more control!


All of this will become clear in a second. Take a look at the class definition again - you'll see 2 members, m_iCursorX and m_iCursorY. Now look in the constructor:

m_iCursorX = 0;
m_iCursorY = 0;


For this tutorial, I've decided that the cursor will ALWAYS start at the top left of the window. Whenever DInput tells us about a relative movement of the mouse, we simply add and subtract that movement from these initial values.

Now that the principle is out the way, here's the code:

m_iCursorX += m_kMouseState.lX;

if(m_iCursorX < 0)
    m_iCursorX = 0;

if(m_iCursorX > m_iWindowWidth)
    m_iCursorX = m_iWindowWidth;

m_iCursorY += m_kMouseState.lY;

if(m_iCursorY < 0)
    m_iCursorY = 0;

if(m_iCursorY > m_iWindowHeight)
    m_iCursorY = m_iWindowHeight;


Very simple, we just add the relative movement retrieved from the GetDeviceState() call to the m_iCursorX and m_iCursorY tracker variables and make sure they stay within the screen boundaries. Just to state the obvious, for a movement from left to right DInput returns a positive value, for right to left movement DInput returns a negative value.

I'm going to skip the rest of the class methods as they're all very simple, and move on to the CDIMouse::IsMouseButtonDown() method. On your way down the code take a quick look at the Render() method - using the IDirect3DDevice8::Clear() method is a quick and handy way to render a rect on-screen without worrying about vertices or sprites.

BOOL CDIMouse::IsMouseButtonDown(int iButton, POINT* kClickPoint)
{
    if(m_kMouseState.rgbButtons[iButton] & 0xF0)
    {
        if(kClickPoint != NULL)
        {
            kClickPoint->x = m_iCursorX + m_iHotSpotX;
            kClickPoint->y = m_iCursorY + m_iHotSpotY;
        }
        return TRUE;
    }


There are 2 potentially confusing parts to this. First, the m_kMouseState.rgbButtons[iButton] code. iButton is passed as a variable to this parameter from the main code to indicate which button is to be checked for a click. If you look at the top of DIMouse.h you'll see a couple of defines (DIMOUSE_LEFTBUTTON etc) which can be passed for this parameter. You don't need the defines, it just makes it easier to remember which value corresponds to which button.

I said value, but I actually mean "array element". The rgbButtons member of the (DIMOUSESTATE) m_kMouseState struct is actually an array of BYTEs, where each element corresponds to the state of one mouse button. Element 0 is the left mouse button, element 1 is the right mouse button and so on. To retrieve the button state, we need to check the BYTE in the corresponding element. According to the SDK, the high order bit of the byte is set (has a value other than 0) when the button is down, which means checking it is basic bit testing. Read up on your logical operations if you're unsure about this, but if we AND the BYTE with 0xF0 the result will be true if the high order bit has a value other than 0, and false if it is set to zero.

The remainder of the code is also simple. If the kClickPoint parameter is a valid pointer to a POINT struct it is filled out with the current position of the cursor (taking the hotspot into account), otherwise we just return TRUE.

That's the class covered, let's take a look at the main code (Dot3DInput.cpp) and see how the mouse is implemented. The first thing to notice is that I created a global pointer to a CDIMouse class, instead of an explicit object:

CDIMouse* g_pDIMouse;


I didn't do this for any technical reason other than personal preference - you may not always want to have a mouse cursor hanging around (especially if it's in foreground exclusive mode), so creating it on the heap makes it easier to destroy as required. At the bottom of AppInit():

g_pDIMouse = new CDIMouse;
g_pDIMouse->Initialise(g_hWnd, g_D3DSettings.m_nDeviceWidth, g_D3DSettings.m_nDeviceHeight);


No big surprises there. Next, we've finally found a use for the AppLoop() function!

HRESULT AppLoop()
{
    // Update the mouse position
    g_pDIMouse->Update();

    return Render();
}


And it only took 4 series and 20 tutorials :). It is important to note that the call to update the mouse information is in AppLoop() and not in Render() - if you're unsure why, we covered the logical application/game loop back in DirectX Basics Series 1. Properly managing devices and user input should not depend on the render function being called & completing successfully.

In AppShutdown() we've added a line of code to destroy the mouse object. It's very important to do this with DInput devices, as very funny things might start happening if you don't deallocate them properly.

delete g_pDIMouse;


Finally, the Render() function. Near the top I've added a piece of code to demonstrate the button click testing:

if(g_pDIMouse->IsMouseButtonDown(DIMOUSE_LEFTBUTTON))
    g_pDevice->Clear(0,0, D3DCLEAR_TARGET | D3DCLEAR_ZBUFFER, D3DCOLOR_XRGB(0,100,55), 1.0f, 0);
else
    g_pDevice->Clear(0,0, D3DCLEAR_TARGET | D3DCLEAR_ZBUFFER, D3DCOLOR_XRGB(0,0,55), 1.0f, 0);


If the left mouse button is pressed the backbuffer gets cleared to green, otherwise the standard dark blue is used. And down the bottom of Render(), the last change:

g_pDIMouse->Render(g_pDevice);


Mustn't forget to render the cursor!

 

Well, congratulations! You've reached the end of the final tutorial in the DirectX Techniques series! If you've understood all the topics we've covered over the last 20 tutorials you are well on the way to becoming an expert D3D coder. As I've said many times before, once you know the basics it's all about applying the same theory to anything you want to create. Don't be afraid to try new things & write code for crazy ideas, it's the best way to learn. And as always, the 32Bits Forums are there to help.

We will be back with a brand new DirectX tutorial series just as soon as we can, but meanwhile - good luck with the coding!

 

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