Friday 6 July 2012

A Basic Framework

It’s been a pretty heavy week for me learning and re learning C++. Having been a C# developer for so many years, going back to C++ has been…interesting. From the perspective of a C# developer taking up C++, you realize just how lazy you can be in C#, C++ does not afford you that luxury, you have to keep your eye on the ball at all times.

I am pretty sure that the post(s) I am writing now, will have a lot of C++ faux pa’s in as well as DirectX11 ones too, but these are my creations after a week of spare time using C++ and DirectX 11, so please, if you are a solid C++ and/or DirectX 11 person, don’t judge me to harshly, if you see I have done something particularly daft, then please point it out in the comments and we can all learn from my mistakes, but be nice :P

I’d also like to thank at this point Ed Powel for all his help. Ed is another blogger from the old XNA-UK site, but his day job is C++, so I have leant on him a fair bit, and I will continue to do so, thanks Ed.

With that caveat, Ill begin…

My first few posts, while work as a whole are kind of my working out as I have been going through this process and I dare say, will continue to be, but in this post I am going to try and set out a simple framework that I can then use moving forward.

Where to start.. 

I am going to start with a new project, for a proto engine if you like, same sequence of events we used two posts ago for creating the project, this time, once you have set the Include and Library  folders we need to add the following for this project to the Linker->Input->Additional Dependencies section:

image

Where are we going to start with this proto engine framework? I think a good place to start with when coming up with a framework is a definition of interfaces. As Ed points out in his new blog on the subject, (where I am cross posting these by the way :P) there are a few ways of doing it, I am using Microsoft’s __interface extension in my code as I am targeting Microsoft platforms.

At this point I only have a few, one to describe my graphics device object, one to describe a world object, one to describe my camera objects, one to describe an object that has an update method and finally one to describe an object that can be rendered or drawn. I dare say these will change as I learn more, but that’s great, it will just lead to more blog posts :)

Create a header fill and call it Interfaces, this is where we will keep them all.

Add the following headers

#pragma once
#include <Windows.h>
#include <D3D11.h>
#include <D3DX11.h>
#include <xnamath.h>

Now we will create the interfaces.

IRC3DGraphicsDevice

__interface IRC3DGraphicsDevice
{
    HRESULT Initialize();

    D3D_DRIVER_TYPE getDriverType();
    D3D_FEATURE_LEVEL getFeatureLevel();

    ID3D11RasterizerState* getRasterizer();
    IDXGISwapChain* getSwapChain();
    ID3D11RenderTargetView* getRenderTerget();
    ID3D11DepthStencilView* getDepthStencilView();
    D3D11_VIEWPORT getViewPort();
    ID3D11Texture2D* getDepthStencil();
    ID3D11Device* getDevice();
    ID3D11DeviceContext* getDeviceContext();

    HWND getWindowHandle();

    void Clear(XMFLOAT4 Color, UINT clearFlag,FLOAT depth, UINT8 stencil);
    void Present(UINT sysncInterval, UINT flags);

    HWND InitializeWindow(LPCTSTR str_Title,int int_XPos, int int_YPos, int int_Width, int int_Height, WNDPROC WinProc, int colorBrush = COLOR_BTNFACE);
};

So in here, we have an initialization method, functions to get various member variables (I know, C++ does not have Properties!!), a method to clear the device etc..

IRC3DBaseObject

__interface IRC3DBaseObject
{
    virtual XMFLOAT3 getPosition();
    virtual XMFLOAT3 getScale();
    virtual XMVECTOR getOrientation();
    virtual XMMATRIX getWorld();

    virtual void setPosition(XMFLOAT3 newPosition);
    virtual void setScale(XMFLOAT3 newScale);
    virtual void setOrientation(XMVECTOR newOrientation);
    
    virtual void Translate(XMFLOAT3 distance);
    virtual void Rotate(XMVECTOR axis, float angle);
};

This interface has most of the  things we will need to place and object in the 3D world, position, orientation, a way to translate and rotate the object etc..

IRC3DCamera

