在上一节,我们已经研究了关于Direct3D的基本概念,现在让我们开始构建一个简单的Direct3D程序,以便深入探讨实际问题。在这个程序中,我们只是初始化Direct3D,然后将其关闭。虽然内容不多,甚至连一个“hello world”级别都不算,但这是一个好的开始。
COM
是组件对象模型(Component Object Model)
的缩写,它是一种创建非常高级对象的方法,它的行为就像乐高一样,可以将普通的乐高积木粘在一起就可以组装出更高级的形状。积木和积木之间相互独立但彼此兼容,你只需要将它们粘在一起就行。如果需要更换零件,只用拔下一块积木,然后再放置另一块。COM
也是如此,COM
对象实际上就是C++类或类组,你可以从中调用函数并实现某些目标。没有一个类需要另一类来进行操作,同时它们也不需要真正一起去完成工作,但是你可以根据需要将它们插入或拔出,而无需更改程序的其余部分。
例如,你有一个广泛分布的游戏,并且想对其进行升级。你与其跟踪并向购买游戏的每个用户发送新的副本,还不如让他们下载更新的COM
对象,然后将新对象直接插入到程序中,而无任何麻烦。我们不会对COM
进行太详细的介绍,因为对我们来说太过复杂。COM
目的就是消除所有复杂的内容,让你能够轻松进行开发,我们只需要懂得如何使用就行,不必去深究COM
的原理了。
那为什么要学习COM
?实际上,DirectX就是一系列的COM
对象,其中之一就是Direct3D
。Direct3D
其实就是一个COM对象,其中包含了其他的COM对象,最终,它包含了运行2D和3D图形所需的一切软件和硬件。由于Direct3D
已经存储在类中,因此我们可以通过箭头访问运算符访问Direct3D
类中的CreateRenderTargetView()
和Release()
函数。后面在实践中,我们会对此进行详细的介绍。
device->CreateRenderTargetView()
device->Release()
即使COM隐藏了很多细节,但是你仍然需要了解以下四件事:1、COM对象就是一个类或一组类,这些类通过接口控制。一个接口就是一组函数,这些函数可以很好的控制COM对象。例如上面的示例中,“device”就是一个COM对象,并由函数进行控制;2、每种类型的COM对象都有唯一的ID。例如,Direct3D对象有自己的ID,DirectSound对象也有自己的ID,有时你需要在代码中使用此ID;3、使用COM对象完成操作之后,必须调用Release()函数,这么做的目的是为了告诉对象释放其内存并关闭其线程;4、COM对象很容易识别,因为它们通过以“I”(大写的i)开头,例如"ID3D10Device"。下面,让我们继续学习实际的代码。
在开始实际的Direct3D代码之前,让我们先谈谈头文件和库文件。在我们的演示程序中,我们将这些内容放在顶部,从而使我们能够在全局访问DIrect3D。下面,让我们看看代码:
// 包括基本的Windows头文件和Direct3D头文件
#include
#include
#include
#include
#include
// 包括Direct3D库文件
#pragma comment (lib, "d3d11.lib")
#pragma comment (lib, "d3dx11.lib")
#pragma comment (lib, "d3dx10.lib")
// 全局声明
IDXGISwapChain *swapchain; // 指向交换链接口的指针
ID3D11Device *dev; // 指向Direct3D设备接口的指针
ID3D11DeviceContext *devcon; // 指向Direct3D设备上下文的指针
// 函数原型
void InitD3D(HWND hWnd); // 设置并初始化Direct3D
void CleanD3D(void); // 关闭Direct3D并释放内存
// WindowProc函数原型
LRESULT CALLBACK WindowProc(HWND hWnd, UINT message, WPARAM wParam, LPARAM lParam);
#include
#include
这两行代码包含了Direct3D 11头文件,这两个头文件由Direct3D 11库中包含的实际方法的各种声明组成。这两个头文件包含不同的内容,d3d11.h
头文件包含Direct3D的核心部分,d3dx11.h
头文件包含Direct3D的扩展部分,这些扩展对于图形库不是必需的,但是在编写游戏或其他图形程序时可以非常方便。
注意:并非所有编译器都能自动找到这些文件。在某些情况下,你需要设置你的项目,以便可以查看DirectX SDK的文件夹。如果你使用的是Visual Studio,我在此处编写了一个简短的mini课程。
#include
Direct3D 11
实际上是Direct3D 10
的扩展。因此,它从Direct3D 10
借用了许多宏、函数和类。通过包含d3d10.h
头文件,我们就可以在程序中使用它们。
#pragma comment (lib, "d3d11.lib")
#pragma comment (lib, "d3dx11.lib")
#pragma comment (lib, "d3dx10.lib")
这些代码包含了Direct3D 11
的库文件。“#pragma comment”
指令将某些信息放入项目的目标文件中。使用第一个参数lib
,表示我们要向项目中添加库文件,然后我们指定需要添加哪些文件:“d3d11.lib”
、"d3dx11.lib"
、"d3dx10.lib"
。
ID3D11Device *dev;
这个变量是指向设备的指针。在Direct3D中,设备是一个对象,作为视频适配器的虚拟表示。这行代码的意思是,我们将创建一个名为ID3D11Device的COM
对象。当COM创建对象时,我们将忽略这个对象,并且仅适用此指针间接访问它。后面我们会详细介绍这种情况。
ID3D11DeviceContext *devcon;
设备上下文类似于设备,但是它负责管理GPU和渲染管道(而设备主要主要负责处理显存)。该对象主要用于渲染图形并确定如何渲染图形。
IDXGISwapChain *swapchain;
正如我们上一课所讲,交换链是一系列缓冲区,这些缓冲区轮流呈现。此变量是指向此类链的指针。当请注意,该对象不属于Direct3D,但实际上是DXGI的一部分。
实际编码Direct 3D的第一步是创建上述三个COM对象并对其进行初始化,这是通过一个函数以及一个包含图形设备信息的结构体来完成的。让我们先看一下这个功能,然后再看一下它的各个部分。
// 这个函数初始化并准备Direct3D以供使用
void InitD3D(HWND hWnd)
{
// 创建一个结构体来保存有关交换链的信息
DXGI_SWAP_CHAIN_DESC scd;
// 清空这个结构体以供使用
ZeroMemory(&scd, sizeof(DXGI_SWAP_CHAIN_DESC));
// 填写交换链描述结构
scd.BufferCount = 1; // 一个后缓冲区
scd.BufferDesc.Format = DXGI_FORMAT_R8G8B8A8_UNORM; // 使用32位色
scd.BufferUsage = DXGI_USAGE_RENDER_TARGET_OUTPUT; // 交换链如何使用
scd.OutputWindow = hWnd; // 要使用的窗口
scd.SampleDesc.Count = 4; // 多重采样的数量
scd.Windowed = TRUE; // 窗口全屏模式
// 使用scd结构中的信息来创建设备、设备上下文和交换链
D3D11CreateDeviceAndSwapChain(NULL,
D3D_DRIVER_TYPE_HARDWARE,
NULL,
NULL,
NULL,
NULL,
D3D11_SDK_VERSION,
&scd,
&swapchain,
&dev,
NULL,
&devcon);
}
DXGI_SWAP_CHAIN_DESC scd;
在高级游戏编程的一开始,就需要将某些信息输入Direct3D中。这些信息有很多,但我们这里只介绍其中的几个。目前,DXGI_SWAP_CHAIN_DESC
是一个结构体,它的成员将包含我们交换链的描述。我们将介绍我们所需的成员,并在整个教程中陆续介绍新的成员。
ZeroMemory(&scd, sizeof(DXGI_SWAP_CHAIN_DESC));
我们使用ZermMemory()
函数将整个scd结构体快速初始化为NULL。这样我们就不必遍历结构体的每个成员并分别将它们初始化为NULL。
scd.BufferCount = 1;
该成员包含要在我们的交换链上使用的后台缓冲区的数量。我们将只使用一个后缓冲区和一个前缓冲区,因此我们将此值设置为1。我们可以使用更多的缓冲区,但是目前1就可以满足我们所有的需求。
scd.BufferDesc.Format = DXGI_FORMAT_R8G8B8A8_UNORM;
这个成员用来设置颜色的格式,在前后缓冲区中,每个像素均按颜色存储。这个值确定数据以什么格式存储,在这里我们将格式设置为 DXGI_FORMAT_R8G8B8A8_UNORM
,这是一个用于指示格式的编码标志。
scd.BufferUsage = DXGI_USAGE_RENDER_TARGET_OUTPUT;
这个成员用于设置我们将如何使用交换链,它有两个常用的值,这两个值也可以进行“或”运算。
值 | 描述 |
---|---|
DXGI_USAGE_RENDER_TARGET_OUTPUT |
当你希望将图形绘制到后台缓冲区中时使用此值 |
DXGI_USAGE_SHARED |
通常,但设备创建缓冲区时,只有该设备可以使用它。此值允许缓冲区在多个设备对象之间共享 |
scd.OutputWindow = hWnd;
这个成员用于设置Direct3D应该绘制的窗口的句柄,我们将一直使用相同的hWnd
。
scd.SampleDesc.Count = 1;
这个成员告诉Direct3D如何执行多重采样抗锯齿(MSAA
)渲染。基本上,抗锯齿通过将每个像素与周围的像素稍微融合,以使形状的边缘平滑。
如上图所示,左侧的线条有类似楼梯的锯齿效果,而右侧由于Direct3D融合了像素,因此显得平滑。这个值用于高速Direct3D在抗锯齿中应该加入多少细节,该值越高越好。Direct3D 11显卡在此处最多支持4个,但最小必须为1个。
scd.Windowed = TRUE;
当我们在像现在这样的窗口中运行Direct3D时,此值设置为TRUE。否则,对于全屏模式,它将设置为FALSE。注意:进去全屏模式之前,您好需要进行其他更改,单独更改此值并不会使你的程序正确地全屏显示。
D3D11CreateDeviceAndSwapChain()
这是一个很大的函数,但实际上它使用非常简单,在你编写的每个游戏中,大多数参数都可能保持不变。该函数的作用是创建设备、设备上下文和交换链等COM对象。创建完之后,我们就可以使用它们去执行实际的渲染。下面是函数原型:
HRESULT D3D11CreateDeviceAndSwapChain(
IDXGIAdapter *pAdapter,
D3D_DRIVER_TYPE DriverType,
HMODULE Software,
UINT Flags,
D3D_FEATURE_LEVEL *pFeatureLevels,
UINT FeatureLevels,
UINT SDKVersion,
DXGI_SWAP_CHAIN_DESC *pSwapChainDesc,
IDXGISwapChain **ppSwapChain,
ID3D11Device **ppDevice,
D3D_FEATURE_LEVEL *pFeatureLevel,
ID3D11DeviceContext **ppDeviceContext);
参数 | 描述 |
---|---|
IDXGIAdapter *pAdapter |
此参数用于指示Direct3D应该使用什么图形适配器。图形适配器通常是值GPU以及显存和数模转换器等。我们通常让DXGI帮我们解决这个问题,为了告诉DXGI需要让它决定,我们在此使用NULL,以指示默认适配器 |
D3D_DRIVER_TYPE DriverType |
此参数用于确定DIrect3D应该使用硬件还是软件进行渲染。你可以使用许多标志来确定这一点。在这里我们使用“D3D_DRIVER_TYPE_HARDWARE ”,这显然是最佳选择,使用高级GPU硬件进行渲染。 |
HMODULE Software |
我们不会涉及这个参数,设置为NULL。它与上一个参数的D3D_DRIVER_TYPE_SOFTWARE 标志一起使用,以设置软件代码。它非常慢,因此我们不会使用 |
UINT Flags | 此参数拥有一些UINT Flag可以改变Direct3D运行方式的标志,这些标志可以在一起进行“或”运算,幸运的是,即使我们不设置任何标志,设为NULL,也依然可以顺序进行 |
D3D_FEATURE_LEVEL *pFeatureLevels |
每个Direct3D的主要版本都具有一系列所需的显卡功能,如果知道你的硬件满足哪个版本的要求,则可以更加轻松地了解硬件的功能(假设用户将使用不同的显卡)。 此参数用于创建功能列表,此列表告诉Direct3D,你期望程序使用哪些功能。 对于本程序,你将需要使用Direct 3D 11的显卡,因此不需要使用此参数,直接设置为NULL即可 |
UINT FeatureLevels |
此参数指示列表中有多少个功能级别,我们直接设置为NULL即可。 |
UINT SDKVersion |
此参数始终相同:D3D11_SDK_VERSION 。为什么是这样呢?实际上仅与其他计算机的兼容性有关。每台机器通常具有不同次要版本的DirectX。这个参数告诉了你开发的游戏是为了哪个版本的用户DirectX。在不同版本的SDK中,此值返回不同的数字,你不需要更改,只需要使用D3D11_SDK_VERSION 就能正常工作。 |
DXGI_SWAP_CHAIN_DESC *pSwapChainDesc |
这是指向交换链描述结构的指针,我们在这里设置成“&scd” |
IDXGISwapChain **ppSwapChain |
这是指向交换链对象的指针,该函数为我们创建交换链对象,并将对象的地址存储在此指针中 |
ID3D11Device **ppDevice |
这是指向设备对象的指针 |
D3D_FEATURE_LEVEL *FeatureLevel |
有关功能级别的更多信息。这是指向功能级别变量的指针,当函数完成之后,这个变量将被找到的最高功能级别的标志填充,这使得程序员知道可以使用哪些硬件。我们通常将其设为NULL。 |
ID3D11DeviceContext **ppImmediateContext |
这是指向设备上下文对象的指针 |
现在我们已经初始化了Direct3D,让我们继续关闭它。
无论何时创建Direct3D,都必须将其关闭,这很简单,只需要三个命令。
// 这个函数是为了清理Direct3D和COM
void CleanD3D()
{
// 关闭并释放所有的COM对象
swapchain->Release();
dev->Release();
devcon->Release();
}
在函数中,我们分别对三个COM对象调用Release()
函数,从而释放一切。如果不这样做,将会造成糟糕的结果。如果你创建一个COM对象,但是不关闭它,那么即使在程序结束之后,它也将一直在计算机的后台运行,直到你关机重启。期间它将占用大量的资源,释放COM对象可以让一切资源都被释放,并允许Windows
收回其内存。
// include the basic windows header files and the Direct3D header files
#include
#include
#include
#include
#include
// include the Direct3D Library file
#pragma comment (lib, "d3d11.lib")
#pragma comment (lib, "d3dx11.lib")
#pragma comment (lib, "d3dx10.lib")
// global declarations
IDXGISwapChain *swapchain; // the pointer to the swap chain interface
ID3D11Device *dev; // the pointer to our Direct3D device interface
ID3D11DeviceContext *devcon; // the pointer to our Direct3D device context
// function prototypes
void InitD3D(HWND hWnd); // sets up and initializes Direct3D
void CleanD3D(void); // closes Direct3D and releases memory
// the WindowProc function prototype
LRESULT CALLBACK WindowProc(HWND hWnd, UINT message, WPARAM wParam, LPARAM lParam);
// the entry point for any Windows program
int WINAPI WinMain(HINSTANCE hInstance,
HINSTANCE hPrevInstance,
LPSTR lpCmdLine,
int nCmdShow)
{
HWND hWnd;
WNDCLASSEX wc;
ZeroMemory(&wc, sizeof(WNDCLASSEX));
wc.cbSize = sizeof(WNDCLASSEX);
wc.style = CS_HREDRAW | CS_VREDRAW;
wc.lpfnWndProc = WindowProc;
wc.hInstance = hInstance;
wc.hCursor = LoadCursor(NULL, IDC_ARROW);
wc.hbrBackground = (HBRUSH)COLOR_WINDOW;
wc.lpszClassName = L"WindowClass";
RegisterClassEx(&wc);
RECT wr = {
0, 0, 800, 600};
AdjustWindowRect(&wr, WS_OVERLAPPEDWINDOW, FALSE);
hWnd = CreateWindowEx(NULL,
L"WindowClass",
L"Our First Direct3D Program",
WS_OVERLAPPEDWINDOW,
300,
300,
wr.right - wr.left,
wr.bottom - wr.top,
NULL,
NULL,
hInstance,
NULL);
ShowWindow(hWnd, nCmdShow);
// set up and initialize Direct3D
InitD3D(hWnd);
// enter the main loop:
MSG msg;
while(TRUE)
{
if(PeekMessage(&msg, NULL, 0, 0, PM_REMOVE))
{
TranslateMessage(&msg);
DispatchMessage(&msg);
if(msg.message == WM_QUIT)
break;
}
else
{
// Run game code here
// ...
// ...
}
}
// clean up DirectX and COM
CleanD3D();
return msg.wParam;
}
// this is the main message handler for the program
LRESULT CALLBACK WindowProc(HWND hWnd, UINT message, WPARAM wParam, LPARAM lParam)
{
switch(message)
{
case WM_DESTROY:
{
PostQuitMessage(0);
return 0;
} break;
}
return DefWindowProc (hWnd, message, wParam, lParam);
}
// this function initializes and prepares Direct3D for use
void InitD3D(HWND hWnd)
{
// create a struct to hold information about the swap chain
DXGI_SWAP_CHAIN_DESC scd;
// clear out the struct for use
ZeroMemory(&scd, sizeof(DXGI_SWAP_CHAIN_DESC));
// fill the swap chain description struct
scd.BufferCount = 1; // one back buffer
scd.BufferDesc.Format = DXGI_FORMAT_R8G8B8A8_UNORM; // use 32-bit color
scd.BufferUsage = DXGI_USAGE_RENDER_TARGET_OUTPUT; // how swap chain is to be used
scd.OutputWindow = hWnd; // the window to be used
scd.SampleDesc.Count = 4; // how many multisamples
scd.Windowed = TRUE; // windowed/full-screen mode
// create a device, device context and swap chain using the information in the scd struct
D3D11CreateDeviceAndSwapChain(NULL,
D3D_DRIVER_TYPE_HARDWARE,
NULL,
NULL,
NULL,
NULL,
D3D11_SDK_VERSION,
&scd,
&swapchain,
&dev,
NULL,
&devcon);
}
// this is the function that cleans up Direct3D and COM
void CleanD3D(void)
{
// close and release all existing COM objects
swapchain->Release();
dev->Release();
devcon->Release();
}