In this chapter, you finalize the foundation of your rendering engine. You take your first look at the Direct3D C++ API and write an initial Direct3D application. Along the way, you revisit some previously discussed topics, including the DirectX swap chain and depth/stencil buffering.
1. Create the Direct3D device and device context interfaces.
2. Check for multisampling support.
3. Create the swap chain.
4. Create a render target view.
5. Create a depth-stencil view.
6. Associate the render target view and depth-stencil view with the output-merger stage of the Direct3D pipeline.
7. Set the viewport.
The next few sections cover each of these steps.
A Direct3D device represents a display adapter, an abstraction of the underlying graphics capability of your computer. Your PC likely contains several adapters, including the graphics card itself (fast) and adapters that are implemented entirely in software (slow). A device is represented with the ID3D11Device
interface, which creates resources and enumerates the capabilities of the display adapter.
A device context also represents the display adapter, but within a particular setting. The device context is encapsulated in the ID3D11DeviceContext
interface, which generates rendering commands using the resources of a device. You create a Direct3D device through the function D3D11CreateDevice()
whose prototype and parameters are listed here:
HRESULT WINAPI D3D11CreateDevice(
IDXGIAdapter* pAdapter,
D3D_DRIVER_TYPE DriverType,
HMODULE Software,
UINT Flags,
CONST D3D_FEATURE_LEVEL* pFeatureLevels,
UINT FeatureLevels,
UINT SDKVersion,
ID3D11Device** ppDevice,
D3D_FEATURE_LEVEL* pFeatureLevel,
ID3D11DeviceContext** ppImmediateContext );
pAdapter: A pointer to the display adapter that this device will represent. Pass a value of
NULL
to use the default adapter. You can enumerate the available adapters with a call to IDXGIFactory::EnumAdapters()
.
DriverType: The driver type to use with the device. Specify
D3D_DRIVER_TYPE_HARDWARE
to use the actual video card. This offers the best performance. However, you could specify D3D_DRIVER_TYPE_REFERENCE
or D3D_DRIVER_TYPE_WARP
for software implementations provided by Microsoft. See the online documentation for a complete list of driver types. All the work in this book uses the hardware driver.
Software: A handle to a third-party DLL that implements Direct3D in software. This parameter is used in conjunction with a driver type of
D3D_DRIVER_TYPE_SOFTWARE
.
Flags: Options to use when creating the Direct3D device. This parameter consists of bitwise OR’d values from the
D3D11_CREATE_DEVICE_FLAG
enumeration. A common option is D3D11_CREATE_DEVICE_DEBUG
to create a device with debugging capability.
pFeatureLevels: A pointer to an array of targeted feature levels, listed in order of preference from most to least preferred. Feature levels describe the capabilities of a device and roughly correspond to a version of Direct3D. If you specify a value of NULL, you get the highest feature level the device supports—unless that device supports Direct3D version 11.1. In that scenario, you see support for only version 11.0, not version 11.1. You must explicitly list
D3D_FEATURE_LEVEL_11_1
in the pFeatureLevels array if you want to target Direct3D 11.1. Furthermore, if you specify 11.1 as a targeted feature level and the device doesn’t support it, the D3D11CreateDevice()
function fails. Table 11.1 lists the possible feature levels.
FeatureLevels: Specifies the number of elements in the pFeatureLevels array.
SDKVersion: States the version of the SDK. You will always specify
D3D11_SDK_VERSION
.
ppDevice: Returns the created device.
pFeatureLevel: Returns the selected feature level.
ppImmediateContext: Returns the created device context.
Listing 11.1 gives an example of the D3D11CreateDevice()
call.
Listing 11.1 An Example Call to D3D11CreateDevice()
HRESULT hr;
UINT createDeviceFlags = 0;
#if defined(DEBUG) || defined(_DEBUG)
createDeviceFlags |= D3D11_CREATE_DEVICE_DEBUG;
#endif
D3D_FEATURE_LEVEL featureLevels[] = {
D3D_FEATURE_LEVEL_11_0,
D3D_FEATURE_LEVEL_10_1,
D3D_FEATURE_LEVEL_10_0
};
ID3D11Device* direct3DDevice;
D3D_FEATURE_LEVEL selectedFeatureLevel;
ID3D11DeviceContext* direct3DDeviceContext;
if (FAILED(hr = D3D11CreateDevice(NULL, D3D_DRIVER_TYPE_HARDWARE,
NULL, createDeviceFlags, featureLevels, ARRAYSIZE(featureLevels),
D3D11_SDK_VERSION, &direct3DDevice, &selectedFeatureLevel,
&direct3DDeviceContext)))
{
throw GameException("D3D11CreateDevice() failed", hr);
}
Multisample Anti-Aliasing (MSAA) is a technique to improve image quality. Aliasing refers to the jagged look of a line or triangle edge when presented on a computer monitor. It occurs because monitors use discrete points of light (pixels) to display virtual objects. As small as a pixel might be, it is not infinitesimal. Anti-aliasing refers to techniques to remove these “jaggies.” Figure 11.1 shows an example of a shape before and after anti-aliasing is applied.
Figure 11.1 An example of a shape rendered before (top) and after (bottom) anti-aliasing is applied.
Reducing the size of each pixel can ameliorate aliasing, so increasing your monitor’s resolution can help. But even at your monitor’s maximum resolution, aliasing artifacts might still be unacceptable. Supersampling is a technique that reduces the effective size of a pixel by increasing the resolution of the render target. Thus, multiple subpixels are sampled and averaged together (in a process known as downsampling) to produce the final color of each pixel displayed. A render target four times (4x) larger than the display resolution is commonly used for super-sampling, although higher resolutions can be used at the cost of memory and performance. With supersampling, the pixel shader is executed for each subpixel. Thus, to produce a render target 4x larger than the display, the pixel shader must run 4x as often. Multisampling is an optimization of supersampling that executes the pixel shader just once per display resolution pixel (at the pixel’s center) and uses that color for each of the subpixels. All Direct3D 11 devices are required to support 4x MSAA, although they can support even higher-quality levels. You can query the available quality levels through ID3D11Device::CheckMultisampleQuality-Levels();
the prototype and parameters are listed here:
HRESULT CheckMultisampleQualityLevels(
DXGI_FORMAT Format,
UINT SampleCount,
UINT *pNumQualityLevels);
Format: The texture format. Commonly, you specify this parameter as
DXGI_FORMAT_R8G8B8A8_UNORM
, a four-component format with 8 bits for each of the red, green, blue, and alpha channels. However, you can choose from many texture formats. Visit the online documentation for details.
SampleCount: The number of samples per pixel (for example, 4 for 4x multisampling).
pNumQualityLevels: The number of quality levels supported for the given format and sample count. The actual meaning of “quality level” can differ among hardware manufacturers. Generally, higher quality levels mean better results at higher performance cost. A returned value of
0
indicates that this device does not support the combination of format and sample count.
The multisampling quality values are used during swap chain creation, so a code example is deferred to the next section.
A swap chain is a set of buffers used to display frames to the monitor. As discussed back in Chapter 4, “Hello, Shaders!,” double buffering refers to a swap chain containing two buffers, a front buffer and a back buffer. The front buffer is what’s actually displayed while the back buffer is being filled with new data. When the application presents the newly completed back buffer, the two are swapped and the process starts over.
You create the swap chain by first populating an instance of the DXGI_SWAP_CHAIN_DESC1
structure, whose members are presented and described here:
typedef struct DXGI_SWAP_CHAIN_DESC1
{
UINT Width;
UINT Height;
DXGI_FORMAT Format;
BOOL Stereo;
DXGI_SAMPLE_DESC SampleDesc;
DXGI_USAGE BufferUsage;
UINT BufferCount;
DXGI_SCALING Scaling;
DXGI_SWAP_EFFECT SwapEffect;
DXGI_ALPHA_MODE AlphaMode;
UINT Flags;
} DXGI_SWAP_CHAIN_DESC1;
Width: The resolution width.
Height: The resolution height.
Format: The display format of the back buffer, e.g.
DXGI_FORMAT_R8G8B8A8_UNORM
.
Stereo: Whether the back buffer will be used for stereoscopic rendering.
SampleDesc: Multisampling parameters. When multisampling is enabled, this member is populated with data validated through
ID3D11Device::CheckMultisampleQualityLevels()
.
BufferUsage: Buffer usage and CPU access options. For example, the back buffer is commonly used for render target output (
DXGI_USAGE_RENDER_TARGET_OUTPUT
) but can also be used for shader input (DXGI_USAGE_SHADER_INPUT
).
BufferCount: The number of back buffers to include in the swap chain. A value of
1
indicates double buffering (a front buffer and one back buffer). A value of 2
indicates two back buffers (triple buffering).
Scaling: The resize behavior when the size of the back buffer is not equal to the target output (such as with a window). For example, a value of
DXGI_SCALING_STRETCH
scales the image to fit the output.
SwapEffect: How the contents of a buffer are handled after being presented.
DXGI_SWAP_EFFECT_DISCARD
is the most efficient of the options and is the only one that supports multisampling.
AlphaMode: The transparency behavior of the back buffer.
Flags: Options to use when creating the swap chain. This value consists of bitwise OR’d values from the
D3D11_SWAP_CHAIN_FLAG
enumeration. Visit the online documentation for a list of available options.
Listing 11.2 presents the code for populating a DXGI_SWAP_CHAIN_DESC1
instance and the aforementioned CheckMultisampleQualityLevels()
call.
Listing 11.2 Populating a Swap Chain Description Structure
mDirect3DDevice->CheckMultisampleQualityLevels(D
XGI_FORMAT_R8G8B8A8_UNORM,
mMultiSamplingCount, &mMultiSamplingQualityLevels);
if (mMultiSamplingQualityLevels == 0)
{
throw GameException("Unsupported multi-sampling quality");
}
DXGI_SWAP_CHAIN_DESC1 swapChainDesc;
ZeroMemory(&swapChainDesc, sizeof(swapChainDesc));
swapChainDesc.Width = mScreenWidth;
swapChainDesc.Height = mScreenHeight;
swapChainDesc.Format = DXGI_FORMAT_R8G8B8A8_UNORM;
if (mMultiSamplingEnabled)
{
swapChainDesc.SampleDesc.Count = mMultiSamplingCount;
swapChainDesc.SampleDesc.Quality = mMultiSamplingQualityLevels - 1;
}
else
{
swapChainDesc.SampleDesc.Count = 1;
swapChainDesc.SampleDesc.Quality = 0;
}
swapChainDesc.BufferUsage = DXGI_USAGE_RENDER_TARGET_OUTPUT;
swapChainDesc.BufferCount = 1;
swapChainDesc.SwapEffect = DXGI_SWAP_EFFECT_DISCARD;
Notice that the code in Listing 11.2 omits explicit assignment of the fields Stereo
, Scaling
, AlphaMode
, and Flags
. This is possible because the “zero” values align with the default values for these members.
You can define your swap chain as either full screen or windowed. To create a full-screen swap chain, you must populate an instance of the DXGI_SWAP_CHAIN_FULLSCREEN_DESC
structure; its members are presented and described next:
typedef struct DXGI_SWAP_CHAIN_FULLSCREEN_DESC
{
DXGI_RATIONAL RefreshRate;
DXGI_MODE_SCANLINE_ORDER ScanlineOrdering;
DXGI_MODE_SCALING Scaling;
BOOL Windowed;
} DXGI_SWAP_CHAIN_FULLSCREEN_DESC;
RefreshRate: Describes the refresh rate of the display, in hertz. The
DXGI_RATIONAL
structure has two fields, Numerator
and Denominator
, both unsigned integers. If your refresh rate were, for example, 60Hz, you would specify 60
for the numerator and 1
for the denominator.
ScanlineOrdering: Indicates how the monitor should draw the image. For example,
DXGI_MODE_SCANLINE_ORDER_PROGRESSIVE
specifies that the image should be created one line after another without skipping any (noninterlaced scanning).
Scaling: Indicates how the image is stretched to fit the monitor’s resolution. For example,
DXGI_MODE_SCALING_CENTERED
specifies that the image should be centered on the display and that no scaling should be performed.
Windowed: Specifies whether the swap chain is in windowed mode.
Listing 11.3 presents the code for populating a DXGI_SWAP_CHAIN_FULLSCREEN_DESC
instance.
Listing 11.3 Populating a Full-Screen Description Structure
DXGI_SWAP_CHAIN_FULLSCREEN_DESC fullScreenDesc;
ZeroMemory(&fullScreenDesc, sizeof(fullScreenDesc));
fullScreenDesc.RefreshRate.Numerator = mFrameRate;
fullScreenDesc.RefreshRate.Denominator = 1;
fullScreenDesc.Windowed = !mIsFullScreen;
With swap chain and full-screen descriptions ready, you can create the swap chain through a call to IDXGIFactory2::CreateSwapChainForHwnd()
.The prototype and parameters are listed here:
HRESULT CreateSwapChainForHwnd(
IUnknown *pDevice,
HWND hWnd,
const DXGI_SWAP_CHAIN_DESC1 *pDesc,
const DXGI_SWAP_CHAIN_FULLSCREEN_DESC *pFullscreenDesc,
IDXGIOutput *pRestrictToOutput,
IDXGISwapChain1 **ppSwapChain);
pDevice: The Direct3D device.
hWnd: The window handle to associate with the swap chain. This is the return value of the
CreateWindow()
call.
pDesc: The swap chain description structure.
pFullscreenDesc: The full-screen description structure. You can set this to
NULL
to use a windowed swap chain, or you can set the Windowed
member of the description structure to true
.
pRestrictToOutput: Enables you to restrict content to a specific output target. If this parameter is set to
NULL
, the output is not restricted.
ppSwapChain: Returns the created swap chain.
Before examining a code sample, notice that the CreateSwapChainForHwnd()
function is a member of the IDXGIFactory2
interface, which no section thus far has explicitly defined. DXGI is the DirectX Graphics Infrastructure and manages such tasks as presenting rendered frames to the screen and transitioning from windowed to full-screen display modes. These tasks are common across many APIs and are therefore independent of Direct3D. An IDXGIFactory2
object was instantiated when you created the Direct3D device, but you need to query the device to extract the required interface. You do this through the IUnknown::QueryInterface()
method. IUknown
is a Component Object Model (COM) interface, and COM interfaces have different acquisition and release patterns than typical C++ classes. After you query the interface, you call IUnknown::Release()
when you are finished with the object. Listing 11.4 presents the code required to acquire the IDXGIFactory2
instance, along with the call to CreateSwapChainForHwnd()
.
Listing 11.4 Creating a Swap Chain
#define ReleaseObject(object) if((object) != NULL) { object->Release();
object = NULL; }
IDXGIDevice* dxgiDevice = nullptr;
if (FAILED(hr = mDirect3DDevice->QueryInterface(__uuidof(IDXGIDevice),
reinterpret_cast<void**>(&dxgiDevice))))
{
throw GameException("ID3D11Device::QueryInterface() failed", hr);
}
IDXGIAdapter *dxgiAdapter = nullptr;
if (FAILED(hr = dxgiDevice->GetParent(__uuidof(IDXGIAdapter),
reinterpret_cast<void**>(&dxgiAdapter))))
{
ReleaseObject(dxgiDevice);
throw GameException("IDXGIDevice::GetParent() failed retrieving
adapter.", hr);
}
IDXGIFactory2* dxgiFactory = nullptr;
if (FAILED(hr = dxgiAdapter->GetParent(__uuidof(IDXGIFactory2),
reinterpret_cast<void**>(&dxgiFactory))))
{
ReleaseObject(dxgiDevice);
ReleaseObject(dxgiAdapter);
throw GameException("IDXGIAdapter::GetParent() failed retrieving
factory.", hr);
}
IDXGISwapChain1* mSwapChain;
if (FAILED(hr = dxgiFactory->CreateSwapChainForHwnd(dxgiDevice,
mWindowHandle, &swapChainDesc, &fullScreenDesc, nullptr, &mSwapChain)))
{
ReleaseObject(dxgiDevice);
ReleaseObject(dxgiAdapter);
ReleaseObject(dxgiFactory);
throw GameException("IDXGIDevice::CreateSwapChainForHwnd()
failed.", hr);
}
ReleaseObject(dxgiDevice);
ReleaseObject(dxgiAdapter);
ReleaseObject(dxgiFactory);
When you created the swap chain, you established a back buffer, a texture to render to. The intent is to bind that texture to the output-merger stage of the Direct3D pipeline. However, such resources aren’t bound directly to a pipeline stage; instead, you create a resource view of the texture and bind the view to the pipeline. You create a render target view with a call to ID3D11Device::CreateRenderTargetView()
; its prototype and parameters are listed next:
HRESULT CreateRenderTargetView(
ID3D11Resource *pResource,
const D3D11_RENDER_TARGET_VIEW_DESC *pDesc,
ID3D11RenderTargetView **ppRTView);
pResource: The render target resource.
pDesc: A render target view description structure that allows explicit definition of the render target view. Set this parameter to
NULL
to create a view that accesses all subresources at mip-level 0.
ppRTView: The created render target view.
You can query the texture resource for the CreateRenderTargetView()
function from the swap chain with a call to IDXGISwapChain::GetBuffer()
. Listing 11.5 presents the code for querying the swap chain and creating a render target view.
Listing 11.5 Creating a Render Target View
ID3D11Texture2D* backBuffer;
if (FAILED(hr = mSwapChain->GetBuffer(0, __uuidof(ID3D11Texture2D),
reinterpret_cast<void**>(&backBuffer))))
{
throw GameException("IDXGISwapChain::GetBuffer() failed.", hr);
}
if (FAILED(hr = mDirect3DDevice->CreateRenderTargetView(backBuffer,
nullptr, &mRenderTargetView)))
{
ReleaseObject(backBuffer);
throw GameException("IDXGIDevice::CreateRenderTargetView()
failed.", hr);
}
ReleaseObject(backBuffer);
Objects in a scene can overlap. An object nearer the camera can partially or completely occlude an object that is farther away. When a pixel is written to the render target, its depth (its distance from the camera) can be written to a depth buffer. The depth buffer is just a 2D texture, but instead of storing colors, it stores depths. The output-merger stage can use these depths to determine whether new pixels should overwrite existing colors already present in the render target. This process is known as depth testing; we discussed it briefly back in Chapter 1, “Introducing DirectX.”
Stencil testing uses a mask to determine which pixels to update. This is conceptually similar to a cardboard or plastic stencil you might use to paint or print a design on a physical surface. The depth and stencil buffers are attached, hence the terms depth-stencil buffer and depth-stencil view.
When you created the swap chain, you implicitly created the associated 2D texture for the back buffer, but a depth buffer was not created. You build a 2D texture for the depth buffer through a call to ID3D11Device::CreateTexture2D()
. This method follows the description structure pattern you used when creating the swap chain and requires you to populate an instance of the D3D11_TEXTURE2D_DESC
structure. The members of this structure are presented and described next:
typedef struct D3D11_TEXTURE2D_DESC
{
UINT Width;
UINT Height;
UINT MipLevels;
UINT ArraySize;
DXGI_FORMAT Format;
DXGI_SAMPLE_DESC SampleDesc;
D3D11_USAGE Usage;
UINT BindFlags;
UINT CPUAccessFlags;
UINT MiscFlags;
} D3D11_TEXTURE2D_DESC;
Width: The texture width.
Height: The texture height.
MipLevels: The number of mip-levels in the texture.
ArraySize: The number of textures in a texture array.
Format: The texture format. Unlike the back buffer format, which stores color, the depth buffer stores depth and stencil data. Common formats include
DXGI_FORMAT_D24_UNORM_S8_UINT
(a 24-bit depth buffer and an 8-bit stencil buffer) and DXGI_FORMAT_D32_FLOAT (
all 32 bits are for the depth buffer).
SampleDesc: Multisampling parameters.
Usage: How the texture will be read from and written to. Most commonly set to
D3D11_USAGE_DEFAULT
.
BindFlags:
D3D11_BIND_DEPTH_STENCIL
for a depth-stencil buffer.
CPUAccessFlags: Bitwise OR’d values that specify whether the CPU is allowed to read or write to the texture. Use
0
when no CPU access is required.
MiscFlags: Flags for less commonly used resource options. Not used for the depth-stencil buffer.
Listing 11.6 shows an example of populating an instance of the D3D11_TEXTURE2D_DESC
structure.
Listing 11.6 Populating a 2D Texture Description Structure
D3D11_TEXTURE2D_DESC depthStencilDesc;
ZeroMemory(&depthStencilDesc, sizeof(depthStencilDesc));
depthStencilDesc.Width = mScreenWidth;
depthStencilDesc.Height = mScreenHeight;
depthStencilDesc.MipLevels = 1;
depthStencilDesc.ArraySize = 1;
depthStencilDesc.Format = DXGI_FORMAT_D24_UNORM_S8_UINT;
depthStencilDesc.BindFlags = D3D11_BIND_DEPTH_STENCIL;
depthStencilDesc.Usage = D3D11_USAGE_DEFAULT;
if (mMultiSamplingEnabled)
{
depthStencilDesc.SampleDesc.Count = mMultiSamplingCount;
depthStencilDesc.SampleDesc.Quality = mMultiSamplingQualityLevels - 1;
}
else
{
depthStencilDesc.SampleDesc.Count = 1;
depthStencilDesc.SampleDesc.Quality = 0;
}
You use the D3D11_TEXTURE2D_DESC
structure instance with a call to ID3D11Device::CreateTexture2D()
, whose prototype and parameters are listed next:
HRESULT CreateTexture2D(
const D3D11_TEXTURE2D_DESC *pDesc,
const D3D11_SUBRESOURCE_DATA *pInitialData,
ID3D11Texture2D **ppTexture2D);
pDesc: The 2D texture description structure.
pInitialData: Initial data used to populate the texture. No initial data is required for the depth buffer, so this parameter is set to
NULL
.
ppTexture2D: Returns the created 2D texture.
Finally, the depth-stencil view is created with a call to ID3D11Device::CreateDepthStencilView()
. This method has a set of parameters analogous to those of CreateRenderTargetView()
. Listing 11.7 presents example code for creating a depth buffer and depth-stencil view.
Listing 11.7 Creating a Depth Buffer and Depth-Stencil View
ID3D11Texture2D* mDepthStencilBuffer;
ID3D11DepthStencilView* mDepthStencilView;
if (FAILED(hr = mDirect3DDevice->CreateTexture2D(&depthStencilDesc,
nullptr, &mDepthStencilBuffer)))
{
throw GameException("IDXGIDevice::CreateTexture2D() failed.", hr);
}
if (FAILED(hr = mDirect3DDevice->CreateDepthStencilView(mDepthStencil
Buffer, nullptr, &mDepthStencilView)))
{
throw GameException("IDXGIDevice::CreateDepthStencilView()
failed.", hr);
}
With the render target view and the depth-stencil view created, you can now bind them to the output-merger stage of the Direct3D pipeline with a call to ID3D11DeviceContext::OMSet-RenderTargets()
. The prototype and parameters of this method are listed next:
void OMSetRenderTargets(
UINT NumViews,
ID3D11RenderTargetView *const *ppRenderTargetViews,
ID3D11DepthStencilView *pDepthStencilView);
NumViews: The number of render targets to bind to the pipeline. Part IV, “Intermediate-Level Rendering Topics,” covers using multiple render targets. For now, you specify
1
to bind just a single render target.
ppRenderTargetViews: The (input) array of render targets to bind to the pipeline (must have the same number of elements specified in NumViews).
pDepthStencilView: The depth-stencil view to bind to the pipeline.
Listing 11.8 presents the code for binding the views to the output-merger stage.
Listing 11.8 Binding the Views to the Output-Merger Stage
mDirect3DDeviceContext->OMSetRenderTargets(1, &mRenderTargetView,
mDepthStencilView);
The final step of Direct3D initialization is to set a viewport. A viewport is a rectangular area that can encompass the entire back buffer, or some portion of it; your scenes are rendered to this area. Viewports are commonly used for split-screen multiplayer games, where different views of the game world are displayed to separate areas of the screen. You set the viewport by populating an instance of the D3D11_VIEWPORT
structure and calling ID3D11DeviceContext::RSSetViewports()
. The members of the D3D11_VIEWPORT
structure are described here:
typedef struct D3D11_VIEWPORT
{
FLOAT TopLeftX;
FLOAT TopLeftY;
FLOAT Width;
FLOAT Height;
FLOAT MinDepth;
FLOAT MaxDepth;
} D3D11_VIEWPORT;
TopLeftX, TopLeftY, Width, Height: Members that define the area of the viewport.
MinDepth, MaxDepth: The minimum and maximum values for the depth buffer. These values are typically set to
0.0
, and 1.0
, respectively.
The RSSetViewports()
method has just two parameters: the number of viewports to bind and an array of viewports. Listing 11.9 shows an example of creating a viewport structure and binding it to the pipeline.
Listing 11.9 Setting the Viewport
D3D11_VIEWPORT mViewport;
mViewport.TopLeftX = 0.0f;
mViewport.TopLeftY = 0.0f;
mViewport.Width = static_cast<float>(mScreenWidth);
mViewport.Height = static_cast<float>(mScreenHeight);
mViewport.MinDepth = 0.0f;
mViewport.MaxDepth = 1.0f;
mDirect3DDeviceContext->RSSetViewports(1, &mViewport);
With the specifics of Direct3D initialization in hand, you are ready to create your first Direct3D application. This entails augmenting the general-purpose Game
class to incorporate the Direct3D initialization phases, creating a specialized Game
class, and modifying the WinMain()
function to use the specialized game class. The next few sections describe these steps.
Listing 11.10 presents an abbreviated version of the Game.h
file. This code builds upon the window initialization code from Chapter 10, “Project Setup and Window Initialization.” Visit the book’s companion website for the full source code.
Listing 11.10 An Abbreviated Game.h
Header File
#pragma once
#include "Common.h"
#include "GameClock.h"
#include "GameTime.h"
namespace Library
{
class Game
{
public:
/* ... Previously presented members removed for brevity ... */
ID3D11Device1* Direct3DDevice() const;
ID3D11DeviceContext1* Direct3DDeviceContext() const;
bool DepthBufferEnabled() const;
bool IsFullScreen() const;
const D3D11_TEXTURE2D_DESC& BackBufferDesc() const;
const D3D11_VIEWPORT& Viewport() const;
protected:
/* ... Previously presented members removed for brevity ... */
virtual void InitializeDirectX();
static const UINT DefaultFrameRate;
static const UINT DefaultMultiSamplingCount;
D3D_FEATURE_LEVEL mFeatureLevel;
ID3D11Device1* mDirect3DDevice;
ID3D11DeviceContext1* mDirect3DDeviceContext;
IDXGISwapChain1* mSwapChain;
UINT mFrameRate;
bool mIsFullScreen;
bool mDepthStencilBufferEnabled;
bool mMultiSamplingEnabled;
UINT mMultiSamplingCount;
UINT mMultiSamplingQualityLevels;
ID3D11Texture2D* mDepthStencilBuffer;
D3D11_TEXTURE2D_DESC mBackBufferDesc;
ID3D11RenderTargetView* mRenderTargetView;
ID3D11DepthStencilView* mDepthStencilView;
D3D11_VIEWPORT mViewport;
};
}
As this code demonstrates, the Game
class now stores members for the Direct3D device and device context, multisampling settings, the depth-stencil buffer and view, the render target view, and the viewport. A few of these members are exposed through public accessors, and Direct3D initialization is performed through the InitializeDirectX()
method. Before presenting the implementation of these new Game
members, notice the include directive for Common.h
. This is a header file for commonly needed declarations (see Listing 11.11).
Listing 11.11 The Common.h
Header File
#pragma once
#include <windows.h>
#include <exception>
#include <cassert>
#include <string>
#include <vector>
#include <map>
#include <memory>
#include <d3d11_1.h>
#include <DirectXMath.h>
#include <DirectXPackedVector.h>
#define DeleteObject(object) if((object) != NULL) { delete object;
object = NULL; }
#define DeleteObjects(objects) if((objects) != NULL) { delete[]
objects; objects = NULL; }
#define ReleaseObject(object) if((object) != NULL) { object->Release();
object = NULL; }
namespace Library
{
typedef unsigned char byte;
}
using namespace DirectX;
using namespace DirectX::PackedVector;
The d3d11_1.h
, DirectXMath.h
and DirectXPackedVector.h
files include declarations for Direct3D 11.1 and DirectXMath. The DirectXMath types are part of the DirectX
and DirectX::PackedVector
namespaces, hence their inclusion.
Listing 11.12 presents the updated Game
class implementation. Again, this is an abbreviated listing of the file that omits the previously presented material and implementations of pass-through accessors.
Listing 11.12 An Abbreviated Game.cpp
File
#include "Game.h"
#include "GameException.h"
namespace Library
{
const UINT Game::DefaultScreenWidth = 1024;
const UINT Game::DefaultScreenHeight = 768;
const UINT Game::DefaultFrameRate = 60;
const UINT Game::DefaultMultiSamplingCount = 4;
Game::Game(HINSTANCE instance, const std::wstring& windowClass,
const std::wstring& windowTitle, int showCommand)
: mInstance(instance), mWindowClass(windowClass),
mWindowTitle(windowTitle), mShowCommand(showCommand),
mWindowHandle(), mWindow(),
mScreenWidth(DefaultScreenWidth), mScreenHeight(DefaultScreen
Height),
mGameClock(), mGameTime(),
mFeatureLevel(D3D_FEATURE_LEVEL_9_1),
mDirect3DDevice(nullptr),
mDirect3DDeviceContext(nullptr), mSwapChain(nullptr),
mFrameRate(DefaultFrameRate), mIsFullScreen(false),
mDepthStencilBufferEnabled(false),
mMultiSamplingEnabled(false),
mMultiSamplingCount(DefaultMultiSamplingCount),
mMultiSamplingQualityLevels(0),
mDepthStencilBuffer(nullptr), mRenderTargetView(nullptr),
mDepthStencilView(nullptr), mViewport()
{
}
void Game::Run()
{
InitializeWindow();
InitializeDirectX();
Initialize();
MSG message;
ZeroMemory(&message, sizeof(message));
mGameClock.Reset();
while(message.message != WM_QUIT)
{
if (PeekMessage(&message, nullptr, 0, 0, PM_REMOVE))
{
TranslateMessage(&message);
DispatchMessage(&message);
}
else
{
mGameClock.UpdateGameTime(mGameTime);
Update(mGameTime);
Draw(mGameTime);
}
}
Shutdown();
}
void Game::Shutdown()
{
ReleaseObject(mRenderTargetView);
ReleaseObject(mDepthStencilView);
ReleaseObject(mSwapChain);
ReleaseObject(mDepthStencilBuffer);
if (mDirect3DDeviceContext != nullptr)
{
mDirect3DDeviceContext->ClearState();
}
ReleaseObject(mDirect3DDeviceContext);
ReleaseObject(mDirect3DDevice);
UnregisterClass(mWindowClass.c_str(), mWindow.hInstance);
}
void Game::InitializeDirectX()
{
HRESULT hr;
UINT createDeviceFlags = 0;
#if defined(DEBUG) || defined(_DEBUG)
createDeviceFlags |= D3D11_CREATE_DEVICE_DEBUG;
#endif
D3D_FEATURE_LEVEL featureLevels[] = {
D3D_FEATURE_LEVEL_11_0,
D3D_FEATURE_LEVEL_10_1,
D3D_FEATURE_LEVEL_10_0
};
// Step 1: Create the Direct3D device and device context
interfaces
ID3D11Device* direct3DDevice = nullptr;
ID3D11DeviceContext* direct3DDeviceContext = nullptr;
if (FAILED(hr = D3D11CreateDevice(NULL, D3D_DRIVER_
TYPE_HARDWARE, NULL, createDeviceFlags, featureLevels,
ARRAYSIZE(featureLevels),D3D11_SDK_VERSION, &direct3DDevice,
&mFeatureLevel, &direct3DDeviceContext)))
{
throw GameException("D3D11CreateDevice() failed", hr);
}
if (FAILED(hr = direct3DDevice->QueryInterface(__
uuidof(ID3D11Device1), reinterpret_cast<void**>(&mDirect3DDevice))))
{
throw GameException("ID3D11Device::QueryInterface()
failed", hr);
}
if (FAILED(hr = direct3DDeviceContext->QueryInterface
(__uuidof(ID3D11DeviceContext1), reinterpret_cast<void**>
(&mDirect3DDeviceContext))))
{
throw GameException("ID3D11Device::QueryInterface()
failed", hr);
}
ReleaseObject(direct3DDevice);
ReleaseObject(direct3DDeviceContext);
// Step 2: Check for multisampling support
mDirect3DDevice->CheckMultisampleQualityLevels(DXGI_FORMAT_
R8G8B8A8_UNORM, mMultiSamplingCount, &mMultiSamplingQualityLevels);
if (mMultiSamplingQualityLevels == 0)
{
throw GameException("Unsupported multi-sampling quality");
}
DXGI_SWAP_CHAIN_DESC1 swapChainDesc;
ZeroMemory(&swapChainDesc, sizeof(swapChainDesc));
swapChainDesc.Width = mScreenWidth;
swapChainDesc.Height = mScreenHeight;
swapChainDesc.Format = DXGI_FORMAT_R8G8B8A8_UNORM;
if (mMultiSamplingEnabled)
{
swapChainDesc.SampleDesc.Count = mMultiSamplingCount;
swapChainDesc.SampleDesc.Quality =
mMultiSamplingQualityLevels - 1;
}
else
{
swapChainDesc.SampleDesc.Count = 1;
swapChainDesc.SampleDesc.Quality = 0;
}
swapChainDesc.BufferUsage = DXGI_USAGE_RENDER_TARGET_OUTPUT;
swapChainDesc.BufferCount = 1;
swapChainDesc.SwapEffect = DXGI_SWAP_EFFECT_DISCARD;
// Step 3: Create the swap chain
IDXGIDevice* dxgiDevice = nullptr;
if (FAILED(hr = mDirect3DDevice->QueryInterface(__
uuidof(IDXGIDevice), reinterpret_cast<void**>(&dxgiDevice))))
{
throw GameException("ID3D11Device::QueryInterface()
failed", hr);
}
IDXGIAdapter *dxgiAdapter = nullptr;
if (FAILED(hr = dxgiDevice->GetParent(__uuidof(IDXGIAdapter),
reinterpret_cast<void**>(&dxgiAdapter))))
{
ReleaseObject(dxgiDevice);
throw GameException("IDXGIDevice::GetParent() failed
retrieving adapter.", hr);
}
IDXGIFactory2* dxgiFactory = nullptr;
if (FAILED(hr = dxgiAdapter->GetParent(__uuidof(IDXGIFactory2),
reinterpret_cast<void**>(&dxgiFactory))))
{
ReleaseObject(dxgiDevice);
ReleaseObject(dxgiAdapter);
throw GameException("IDXGIAdapter::GetParent() failed
retrieving factory.", hr);
}
DXGI_SWAP_CHAIN_FULLSCREEN_DESC fullScreenDesc;
ZeroMemory(&fullScreenDesc, sizeof(fullScreenDesc));
fullScreenDesc.RefreshRate.Numerator = mFrameRate;
fullScreenDesc.RefreshRate.Denominator = 1;
fullScreenDesc.Windowed = !mIsFullScreen;
if (FAILED(hr = dxgiFactory->CreateSwapChainForHwnd(dxgiDevice,
mWindowHandle, &swapChainDesc, &fullScreenDesc, nullptr, &mSwapChain)))
{
ReleaseObject(dxgiDevice);
ReleaseObject(dxgiAdapter);
ReleaseObject(dxgiFactory);
throw GameException("IDXGIDevice::CreateSwapChainForHwnd()
failed.", hr);
}
ReleaseObject(dxgiDevice);
ReleaseObject(dxgiAdapter);
ReleaseObject(dxgiFactory);
// Step 4: Create the render target view
ID3D11Texture2D* backBuffer;
if (FAILED(hr = mSwapChain->GetBuffer(0, __
uuidof(ID3D11Texture2D), reinterpret_cast<void**>(&backBuffer))))
{
throw GameException("IDXGISwapChain::GetBuffer() failed.", hr);
}
backBuffer->GetDesc(&mBackBufferDesc);
if (FAILED(hr = mDirect3DDevice->CreateRenderTargetView
(backBuffer, nullptr, &mRenderTargetView)))
{
ReleaseObject(backBuffer);
throw GameException("IDXGIDevice::CreateRenderTargetView()
failed.", hr);
}
ReleaseObject(backBuffer);
// Step 5: Create the depth-stencil view
if (mDepthStencilBufferEnabled)
{
D3D11_TEXTURE2D_DESC depthStencilDesc;
ZeroMemory(&depthStencilDesc, sizeof(depthStencilDesc));
depthStencilDesc.Width = mScreenWidth;
depthStencilDesc.Height = mScreenHeight;
depthStencilDesc.MipLevels = 1;
depthStencilDesc.ArraySize = 1;
depthStencilDesc.Format = DXGI_FORMAT_D24_UNORM_S8_UINT;
depthStencilDesc.BindFlags = D3D11_BIND_DEPTH_STENCIL;
depthStencilDesc.Usage = D3D11_USAGE_DEFAULT;
if (mMultiSamplingEnabled)
{
depthStencilDesc.SampleDesc.Count =
mMultiSamplingCount;
depthStencilDesc.SampleDesc.Quality =
mMultiSamplingQualityLevels - 1;
}
else
{
depthStencilDesc.SampleDesc.Count = 1;
depthStencilDesc.SampleDesc.Quality = 0;
}
if (FAILED(hr = mDirect3DDevice->CreateTexture2D
(&depthStencilDesc, nullptr, &mDepthStencilBuffer)))
{
throw GameException("IDXGIDevice::CreateTexture2D()
failed.", hr);
}
if (FAILED(hr = mDirect3DDevice->CreateDepthStencilView
(mDepthStencilBuffer, nullptr, &mDepthStencilView)))
{
throw GameException("IDXGIDevice::CreateDepthStencilView()
failed.", hr);
}
}
// Step 6: Bind the render target and depth-stencil views to OM
stage
mDirect3DDeviceContext->OMSetRenderTargets(1,
&mRenderTargetView, mDepthStencilView);
mViewport.TopLeftX = 0.0f;
mViewport.TopLeftY = 0.0f;
mViewport.Width = static_cast<float>(mScreenWidth);
mViewport.Height = static_cast<float>(mScreenHeight);
mViewport.MinDepth = 0.0f;
mViewport.MaxDepth = 1.0f;
// Step 7: Set the viewport
mDirect3DDeviceContext->RSSetViewports(1, &mViewport);
}
The new code in Listing 11.12 includes the Game
class constructor and the Shutdown()
and InitializeDirectX()
methods. The constructor simply initializes all the class members; the Shutdown()
method releases Direct3D objects instantiated within InitializeDirectX()
. The Run()
method has been updated to call the DirectX initialization method. Recognize that the InitiaizeDirectX()
, Shutdown()
, and Run()
methods are marked virtual and can therefore be overridden in a derived class. The presented implementations cover general-purpose uses of the Game
class.
For all the demo projects, you’ll create a specialized version of the Game
class that resides within the executable project of your Visual Studio solution. Generally, this derived class overrides the general-purpose Initialize()
method and the Update()
and Draw()
methods invoked by the base class Run()
implementation. The base Game
class implementations of these three methods are currently empty. In the next chapter, we discuss the concept of game components, a modular approach for adding functionality to your applications. These methods will initialize, update, and draw the collection of active components.
Listing 11.13 presents the header file for the RenderingGame
class, which derives from the Library::Game
class. Create this class within your Game project, not the Library project, which houses the majority of your code thus far.
Listing 11.13 The RenderingGame.h
File
#pragma once
#include "Common.h"
#include "Game.h"
using namespace Library;
namespace Rendering
{
class RenderingGame : public Game
{
public:
RenderingGame(HINSTANCE instance, const std::wstring&
windowClass, const std::wstring& windowTitle, int showCommand);
~RenderingGame();
virtual void Initialize() override;
virtual void Update(const GameTime& gameTime) override;
virtual void Draw(const GameTime& gameTime) override;
private:
static const XMVECTORF32 BackgroundColor;
};
}
The only item to point out from this header file is the BackgroundColor
member of type XMVECTORF32
. This is a SIMD DirectXMath type that allows initializer syntax. Listing 11.14 shows the implementation of the RenderingGame
class.
Listing 11.14 The RenderingGame.cpp
File
#include "RenderingGame.h"
#include "GameException.h"
namespace Rendering
{
const XMVECTORF32 RenderingGame::BackgroundColor = { 0.392f,
0.584f, 0.929f, 1.0f };
RenderingGame::RenderingGame(HINSTANCE instance, const
std::wstring& windowClass, const std::wstring& windowTitle, int
showCommand)
: Game(instance, windowClass, windowTitle, showCommand)
{
mDepthStencilBufferEnabled = true;
mMultiSamplingEnabled = true;
}
RenderingGame::~RenderingGame()
{
}
void RenderingGame::Initialize()
{
Game::Initialize();
}
void RenderingGame::Update(const GameTime &gameTime)
{
Game::Update(gameTime);
}
void RenderingGame::Draw(const GameTime &gameTime)
{
mDirect3DDeviceContext->ClearRenderTargetView(mRenderTarget
View, reinterpret_cast<const float*>(&BackgroundColor));
mDirect3DDeviceContext->ClearDepthStencilView(mDepthStencil
View, D3D11_CLEAR_DEPTH | D3D11_CLEAR_STENCIL, 1.0f, 0);
Game::Draw(gameTime);
HRESULT hr = mSwapChain->Present(0, 0);
if (FAILED(hr))
{
throw GameException("IDXGISwapChain::Present() failed.", hr);
}
}
}
This class renders a blank screen in cornflower blue (or whatever color values you specify for the BackgroundColor
member). The code shows the general structure of the Game
-derived class, although you’ll actually extend the Initialize()
and Update()
methods in more interesting applications.
In the constructor of the RenderingGame
class, you enable the use of a depth-stencil buffer and multisampling (although you aren’t specifically using them in this example). In the Draw()
method, you clear the render target with a call to ID3D11DeviceContext1::Clear-RenderTargetView()
. This method sets the entire render target to the specified color. Next, you clear the depth-stencil buffer with a call to ID3D11DeviceContext1::ClearDepth-StencilView()
. The second parameter of this method is one or more D3D11_CLEAR_FLAG
options, bitwise OR’d together to specify which part of the depth-stencil buffer to clear. The third parameter specifies the clear value for the depth buffer (typically 1.0
), and the fourth parameter is the clear value for the stencil buffer (typically 0
).
After you clear the render and depth-stencil views, you perform any game-specific rendering calls. In Listing 11.14, the call to Game::Draw()
is intended to render any visible game components. Again, the next chapter covers the topic of game components. When all game-specific rendering has been completed, you flip the buffers in the swap chain with a call to IDXGISwapChain1::Present()
. The first parameter in the Present()
method specifies how the frame is presented with respect to the monitor’s vertical refresh. A value of 0
indicates that presentation should occur immediately, with no vertical refresh synchronization. The second parameter is a set of bitwise OR’d DXGI_PRESENT
flags. A value of 0
simply presents the frame to the output without any special options. You can find details on more advanced vertical synchronization and presentation settings in the online documentation.
The last step before running your application is to modify the WinMain()
function to use the new RenderingGame
class. The changes are simple; you include RenderingGame.h
instead of Game.h
and update the game
variable instantiation to the corresponding class. Listing 11.15 presents the code for the updated WinMain()
from Program.cpp
.
Listing 11.15 The Program.cpp
File
#include <memory>
#include "GameException.h"
#include "RenderingGame.h"
#if defined(DEBUG) || defined(_DEBUG)
#define _CRTDBG_MAP_ALLOC
#include <stdlib.h>
#include <crtdbg.h>
#endif
using namespace Library;
using namespace Rendering;
int WINAPI WinMain(HINSTANCE instance, HINSTANCE previousInstance,
LPSTR commandLine, int showCommand)
{
#if defined(DEBUG) | defined(_DEBUG)
_CrtSetDbgFlag(_CRTDBG_ALLOC_MEM_DF | _CRTDBG_LEAK_CHECK_DF);
#endif
std::unique_ptr<RenderingGame> game(new RenderingGame(instance,
L"RenderingClass", L"Real-Time 3D Rendering", showCommand));
try
{
game->Run();
}
catch (GameException ex)
{
MessageBox(game->WindowHandle(), ex.whatw().c_str(),
game->WindowTitle().c_str(), MB_ABORTRETRYIGNORE);
}
return 0;
}
Build and run your application; you should see the image in Figure 11.2.
Again, the results of your efforts are a bit anticlimactic, but most of the preliminary work is now complete and you’ve set the stage for rendering with Direct3D.
In this chapter, you learned how to initialize Direct3D. You took a first look at the Direct3D C++ API and finalized the foundation of your rendering engine. Additional supporting systems are left to develop, but you have successfully completed your first Direct3D application.
In the next chapter, you create interfaces for game components, develop mouse and keyboard input systems, build a dynamic 3D camera, and learn how to render 2D text to the screen.
1. From within the debugger, walk through the code used to initialize Direct3D, to better understand the initialization process. You’ll find an associated demo application on the book’s companion website.
18.116.87.250