__interface IRC3DCamera
{
    XMMATRIX getProjection();
    XMMATRIX getView();

    void setWorldUp(XMFLOAT3 up);
    void setWidth(UINT newWidth);
    void setHeight(UINT newHeight);
};

In here, two get methods to get the cameras projection and view matrix and some setters for world up and dimensions.

IUpdateable

__interface IUpdateable
{
    void Update(float time);
};

In this interface an Update method, I am passing a float for elapsed time, but at this point, I am not using it, and it may well get dropped all together.

IDrawable

__interface IDrawable
{
    void Draw(float time, ID3D11DeviceContext* context, IRC3DCamera* camera);    
};

Finally in this last interface, we have a draw call described, looking at this as I write up the post, I should probably replace the ID3D11DeviceContext with my new IRC3DGraphicsDevice interface, bit for this set of posts it will do fine.

RC3DGraphicsDevice

We have already created a device in earlier post, so Ill just cover the bits I have added so that, that  code will play well with this framework. Create a new class called RC3DGraphicsDevice, this time we are going to also specify that we are going to derive from a base class, now I know it’s an interface, we can put our interface in here too

image

If you take a look in the RC3DGraphicsDevice header file, it will look something like this

#pragma once
#include "interfaces.h"
class RC3DGraphicsDevice :
    public IRC3DGraphicsDevice
{
public:
    RC3DGraphicsDevice(void);
    ~RC3DGraphicsDevice(void);
};

As you can see, the wizard has added out include file, and set our class up so that it is implementing our interface. Now, personally I don’t like that layout, so Ill tidy it up a bit. We can now populate our class definition with the methods from the interface. In C# you can right click the instance and tell the IDE to do it all for you, as far as I can see in VS2012 RC for a C++ project, you  can’t, so I have copied and pasted it from the Interfaces.h file.

#pragma once
#include "interfaces.h"

class RC3DGraphicsDevice : public IRC3DGraphicsDevice
{
protected:

public:
    RC3DGraphicsDevice(void);
    ~RC3DGraphicsDevice(void);

    HRESULT Initialize();

    D3D_DRIVER_TYPE getDriverType();
    D3D_FEATURE_LEVEL getFeatureLevel();

    ID3D11RasterizerState* getRasterizer();
    IDXGISwapChain* getSwapChain();
    ID3D11RenderTargetView* getRenderTerget();
    ID3D11DepthStencilView* getDepthStencilView();
    D3D11_VIEWPORT getViewPort();
    ID3D11Texture2D* getDepthStencil();
    ID3D11Device* getDevice();
    ID3D11DeviceContext* getDeviceContext();

    HWND getWindowHandle();

    void Clear(XMFLOAT4 Color, UINT clearFlag,FLOAT depth, UINT8 stencil);
    void Present(UINT sysncInterval, UINT flags);

    HWND InitializeWindow(LPCTSTR str_Title,int int_XPos, int int_YPos, int int_Width, int int_Height, WNDPROC WinProc, int colorBrush = COLOR_BTNFACE);
};

We now need to back these methods up with some member variables and the actual function bodies. First of all we need the variables for the methods to work with, add a protected section to the class and add them in like this

protected:

    D3D_DRIVER_TYPE driverType;
    ID3D11RasterizerState* pRasterState;
    IDXGISwapChain* pSwapChain;
    ID3D11RenderTargetView* pRenderTargetView;
    ID3D11DepthStencilView* pDepthStencilView;
    D3D11_VIEWPORT viewPort;
    D3D_FEATURE_LEVEL  featureLevel;
    ID3D11Texture2D* pDepthStencil;
    ID3D11Device*  pd3dDevice;
    ID3D11DeviceContext* pImmediateContext;

    HWND hWnd;

So, now we create and populate the function bodies in the RC3DGraphicsDevice.cpp file

#include "RC3DGraphicsDevice.h"

RC3DGraphicsDevice::RC3DGraphicsDevice(void){ }


RC3DGraphicsDevice::~RC3DGraphicsDevice(void){ }

