DirectX是众所周知的比较难以入门的3D渲染引擎,除了概念比较抽象以外,还有一点是程序的逻辑比较难以处理。
经过比较长时间的探索,我发现程序逻辑方面的难点主要在几个方面:第一是庞大的结构体,第二是错误处理的姿势,第三是对象的释放。
第一个难点是庞大的结构体
这是学习DirectX 10/11面对的第一个结构体(WNDCLASSEX不算),其实这个结构体并不算大,但是看起来信息量有点大。
<code class="language-cpp">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 = g_hWnd;
sd.SampleDesc.Count = 1;
sd.SampleDesc.Quality = 0;
sd.Windowed = TRUE;
</code>
如果不习惯的话,可以使用C++的列表初始化语法进行初始化。要注意的是,如果有嵌套结构体,最好不要用嵌套的括号,而是先初始化内层结构体,再初始化外层结构体,这样可以方便在Visual Studio中使用“查看定义”功能进行实时参考。
<code class="language-cpp">DXGI_RATIONAL refrate = { 60, 1 };
DXGI_SAMPLE_DESC smpdesc = { 1, 0 };
DXGI_MODE_DESC bufdesc = {
width, height, refrate, DXGI_FORMAT_R8G8B8A8_UNORM, DXGI_MODE_SCANLINE_ORDER_UNSPECIFIED, DXGI_MODE_SCALING_UNSPECIFIED
};
DXGI_SWAP_CHAIN_DESC sd = { bufdesc, smpdesc, DXGI_USAGE_RENDER_TARGET_OUTPUT, 1, g_hwnd, TRUE, DXGI_SWAP_EFFECT_DISCARD, 0 };
</code>
另外,联合体在C++中只能初始化第一项,要想初始化其它项,必须用赋值操作。
<code class="language-cpp">D3D10_DEPTH_STENCIL_VIEW_DESC descDSV = { descDepth.Format, D3D10_DSV_DIMENSION_TEXTURE2D };
descDSV.Texture2D.MipSlice = 0; // Texture2D是匿名联合体的第三个成员,只能通过赋值初始化
</code>
第二个难点是错误处理的姿势
DirectX返回一个叫做HRESULT的错误码,其中>=0表示成功,用SUCCEEDED(hr)表示,<0表示错误,用FAILED(hr)表示。但是通过if (SUCCEEDED(hr))和if (FAILED(hr))处理错误,会使得程序流程严重改变,还会产生冗余代码,这使得编写程序变得非常痛苦。
其实DirectX返回错误值大多意味着严重错误,这时一般需要终止程序。但是又要保证程序可调试性强,这个条件下C++的异常机制就非常适合使用了。将DirectX返回值转换为C++异常,最简单的方法是if (FAILED(hr)) throw hr;。但是这样有时并不合适,最好使用强类型的异常。
为了让返回值转换为异常的过程变得简单,这里编写了一个“一行工具类”如下:
<code class="language-cpp">struct comexcept { explicit comexcept(HRESULT ret) : hr(ret) { if (FAILED(ret)) throw *this; } HRESULT hr; };
</code>
使用方法举例:
<code class="language-cpp">(comexcept)D3D10CreateDeviceAndSwapChain(NULL, D3D10_DRIVER_TYPE_WARP, NULL, 0, D3D10_SDK_VERSION, &sd, &g_swpch, &g_dev);
(comexcept)g_swpch->GetBuffer(0, __uuidof(ID3D10Texture2D), (LPVOID*)&pBackBuffer);
(comexcept)g_dev->CreateRenderTargetView(pBackBuffer, NULL, &g_rtv);
(comexcept)g_dev->CreateTexture2D(&descDepth, NULL, &g_dsbuf);
(comexcept)g_dev->CreateDepthStencilView(g_dsbuf, &descDSV, &g_dsv);
</code>
类似的,WinAPI一般返回0表示错误,也有相应的“一行工具类”:
<code class="language-cpp">struct zeroexcept { template <typename t> explicit zeroexcept(T ret) { if (ret == (T)0) throw *this; } };
</typename></code>
使用方法与上述DirectX工具类类似。
第三个难点是资源的释放
DirectX对象使用引用计数来管理生命周期,程序使用完毕需使用->Release()成员函数释放所有权,但是比较麻烦的地方在于,对象释放了以后便不再有效,对无效的对象释放会导致程序崩溃,为了避免这种情况,标准的释放对像流程如下:
<code class="language-cpp">if (g_dev)
{
g_dev->Release();
g_dev = NULL;
}
</code>
这段程序每个对象都打一遍是很痛苦的事情,为了减轻这种痛苦,可以定义一个“一行工具函数”:
<code class="language-cpp">template <typename t> inline void release(T& i){ if (i) { i->Release(); i = NULL; } }
</typename></code>
使用方法很简单,release(g_dev);即可。
虽然已经很方便了,但是这样做不到防止资源泄漏的效果。如果要做到没有资源泄露,那么就需要用到ATL的一个类模板:CComPtr<T>。
这个模板是用来包装COM对象指针的,由于DirectX也是基于COM的,因此也可以拿来用。只是要注意三点:
- &xxx只能用于初始化,不能用于初始化后取地址,初始化后若需要取地址,应取成员p的地址,即&xxx.p
- 手动释放直接赋值NULL即可
- 没有QueryInterface、AddRef、Release成员函数的对象不需要使用CComPtr<T>,直接裸指针即可
使用示例:
<code class="language-cpp">#include <atlbase.h> // CComPtr<t>
CComPtr<id3d10device> g_dev;
CComPtr<idxgiswapchain> g_swpch;
CComPtr<id3d10rendertargetview> g_rtv;
CComPtr<id3d10texture2d> g_dsbuf;
CComPtr<id3d10depthstencilview> g_dsv;
CComPtr<id3d10effect> g_effect;
ID3D10EffectTechnique *g_technique; // 不需要Release,因此不需要CComPtr<t>
CComPtr<id3d10inputlayout> g_inlayout;
CComPtr<id3d10buffer> g_vertexbuf;
CComPtr<id3d10rasterizerstate> g_raststate;
// 初始化
(comexcept)g_dev->CreateRenderTargetView(pBackBuffer, NULL, &g_rtv);
// 手动释放
pBackBuffer = NULL;
// 取地址
g_dev->OMSetRenderTargets(1, &g_rtv.p, g_dsv); // 注意是&g_rtv.p不是&g_rtv,后者只能用于初始化
</id3d10rasterizerstate></id3d10buffer></id3d10inputlayout></t></id3d10effect></id3d10depthstencilview></id3d10texture2d></id3d10rendertargetview></idxgiswapchain></id3d10device></t></atlbase.h></code>
一个简单的DirectX 10程序
绘制一个旋转的三角形。其中TODO: begin和TODO: end之间的程序可以替换为自己的程序,因此也算是一个模板程序。
<code>// main.fx - HLSL效果文件
// TODO: 编写效果文件 begin
float4x4 matWorld;
float4x4 matView;
float4x4 matProj;
struct VS_OUTPUT
{
float4 Pos : SV_POSITION;
float4 Color : COLOR;
};
VS_OUTPUT VS(float4 Pos : POSITION, float4 Color : COLOR)
{
VS_OUTPUT Out;
Out.Pos = mul(mul(mul(Pos, matWorld), matView), matProj);
Out.Color = Color;
return Out;
}
float4 PS(VS_OUTPUT In) : SV_Target
{
return In.Color;
}
technique10 Render
{
pass P0
{
SetVertexShader(CompileShader(vs_4_0, VS()));
SetGeometryShader(NULL);
SetPixelShader(CompileShader(ps_4_0, PS()));
}
}
// TODO: 编写效果文件 end
</code>
<code class="language-cpp">// main.cpp - 主程序
// 头文件和库
#pragma warning(disable: 4005) // macro redefinition
#undef UNICODE
#define UNICODE
#include <windows.h>
#include <d3d10.h>
#include <d3dx10.h>
#include <atlbase.h> // CComPtr<t>
#pragma comment(lib, "d3d10.lib")
#pragma comment(lib, "d3dx10.lib")
// CComPtr<t>注意三点:
// 初始化用&xxx,取地址用&xxx.p
// 手动释放直接赋值NULL
// 不需要Release的对象不需要使用CComPtr<t>
// 将值转换为异常的类
struct zeroexcept { template <typename t> explicit zeroexcept(T ret) { if (ret == (T)0) throw *this; } };
struct comexcept { explicit comexcept(HRESULT ret) : hr(ret) { if (FAILED(ret)) throw *this; } HRESULT hr; };
// 全局变量
HINSTANCE g_hinst;
HWND g_hwnd;
CComPtr<id3d10device> g_dev;
CComPtr<idxgiswapchain> g_swpch;
CComPtr<id3d10rendertargetview> g_rtv;
CComPtr<id3d10texture2d> g_dsbuf;
CComPtr<id3d10depthstencilview> g_dsv;
// TODO: 其它全局定义 begin
struct SimpleVertex
{
D3DXVECTOR3 Pos;
D3DXCOLOR Color;
};
CComPtr<id3d10effect> g_effect;
ID3D10EffectTechnique *g_technique; // 不需要Release,因此不需要CComPtr<t>
CComPtr<id3d10inputlayout> g_inlayout;
CComPtr<id3d10buffer> g_vertexbuf;
CComPtr<id3d10rasterizerstate> g_raststate;
// TODO: 其它全局定义 end
// 窗口消息处理函数
LRESULT CALLBACK WndProc(HWND hWnd, UINT message, WPARAM wParam, LPARAM lParam)
{
if (message == WM_DESTROY)
{
PostQuitMessage(0);
return 0;
}
return DefWindowProc(hWnd, message, wParam, lParam);
}
// 程序入口点
int WINAPI wWinMain(HINSTANCE hInstance, HINSTANCE hPrevInstance, LPWSTR lpCmdLine, int nCmdShow)
{
g_hinst = hInstance;
// 关闭显示缩放
SetProcessDPIAware();
// 注册窗口类
WNDCLASSEX wcex = {
sizeof wcex, CS_HREDRAW | CS_VREDRAW, WndProc, 0, 0, GetModuleHandle(NULL), LoadIcon(NULL, IDI_APPLICATION),
LoadCursor(NULL, IDC_ARROW), (HBRUSH)(COLOR_WINDOW + 1), NULL, L"MainWndClass", LoadIcon(NULL, IDI_APPLICATION)
};
(zeroexcept)RegisterClassEx(&wcex);
// 计算预期客户区大小对应的窗口大小,并创建和显示窗口
RECT rc = { 0, 0, 640, 480 };
DWORD style = WS_OVERLAPPED | WS_SYSMENU | WS_CAPTION | WS_MINIMIZEBOX;
AdjustWindowRect(&rc, style, FALSE);
g_hwnd = CreateWindow(
L"MainWndClass", L"Main Window", style, CW_USEDEFAULT, CW_USEDEFAULT,
rc.right - rc.left, rc.bottom - rc.top, NULL, NULL, hInstance, NULL);
(zeroexcept)g_hwnd;
ShowWindow(g_hwnd, SW_SHOWDEFAULT);
UpdateWindow(g_hwnd);
// 获取准确的客户区大小
GetClientRect(g_hwnd, &rc);
UINT width = rc.right - rc.left;
UINT height = rc.bottom - rc.top;
// 创建设备和交换链
DXGI_RATIONAL refrate = { 60, 1 };
DXGI_SAMPLE_DESC smpdesc = { 1, 0 };
DXGI_MODE_DESC bufdesc = {
width, height, refrate, DXGI_FORMAT_R8G8B8A8_UNORM, DXGI_MODE_SCANLINE_ORDER_UNSPECIFIED, DXGI_MODE_SCALING_UNSPECIFIED
};
DXGI_SWAP_CHAIN_DESC sd = { bufdesc, smpdesc, DXGI_USAGE_RENDER_TARGET_OUTPUT, 1, g_hwnd, TRUE, DXGI_SWAP_EFFECT_DISCARD, 0 };
(comexcept)D3D10CreateDeviceAndSwapChain(NULL, D3D10_DRIVER_TYPE_WARP, NULL, 0, D3D10_SDK_VERSION, &sd, &g_swpch, &g_dev);
// 获取后台缓冲区,绑定为渲染目标视图
CComPtr<id3d10texture2d> pBackBuffer = NULL;
(comexcept)g_swpch->GetBuffer(0, __uuidof(ID3D10Texture2D), (LPVOID*)&pBackBuffer);
(comexcept)g_dev->CreateRenderTargetView(pBackBuffer, NULL, &g_rtv);
pBackBuffer = NULL; // 手动释放
// 创建深度/模板缓冲区,绑定为深度/模板视图
D3D10_TEXTURE2D_DESC descDepth = {
width, height, 1, 1, DXGI_FORMAT_D32_FLOAT_S8X24_UINT, smpdesc, D3D10_USAGE_DEFAULT, D3D10_BIND_DEPTH_STENCIL, 0, 0
};
D3D10_DEPTH_STENCIL_VIEW_DESC descDSV = { descDepth.Format, D3D10_DSV_DIMENSION_TEXTURE2D };
descDSV.Texture2D.MipSlice = 0;
(comexcept)g_dev->CreateTexture2D(&descDepth, NULL, &g_dsbuf);
(comexcept)g_dev->CreateDepthStencilView(g_dsbuf, &descDSV, &g_dsv);
// 设置设备OM阶段的渲染目标和深度/模板视图
g_dev->OMSetRenderTargets(1, &g_rtv.p, g_dsv); // 注意是&g_rtv.p不是&g_rtv,后者只能用于初始化
// 设置设备RS阶段的视口参数
D3D10_VIEWPORT vp = { 0, 0, width, height, 0.0f, 1.0f, };
g_dev->RSSetViewports(1, &vp);
// TODO: 初始化其它对象 begin
// 编译效果文件,并获取其中的"Render"技术
(comexcept)D3DX10CreateEffectFromFile(L"main.fx", NULL, NULL, "fx_4_0", 0, 0, g_dev, NULL, NULL, &g_effect, NULL, NULL);
g_technique = g_effect->GetTechniqueByName("Render");
// 建立数据输入布局
D3D10_PASS_DESC passdesc;
(comexcept)g_technique->GetPassByIndex(0)->GetDesc(&passdesc);
D3D10_INPUT_ELEMENT_DESC layout[] =
{
{ "POSITION", 0, DXGI_FORMAT_R32G32B32_FLOAT, 0, 0, D3D10_INPUT_PER_VERTEX_DATA, 0 },
{ "COLOR", 0, DXGI_FORMAT_R32G32B32A32_FLOAT, 0, 12, D3D10_INPUT_PER_VERTEX_DATA, 0 },
};
(comexcept)g_dev->CreateInputLayout(layout, ARRAYSIZE(layout), passdesc.pIAInputSignature, passdesc.IAInputSignatureSize, &g_inlayout);
// 建立顶点缓冲区
SimpleVertex vertices[] =
{
D3DXVECTOR3(0.0f, 1.0f, 0.0f), D3DXCOLOR(1.0f, 0.0f, 0.0f, 1.0f),
D3DXVECTOR3(1.0f, -1.0f, 0.0f), D3DXCOLOR(0.0f, 1.0f, 0.0f, 1.0f),
D3DXVECTOR3(-1.0f, -1.0f, 0.0f), D3DXCOLOR(0.0f, 0.0f, 1.0f, 1.0f),
};
D3D10_BUFFER_DESC vertexbufdesc = { sizeof vertices, D3D10_USAGE_DEFAULT, D3D10_BIND_VERTEX_BUFFER, 0, 0 };
D3D10_SUBRESOURCE_DATA vertexinitdata = { vertices, 0, 0 };
(comexcept)g_dev->CreateBuffer(&vertexbufdesc, &vertexinitdata, &g_vertexbuf);
// 设置RS阶段的栅格化参数,不剔除背面
D3D10_RASTERIZER_DESC rastdesc = { D3D10_FILL_SOLID, D3D10_CULL_NONE, 0, 0, 0, 0, 0, 0, 0, 0 };
(comexcept)g_dev->CreateRasterizerState(&rastdesc, &g_raststate);
g_dev->RSSetState(g_raststate);
// TODO: 初始化其它对象 end
// 消息循环
MSG msg = { 0 };
while (msg.message != WM_QUIT)
{
if (PeekMessage(&msg, NULL, 0, 0, PM_REMOVE))
{
TranslateMessage(&msg);
DispatchMessage(&msg);
}
else
{
// 清空渲染目标和深度/模板
float ClearColor[4] = { 0.0f, 0.125f, 0.3f, 1.0f };
g_dev->ClearRenderTargetView(g_rtv, ClearColor);
g_dev->ClearDepthStencilView(g_dsv, D3D10_CLEAR_DEPTH | D3D10_CLEAR_STENCIL, 1.0f, 0);
// TODO: 绘制图元 begin
// 初始化世界矩阵
float angle = GetTickCount() % 1000 / 1000.0f * (float)D3DX_PI * 2.0f;
D3DXMATRIX matWorld;
D3DXMatrixRotationY(&matWorld, angle);
(comexcept)g_effect->GetVariableByName("matWorld")->AsMatrix()->SetMatrix((float*)&matWorld);
// 初始化观察矩阵
D3DXMATRIX matView;
D3DXVECTOR3 eye(0.0f, 1.0f, -2.0f);
D3DXVECTOR3 lookat(0.0f, 0.0f, 0.0f);
D3DXVECTOR3 up(0.0f, 1.0f, 0.0f);
D3DXMatrixLookAtLH(&matView, &eye, &lookat, &up);
(comexcept)g_effect->GetVariableByName("matView")->AsMatrix()->SetMatrix((float*)&matView);
// 初始化投影矩阵
D3DXMATRIX matProj;
D3DXMatrixPerspectiveFovLH(&matProj, (float)D3DX_PI * 0.5f, width / (FLOAT)height, 0.1f, 100.0f);
(comexcept)g_effect->GetVariableByName("matProj")->AsMatrix()->SetMatrix((float*)&matProj);
// 应用"Render"技术并绘制三角形
D3D10_TECHNIQUE_DESC techdesc;
(comexcept)g_technique->GetDesc(&techdesc);
for (UINT i = 0; i < techdesc.Passes; i++)
{
// 应用"Render"技术中的一个通道
(comexcept)g_technique->GetPassByIndex(0)->Apply(0);
// 设置IA阶段输入布局、顶点缓冲区、图元类型,绘制图元
g_dev->IASetInputLayout(g_inlayout);
UINT strides = sizeof(SimpleVertex), offset = 0;
g_dev->IASetVertexBuffers(0, 1, &g_vertexbuf.p, &strides, &offset); // 注意是&g_vertexbuf.p
g_dev->IASetPrimitiveTopology(D3D10_PRIMITIVE_TOPOLOGY_TRIANGLELIST);
g_dev->Draw(3, 0);
}
// TODO: 绘制图元 end
// 提交后台缓冲区以显示
(comexcept)g_swpch->Present(0, 0);
}
}
// 清空状态设置
g_dev->ClearState();
return (int)msg.wParam;
}
</id3d10texture2d></id3d10rasterizerstate></id3d10buffer></id3d10inputlayout></t></id3d10effect></id3d10depthstencilview></id3d10texture2d></id3d10rendertargetview></idxgiswapchain></id3d10device></typename></t></t></t></atlbase.h></d3dx10.h></d3d10.h></windows.h></code>
200字以内,仅用于支线交流,主线讨论请采用回复功能。