// need to link d3d11.lib
HRESULT RC3DGraphicsDevice::Initialize()
{
    HRESULT hr = S_OK;

    RECT rc;
    GetClientRect( hWnd, &rc );
    UINT width = rc.right - rc.left;
    UINT height = rc.bottom - rc.top;

    UINT createDeviceFlags = 0;
#ifdef _DEBUG
    createDeviceFlags |= D3D11_CREATE_DEVICE_DEBUG;
#endif

    D3D_DRIVER_TYPE driverTypes[] =
    {
        D3D_DRIVER_TYPE_HARDWARE,
        D3D_DRIVER_TYPE_WARP,
        D3D_DRIVER_TYPE_REFERENCE,
    };
    UINT numDriverTypes = ARRAYSIZE( driverTypes );

    D3D_FEATURE_LEVEL featureLevels[] =
    {
        D3D_FEATURE_LEVEL_11_0,
        D3D_FEATURE_LEVEL_10_1,
        D3D_FEATURE_LEVEL_10_0,
    };
    UINT numFeatureLevels = ARRAYSIZE( featureLevels );

    DXGI_SWAP_CHAIN_DESC sd;
    ZeroMemory( &sd, sizeof( sd ) );
    sd.BufferCount = 1;
    sd.BufferDesc.Width = width;
    sd.BufferDesc.Height = height;
    sd.BufferDesc.Format = DXGI_FORMAT_R8G8B8A8_UNORM;
    sd.BufferDesc.RefreshRate.Numerator = 60;
    sd.BufferDesc.RefreshRate.Denominator = 1;
    sd.BufferUsage = DXGI_USAGE_RENDER_TARGET_OUTPUT;
    sd.OutputWindow = hWnd;
    sd.SampleDesc.Count = 1;
    sd.SampleDesc.Quality = 0;
    sd.Windowed = TRUE;

    for( UINT driverTypeIndex = 0; driverTypeIndex < numDriverTypes; driverTypeIndex++ )
    {
        driverType = driverTypes[driverTypeIndex];
        hr = D3D11CreateDeviceAndSwapChain( NULL, driverType, NULL, createDeviceFlags, featureLevels, numFeatureLevels,
                                            D3D11_SDK_VERSION, &sd, &pSwapChain, &pd3dDevice, &featureLevel, &pImmediateContext );
        if( SUCCEEDED( hr ) )
            break;
    }
    if( FAILED( hr ) )
        return hr;

    // Create a render target view
    ID3D11Texture2D* pBackBuffer = NULL;
    hr = pSwapChain->GetBuffer( 0, __uuidof( ID3D11Texture2D ), ( LPVOID* )&pBackBuffer );
    if( FAILED( hr ) )
        return hr;

    hr = pd3dDevice->CreateRenderTargetView( pBackBuffer, NULL, &pRenderTargetView );
    pBackBuffer->Release();

    if( FAILED( hr ) )
        return hr;

    // Create depth stencil texture
    D3D11_TEXTURE2D_DESC descDepth;
    ZeroMemory( &descDepth, sizeof(descDepth) );
    descDepth.Width = width;
    descDepth.Height = height;
    descDepth.MipLevels = 1;
    descDepth.ArraySize = 1;
    descDepth.Format = DXGI_FORMAT_D24_UNORM_S8_UINT;
    descDepth.SampleDesc.Count = 1;
    descDepth.SampleDesc.Quality = 0;
    descDepth.Usage = D3D11_USAGE_DEFAULT;
    descDepth.BindFlags = D3D11_BIND_DEPTH_STENCIL;
    descDepth.CPUAccessFlags = 0;
    descDepth.MiscFlags = 0;
    hr = pd3dDevice->CreateTexture2D( &descDepth, NULL, &pDepthStencil );
    if( FAILED( hr ) )
        return hr;

    // Create the depth stencil view
    D3D11_DEPTH_STENCIL_VIEW_DESC descDSV;
    ZeroMemory( &descDSV, sizeof(descDSV) );
    descDSV.Format = descDepth.Format;
    descDSV.ViewDimension = D3D11_DSV_DIMENSION_TEXTURE2D;
    descDSV.Texture2D.MipSlice = 0;
    hr = pd3dDevice->CreateDepthStencilView( pDepthStencil, &descDSV, &pDepthStencilView );
    if( FAILED( hr ) )
        return hr;

    D3D11_RASTERIZER_DESC rasterDesc;
    rasterDesc.AntialiasedLineEnable = false;
    rasterDesc.CullMode = D3D11_CULL_BACK;
    rasterDesc.DepthBias = 0;
    rasterDesc.DepthBiasClamp = 0.0f;
    rasterDesc.DepthClipEnable = true;
    rasterDesc.FillMode = D3D11_FILL_SOLID;
    rasterDesc.FrontCounterClockwise = false;
    rasterDesc.MultisampleEnable = false;
    rasterDesc.ScissorEnable = false;
    rasterDesc.SlopeScaledDepthBias = 0.0f;

    // Create the rasterizer state from the description we just filled out.
    hr = pd3dDevice->CreateRasterizerState(&rasterDesc, &pRasterState);
    if(FAILED(hr))
    {
        return hr;
    }

    // Now set the rasterizer state.
    pImmediateContext->RSSetState(pRasterState);

    // Setup the viewport    
    viewPort.Width = (FLOAT)width;
    viewPort.Height = (FLOAT)height;
    viewPort.MinDepth = 0.0f;
    viewPort.MaxDepth = 1.0f;
    viewPort.TopLeftX = 0;
    viewPort.TopLeftY = 0;
    pImmediateContext->RSSetViewports( 1, &viewPort );

    pImmediateContext->OMSetRenderTargets( 1, &pRenderTargetView, pDepthStencilView );

    return S_OK;
}

D3D_DRIVER_TYPE RC3DGraphicsDevice::getDriverType()
{
    return driverType;
}
D3D_FEATURE_LEVEL RC3DGraphicsDevice::getFeatureLevel()
{
    return featureLevel;
}

ID3D11RasterizerState* RC3DGraphicsDevice::getRasterizer()
{
    return pRasterState;
}
IDXGISwapChain* RC3DGraphicsDevice::getSwapChain()
{
    return pSwapChain;
}
ID3D11RenderTargetView* RC3DGraphicsDevice::getRenderTerget()
{
    return pRenderTargetView;
}
ID3D11DepthStencilView* RC3DGraphicsDevice::getDepthStencilView()
{
    return pDepthStencilView;
}
D3D11_VIEWPORT RC3DGraphicsDevice::getViewPort()
{
    return viewPort;
}
ID3D11Texture2D* RC3DGraphicsDevice::getDepthStencil()
{
    return pDepthStencil;
}
ID3D11Device* RC3DGraphicsDevice::getDevice()
{
    return pd3dDevice;
}
ID3D11DeviceContext* RC3DGraphicsDevice::getDeviceContext()
{
    return pImmediateContext;
}

HWND RC3DGraphicsDevice::getWindowHandle()
{
    return hWnd;
}

void RC3DGraphicsDevice::Clear(XMFLOAT4 Color, UINT clearFlag,FLOAT depth, UINT8 stencil)
{
    //
    // Clear the back buffer
    //
    float ClearColor[4] = { Color.x, Color.y, Color.z, Color.w }; // red, green, blue, alpha
    pImmediateContext->ClearRenderTargetView( pRenderTargetView, ClearColor );

    //
    // Clear the depth buffer to 1.0 (max depth)
    //
    pImmediateContext->ClearDepthStencilView( pDepthStencilView, clearFlag, depth, stencil );
}
void RC3DGraphicsDevice::Present(UINT sysncInterval, UINT flags)
{
    //
    // Present our back buffer to our front buffer
    //
    pSwapChain->Present( sysncInterval, flags );
}

HWND RC3DGraphicsDevice::InitializeWindow(LPCTSTR str_Title,int int_XPos, int int_YPos, int int_Width, int int_Height, WNDPROC WinProc, int colorBrush)
{
    // Register class
    WNDCLASSEX wnd_Structure;

    wnd_Structure.cbSize = sizeof(WNDCLASSEX);
    wnd_Structure.style = CS_HREDRAW | CS_VREDRAW;
    wnd_Structure.lpfnWndProc = WinProc;
    wnd_Structure.cbClsExtra = 0;
    wnd_Structure.cbWndExtra = 0;
    wnd_Structure.hInstance = GetModuleHandle(NULL);
    wnd_Structure.hIcon = NULL;
    wnd_Structure.hCursor = NULL;
    wnd_Structure.hbrBackground = GetSysColorBrush(colorBrush);
    wnd_Structure.lpszMenuName = NULL;
    wnd_Structure.lpszClassName = L"WindowClassName";
    wnd_Structure.hIconSm = LoadIcon(NULL,IDI_APPLICATION);    

    if( !RegisterClassEx( &wnd_Structure ) )
        return HWND(-1);

    hWnd = CreateWindowEx(WS_EX_CONTROLPARENT, L"WindowClassName", str_Title, WS_OVERLAPPED | WS_CAPTION | WS_SYSMENU | WS_MINIMIZEBOX | WS_VISIBLE | WS_SIZEBOX | WS_MAXIMIZEBOX, int_XPos, int_YPos, int_Width, int_Height, NULL, NULL, GetModuleHandle(NULL), NULL);

    return hWnd;
}

Now, this GraphicsDevice class is a bit ridged, in that a lot of the elements in the Initialize we would want  to pass them in as parameters or have them as objects in there own right, but for this simple framework, this will suffice.

RC3DBaseObject

We need some way of representing objects in the world, so again, create a class called RC3DBaseObject, and set the base class to IRC3DBaseObject

image

We are also going to implement the IUpdateable interface

#pragma once
#include "interfaces.h"

class RC3DBaseObject :  public IRC3DBaseObject, public IUpdateable
{

As before, copy over the function stubs from the interface, and put some member variables in place.

#pragma once
#include "interfaces.h"

class RC3DBaseObject :  public IRC3DBaseObject, public IUpdateable
{
protected:

    XMFLOAT3 position;
    XMFLOAT3 scale;
    XMVECTOR orientation;

    XMMATRIX world;

public:
    RC3DBaseObject(void);
    ~RC3DBaseObject(void);

    virtual XMFLOAT3 getPosition();
    virtual XMFLOAT3 getScale();
    virtual XMVECTOR getOrientation();
    virtual XMMATRIX getWorld();

    virtual void setPosition(XMFLOAT3 newPosition);
    virtual void setScale(XMFLOAT3 newScale);
    virtual void setOrientation(XMVECTOR newOrientation);
    
    virtual void Translate(XMFLOAT3 distance);
    virtual void Rotate(XMVECTOR axis, float angle);

    void Update(float time);
};

Now to the RC3DBaseObject.cpp file and populate the function bodies.

#include "RC3DBaseObject.h"

RC3DBaseObject::RC3DBaseObject(void)
{
    position = XMFLOAT3(0,0,0);
    scale = XMFLOAT3(1,1,1);
    orientation = XMQuaternionIdentity();
}


RC3DBaseObject::~RC3DBaseObject(void){ }

XMFLOAT3 RC3DBaseObject::getPosition()
{
    return position;
}
XMFLOAT3 RC3DBaseObject::getScale()
{
    return scale;
}
XMVECTOR RC3DBaseObject::getOrientation()
{
    return orientation;
}
XMMATRIX RC3DBaseObject::getWorld()
{
    return world;
}

void RC3DBaseObject::setPosition(XMFLOAT3 newPosition)
{
    position = newPosition;
}
void RC3DBaseObject::setScale(XMFLOAT3 newScale)
{
    scale = newScale;
}
void RC3DBaseObject::setOrientation(XMVECTOR newOrientation)
{
    orientation = newOrientation;
}

void RC3DBaseObject::Update(float time)
{
    world = XMMatrixScaling(scale.x,scale.y,scale.z) * XMMatrixRotationQuaternion(orientation) * XMMatrixTranslation(position.x,position.y,position.z);
}

void RC3DBaseObject::Translate(XMFLOAT3 distance)
{
    XMVECTOR vDistance = XMLoadFloat3(&distance);

    XMFLOAT3 outPos;
    
    XMVECTOR v = XMVector3TransformCoord(vDistance, XMMatrixRotationQuaternion(orientation));

    XMVECTOR orgPos = XMLoadFloat3(&position);

    orgPos += v;

    XMStoreFloat3(&position,orgPos);
}
void RC3DBaseObject::Rotate(XMVECTOR axis, float angle)
{
    orientation = XMQuaternionMultiply(XMQuaternionRotationAxis(axis,angle), orientation);
}

As you can see we are using the default constructor (ctor) to pre populate the position, scale and orientation.

Also, my Translate and Rotate methods, taken from my XNA implementation, I guess I could have made the distance parameter a XMVECTOR to save a parse, and I think I need to look into the XMFLOATn and XMVECTOR data types.

RC3DCamera

You can’t really render any 3D goodies without some sort of camera, so we will create one now based on the interface we have already defined. This camera class is taken from my XNA samples and ported to C++, it was a pretty simple thing to do really. Now as I keep harping on, I am new to DirectX11, so I kind of figured out the objects and methods I needed to use, so if there is a better way of doing some of the stuff I am doing, then please, let me know.

Again we will create a new class called RC3DCamera and set the base class to IRC3DCamera

image

We are also going to derive from the RC3DBaseObject class, this way our camera class will have all the goodies from that base class.

And again we can copy the method stubs over, this time we are going to add a (ctor) override, we are also going to override the Update method from the RC3DBaseObject class

#pragma once
#include "interfaces.h"
#include "RC3DBaseObject.h"

class RC3DCamera : public RC3DBaseObject, public IRC3DCamera
{
protected:

    XMFLOAT3 worldUpVector;

    XMMATRIX view;
    XMMATRIX projection;

    UINT width;
    UINT height;

public:
    RC3DCamera(void);
    RC3DCamera(XMFLOAT3 pos,XMFLOAT3 worldUp,XMVECTOR rot, UINT width,UINT height);
    ~RC3DCamera(void);

    virtual XMMATRIX getProjection();
    virtual XMMATRIX getView();

    virtual void setWorldUp(XMFLOAT3 up);
    virtual void setWidth(UINT newWidth);
    virtual void setHeight(UINT newHeight);

    virtual void Update(float time);
};

And again we need to get some member variable to back them up.

protected:
    XMFLOAT3 worldUpVector;

    XMMATRIX view;
    XMMATRIX projection;

    UINT width;
    UINT height;

Now, to add the function bodies in the RC3DCamera.cpp file :)

#include "RC3DCamera.h"

RC3DCamera::RC3DCamera(void){ }

RC3DCamera::RC3DCamera(XMFLOAT3 pos, XMFLOAT3 worldUp, XMVECTOR rot,UINT screenWidth,UINT screenHeight)
{
    position = pos;
    worldUpVector = worldUp;
    orientation = rot;
    width = screenWidth;
    height = screenHeight;
}
RC3DCamera::~RC3DCamera(void)
{
}
XMMATRIX RC3DCamera::getProjection()
{
    return projection;
}
XMMATRIX RC3DCamera::getView()
{
    return view;
}
void RC3DCamera::Update(float time)
{
    XMMATRIX rotMat;

    rotMat = XMMatrixRotationQuaternion(orientation) * XMMatrixTranslation(position.x,position.y,position.z);
    XMVECTOR md = XMMatrixDeterminant(rotMat);    
    view = XMMatrixInverse(&md,rotMat);    

    projection = XMMatrixPerspectiveFovLH(XM_PI/4.0f,(float)width/(float)height,.1f,10000);    
}

void RC3DCamera::setWorldUp(XMFLOAT3 up)
{
    worldUpVector = up;
}
void RC3DCamera::setWidth(UINT newWidth)
{
    width = newWidth;
}
void RC3DCamera::setHeight(UINT newHeight)
{
    height = newHeight;
}

Now we have a simple framework to work with, in the next post Ill put a primitive game class together and we can then create a test object and render it. What we have covered here wont compile by the way, unless you implement a WinMain function, if you do you will get the following compiler error(s)

image

So, next post we will get rendering some stuff, and we can even get to take a look at our first DirectX 11 shader :)

As ever, comments are more than welcome…

2 comments: