现在开始写一些实际的代码。我会手把手教你创建你的第一个DirectX程序。大部分来自DX SDK的例子依赖于一个示例框架,这个框架包含了一堆无聊的代码。在我给的注释与示例里,我不会使用这个框架,所以你可以学到一个真正的游戏所需要的东西。
下面是本章内容:
- 如何创建一个工程
- 如何建立一个Windows应用程序
- 如何初始化DirectX
- 如何清空屏幕
- 如何显示场景
创建工程
任何应用程序的第一步都是创建VS(Visual Studio)工程。运行空的VS。
- 选择 新建—>项目。(位于主菜单 文件 选项下)
- 选择Win32项目(注意不是Win32 控制台应用程序),输入名字,点击“确定”,进入对话框。
- 在对话框里点击“下一步”,然后选择“空项目”。注意一定得是空项目。
- 最后点击“完成”。
添加Windows代码
上面完成后,VS会创建一个空项目。下一步就是创建源代码来初始化程序主窗口。当然,你得先添加一个空的源文件。
- 右键项目菜单上选择 添加—>新建项。
- 在弹出的对话框里选择C++文件(.cpp)。
- 写上名字。
- 点击“打开”按钮。
WinMain
任何Windows应用程序的第一部分都是入口指针。在控制台程序里面,入口指针函数叫
main,Windows程序的入口函数是
WinMain。这个函数用来初始化程序,创建程序窗口,然后开始消息循环。接下来,你可以跟着把代码打一遍,也可以从最后下载winmain.cpp文件。不过建议新手自己动手打一遍。我一直相信 看懂了是骗人的。
/****************************************************************
* example1
* this application shows how to setup a standard windows
* application
* No DirectX libraries are needed to compile this.
****************************************************************/
#include <windows.h>
#include <tchar.h>
// Globals //
HINSTANCE hInst;
HWND mainhWnd;
int width = 640;
int height = 480;
// Forward declarations //
bool InitWindow(HINSTANCE hInstance, int width, int height);
LRESULT CALLBACK WndProc(HWND, UINT, WPARAM, LPARAM);
/*******************************************************************
* WinMain
*******************************************************************/
int APIENTRY _tWinMain(HINSTANCE hInstance, HINSTANCE hPrevInstance, LPTSTR lpCmdLine, int nCmdShow)
{
// Set up the application window
if (!InitWindow(hInstance, width, height))
{
return false;
}
// Main message loop
MSG msg = {0};
while (WM_QUIT != msg.message)
{
while (PeekMessage(&msg, NULL, 0, 0, PM_REMOVE) == TRUE)
{
TranslateMessage(&msg);
DispatchMessage(&msg);
}
}
return (int) msg.wParam;
}
这个函数里最重要的是主消息循环。用于程序从系统接收消息,允许程序运行在Windows环境中。假如你的程序除了简单的接收消息还有额外的处理,则最好使用
PeekMessage函数(游戏开发中一直使用这个)。
大多数Windows程序在消息循环时使用
GetMessage函数。两个函数的区别在于,
GetMessage函数只在消息可用时才传给调用者,而
PeekMessage 函数会立即传递。
在写游戏时,
PeekMessage函数非常重要,因为它可以让你能在每一个循环中处理额外的逻辑。没有这个能力的话,大量来自程序的消息会影响程序的运行。
不管使用哪个函数,如果消息可用,那
TranlateMessage函数和
DispachMessage函数将被调用。
WinMain函数完成后,就可以创建程序窗口了。
InitWindow
Windows允许程序在桌面创建一个窗口前,程序必须先注册一个窗口类。然后,程序就可以创建一个需要的窗口了。下面的代码就是注册一个普通的窗口类,然后使用这个注册类创建一个默认窗口。
/*******************************************************************
* InitWindow
* Inits and creates and main app window
* Inputs - application instance - HINSTANCE
Window width - int
Window height - int
* Outputs - true if successful, false if failed - bool
*******************************************************************/
bool InitWindow(HINSTANCE hInstance, int width, int height)
{
WNDCLASSEX wcex;
wcex.cbSize = sizeof(WNDCLASSEX);
wcex.style = CS_HREDRAW | CS_VREDRAW;
wcex.lpfnWndProc = (WNDPROC)WndProc;
wcex.cbClsExtra = 0;
wcex.cbWndExtra = 0;
wcex.hInstance = hInstance;
wcex.hIcon = 0;
wcex.hCursor = LoadCursor(NULL, IDC_ARROW);
wcex.hbrBackground = (HBRUSH)(COLOR_WINDOW+1);
wcex.lpszMenuName = NULL;
wcex.lpszClassName = TEXT("DirectXExample");
wcex.hIconSm = 0;
RegisterClassEx(&wcex);
// Resize the window
RECT rect = { 0, 0, width, height };
AdjustWindowRect(&rect, WS_OVERLAPPEDWINDOW, FALSE);
// create the window from the class above
mainhWnd = CreateWindow(TEXT("DirectXExample"),
TEXT("DirectXExample"),
WS_OVERLAPPEDWINDOW,
CW_USEDEFAULT,
CW_USEDEFAULT,
rect.right - rect.left,
rect.bottom - rect.top,
NULL,
NULL,
hInstance,
NULL);
if (!mainhWnd)
{
return false;
}
ShowWindow(mainhWnd, SW_SHOW);
UpdateWindow(mainhWnd);
return true;
}
上面这个函数在每个Windows编程书中都有介绍。我只是简要概括下这段代码是干什么的。
每一个想要显示窗口的程序都要先向系统注册一个窗口类。这个窗口类描述了窗口的某些特性,比如背景色,鼠标指针样式和程序关联图标。窗口类使用WNDCLASSEX结构来表示。这个结构被正确填写后就作为参数传递给
RegisterClassEX函数。
ReigisterClassEX函数使用WNDCLASSEX提供的信息向系统注册一个窗口类。现在你就有了一个合法的注册窗口类,接着就可以创建一个程序可用窗口。
然后,通过调用CreateWindow函数来创建需要的窗口。
CreateWindow函数需要多个参数,每一个都向系统描述创建的窗口长什么样。具体参数可以参考前面的示例代码。
注:自从VS2005之后,创建Win32项目时会默认支持Unicode。因此,_tWinMain函数成为了程序的入口指针。而且,所有的字符串也必须是LPCWSTR类型。TEXT宏命令就是用来将字符串转化为Unicode编码的。
WndProc
窗口处理是Windows程序的最后一部分。下面的WndProc示例代码用于处理来自系统并于你的程序有关的事件。例如,鼠标在你的程序窗口内部点击,系统就会发送一个鼠标点击事件到你的窗口处理函数中。你的窗口处理函数会决定是处理消息还是直接忽略。
下面的窗口函数示例仅仅包含了最少的代码用来接收点击关闭按钮或者按下ESC键来关闭应用程序。
/*******************************************************************
* WndProc
* The main window procedure for the application
* Inputs - application window handle - HWND
message sent to the window - UINT
wParam of the message being sent - WPARAM
lParam of the message being sent - LPARAM
* Outputs - LRESULT
*******************************************************************/
LRESULT CALLBACK WndProc(HWND hWnd, UINT message, WPARAM wParam, LPARAM lParam)
{
switch (message)
{
// Allow the user to press the escape key to end the application
case WM_KEYDOWN:
switch(wParam)
{
// Check if the user hit the escape key
case VK_ESCAPE:
PostQuitMessage(0);
break;
}
break;
// The user hit the close button, close the application
case WM_DESTROY:
PostQuitMessage(0);
break;
}
return DefWindowProc(hWnd, message, wParam, lParam);
}
编译运行成功后会出现一个白色背景的空白窗口。
Direct3D登场
现在你已经知道如何获取并运行一个窗口,接下来就该向里面添加一些东西了。不过首先你得知道怎么创建Direct3D。DirectX通过Direct3D组件处理所有的绘制操作。Direct3D提供一个标准接口来访问图形硬件并渲染游戏场景。
使用Direct3D的一般步骤:
- 定义交换链
- 创建D3D设备
- 建立渲染目标
- 准备视口
- 屏幕绘制
初始化Direct3D
Direct3D设备是一个用来访问图形硬件的简单接口。把Direct3D想象成一个中间人:你告诉Direct3D你要做什么,然后Direct3D直接和硬件交互。例如,你想在屏幕上显示一个立方体,那么首先,你告诉D3D有关这个立方体的具体参数,比如绘制位置,长什么样;然后D3D就按照这些要求直接控制硬件绘制立方体。
双缓冲
当你使用D3D进行绘制时,实际上你没有直接绘制到屏幕上,而是绘制到了显存里,再从显存复制到屏幕上。显存中存满了屏幕上所有对象的所有数据,而且数据更新又非常快,更新一个对象不过瞬间的事。所以看起来就像你刚一绘制,它就显示在屏幕上了。这样的话,你可能会看见游戏对象在屏幕上一闪而过或是屏幕狂抖。要解决这个问题,就得使用双缓冲技术。
双缓冲技术就是使用两个存储区,一个用来绘制,绘制完了就复制到屏幕上显示,显示的过程中继续在另一个区域绘制下一帧,然后绘制完了又交换。(有些朋友会觉得游戏画面一帧的显示那么快,后面的缓存在绘制下一帧忙得过来吗?其实一般情况下,不是大型3D游戏的话,绘制比显示还要快,就是说后面的已经把下一帧绘制好了,就等前面一帧显示完。有兴趣的朋友可以去关注下现代GPU。)
双缓冲的作业图是这样的:
交换链
一系列可绘制图形在显示到屏幕之前都是存储在交换链缓存中。在一个交换链中可以有一个用来显示的前缓存和多个用于绘制的后台缓存。推荐使用双缓存(一前一后),当然现今显存容量大,多缓存也不是不可以,不过超过三个缓存后显示效果的提升不明显,游戏性能还可能下降,这就得不偿失了。
在DX中创建交换链,你的先填充DXGI_SWAP_CHAIN_DESC结构体。这个结构体中包含了一些有用信息,比如交换链中要几个缓冲区,缓冲区的细节。这里给一个交换链结构的示例:
DXGI_SWAP_CHAIN_DESC swapChainDesc;
//设置交换链中缓冲区的宽、高
swapChainDesc.BufferDesc.Width = 640;
swapChainDesc.BufferDesc.Height = 480;
//设置刷新率。就是缓冲区的交换频率
swapChainDesc.BufferDesc.RefreshRate.Numerator = 60;
swapChainDesc.BufferDesc.RefreshRate.Denominator = 1;
//设置缓冲区的表面格式
swapChainDesc.BufferDesc.Format = DXGI_FORMAT_R8G8B8A8_UNORM;
swapChainDesc.BufferDesc.ScanlineOrdering.DXGI_MODE_SCANLINE_ORDER_UNSPECIFIED;
swapChainDesc.BufferDesc.Scaling.DXGI_MODE_SCALING_UNSPECIFIED;
swapChainDesc.SampleDesc.Count = 1;
swapChainDesc.SampleDesc.Quality = 0;
//设置缓冲区的使用方式。这里是一绘制到缓冲区就转移到渲染目标上
swapChainDesc.BufferUsage = DXGI_USAGE_RENDER_TARGET_OUTPUT;
//设置后台缓冲区的数量(是后台,不是全部),一般(最小)设成1
swapChainDesc.BufferCount = 1;
//主程序(游戏)窗口句柄
swapChainDesc.OutputWindow = hWnd;
//设置是窗口还是全屏
swapChainDesc.Windowed = TRUE;
// 设置缓冲区如何交换。DISCARD是指交换时允许缓冲区完全被覆盖
swapChainDesc.SwapEffect.DXGI_SWAP_EFFECT_DISCARD;
swapChainDesc.Flags = DXGI_SWAP_CHAIN_FLAG_NONPREROTATED;
该结构还有很多其他选项,都可以设置成0或者直接忽视。这个示例是包含了最少的默认选项。在交换链中还有几个额外的结构也需填充。不过都很容易可以在交换链结构中初始化。
大部分情况下,使用默认值是完全可行的,一般也就改改和你窗口相关联的缓冲区的宽高。下面这个表是关于DXGI_SWAP_CHAIN_DESC结构中的变量的简要描述:
创建D3D设备
准备好了交换链,既可以创建D3D设备了。D3D设备是 ID3D10Device的对象,也是访问视频硬件的主要点。所有的D3D绘图和访问显存的函数都是通过 ID3D10Deice对象来处理的。
使用 D3DCreateDeviceAndSwapChain函数来创建D3D设备。下面是一个创建示例:
D3D10CreateDeviceAndSwapChain(NULL,
D3D10_DRIVER_TYPE_REFERENCE,
NULL,
0,
D3D10_SDK_VERSION,
&swapChianDesc,
&pSwapChain,
&pD3DDevice);
现今大部分机器上都有一块或两块显卡,只有一块显卡时,将会传递一个NULL值给函数的第一个参数。这个参数就是告诉D3D,ID3DDevice会为哪一个显卡创建设备。
第二个参数会传递给D3D10_DRIVER_TYPE_REFERENCE。表示D3D设备使用的驱动类型。如果你的显卡支持D3D10,你就可以用 D3D10_DRIVER_TYPE_HARDWARE,这可以让你享受下硬件加速功能。如果你的显卡仅仅支持DirectX9甚至更低版本,你就要使用参考驱动。
先看看下面来自DirectX SDK中的代码。如果你不知道游戏运行时会使用哪种驱动,下面的代码会先尝试创建硬件设备,如果不行就会自动创建参考设备。
D3D10_DRIVER_TYPE driverType = D3D10_DRIVER_TYPE_NULL;
//定义两种驱动设备类型
D3D10_DRIVER_TYPE driverTypes[] = {
D3D10_DRIVER_TYPE_HARDWARE,
D3D10_DRIVER_TYPE_REFERENCE,
}
UINT numDriverType = sizeof(driverType) / sizeof(driverTypes[0]);
//循环可用类型并尝试首先创建硬件设备
for(UINT driverTypeIndex = 0; driverTypeIndex < numDriverTypes; driverTypeIndex++){
driverType = driverTypes[driverTypesIndex];
hr = D3D10CreateDeviceAndSwapChain(NULL,
driverType,
NULL,
0,
D3D10_SDK_VERSION,
&swapChainDesc,
&pSwapChain,
&pD3DDevice);
if(SUCCEEDED(hr))
break;
}
注:如果你的程序尝试创建一个D3D硬件设备,但没有相应的硬件支持。那么D3D10CreateDeviceAndSwapChain函数会调用失败。如果你的程序没有很好的处理这个问题,用户就很迷惑为什么你的程序不能运行。
这本书上的所有示例都假定支持DirectX10的硬件还没普及。(译注:现在支持DX10的显卡应该普及的差不多了,支持DX11的显卡没普及才是真的)
D3D10CreateDeviceAndSwapChain函数在你先前使用DXGI_SWAP_CHAIN_DESC结构的时候就创建过一次。D3D创建设备时使用的就是你在那个结构中提供的选项。
D3D10CreateDeviceAndSwapChain函数会同时创建两个东西:一个交换链对象和一个D3D设备。(看函数名也知道了)
该函数会用一个合法的IDXGISwapChain对象填充 pSwapChain变量,用一个合法的 ID3D10Device对象填充 pD3DDevice变量。
小贴士:经常通过检查D3D函数的返回值来确认对象是否创建正确是一个良好的习惯。大部分D3D函数会返回一个HRESULT类型的值,D3D_OK表示创建成功。
添加渲染目标
D3D的所有绘制都在
渲染目标上。一个渲染目标就是一种资源类型,被当作一个输出源。交换链初始化时创建的后台缓冲区就在D3D管线的最终阶段被当作渲染目标。
在管线可以使用后台缓冲区之前,它必须被转化为一个渲染目标视口。这时候的后台缓冲就被当作一个2D纹理资源(ID3D10Texture2D)。
一个视口允许资源在管线各阶段被不同方法解释。为资源创建不同视口,则一个资源被装进内存后可以和多个视口进行关联。可以使用CreateRenderTargetView函数来创建管线需要的渲染目标视口。
管线中的整合输出阶段就是根据前面各阶段产生的数据来生成最终图像。管线需要一个地方来记下信息而且这个地方得是渲染目标视口从后台缓冲区创建的。函数OMSetRenderTargets就是用来将渲染目标和管线绑定。接着所有的绘制都会发生在渲染目标视口上了。
//从交换链中获取后台缓冲区
ID3D10Texture2D * pBackBuffer;
HRESULT hr = pSwapChain -> GetBuffer(0, _uuidof(ID3D10Texture2D),(LPVOID*)&pBackBuffer);
if(hr != S_OK)
return false;
//创建渲染目标视口
hr = pD3DDevice -> CreateRenderTargetView(pBackBuffer, NULL, &pRenderTargetView);
//释放后台缓冲
pBackBuffer ->Release();
//确认渲染目标视口是否创建成功
if(hr != S_OK)
return false;
//设置渲染目标
pD3DDevice ->OMSetRenderTargets(1,&pRenderTargetView,NULL);
这段代码片面的展示了渲染目标是如何被管线限制的。注意到,后台缓冲的临时指针在设置渲染目标前就被释放了。
视口
视口用来定义屏幕可见区域的属性。即使你创建的场景是3D空间,但你看见的设备(比如显示器)依然是纯粹的2D环境。视口在管线光栅化阶段使用,继承D3D10_VIEWPORT结构中的定义来裁减场景。任何没在视口中定义的区域将从场景中裁减掉。
视口结构定义之后,可以使用RSSetViewPorts函数来进行设置参数。
//创建并设置视口
D3D10_VIEWPORT viewPort;
viewPort.Width = width;
viewPort.Height = height;
viewPort.MinDepth = 0.0f;
viewPort.MaxDepth = 1.0f;
viewPort.TopLeftX = 0;
viewPort.TopLeftY = 0;
//设置视口
pD3DDevice -> RSSetViewports(1,&viewPort);
清屏
现在D3D设备已经创建好了,你可以开始渲染屏幕了,不管是用图片还是一堆多边形。不过首先你得在每一次游戏主循环中清屏。清屏可以在每一帧给你一块干净的画布来渲染。(译注:计算机绘图就是步骤就是 清屏-绘制-清屏-绘制-清屏……)
清屏一般用ClearRenderTargetView函数。
//清空目标缓存
pD3DDevice ->ClearRenderTargetView(pRenderTargetView, D3DXCOLOR(0.0f,0.0f,0.0f,0.0f));
屏幕清空后,你就可以使用任意D3D可用颜色。D3DXCOLOR函数可以创建32位RGBA模式的颜色,每一个分量值在0.0f—1.0f之间。大多数情况下,黑色用来做清屏色,当然其他色也可以(我一般喜欢用白色)。
显示场景
现在屏幕已被清空,是时候在屏幕上显示了。D3D使用的是Present函数。其实所有的绘制都已经在后台缓冲区画好了,Present函数用来在交换链中执行翻转缓冲区。
翻转指的是在交换链中交换缓冲区。例如,后台缓冲区在显示内容之前就需要翻转来先进行绘制。
小贴士:在渲染任何东西之前最好先确认D3D对象合法。如果D3D对象是NULL,那就跳过所有的渲染操作。
//翻转并显示交换链中的下一帧缓存
pSwapChain ->Present(0,0);
Present函数的第一个参数让你可以同时控制如何绘制。默认值0表示将交换链中的下一缓存立即呈现到屏幕上。
第二个参数让你可以在交换链呈现到屏幕前进行测试。传递一个DXGI_PRESENT_TEST值来执行测试而且HRESULT用来检查任何错误。默认值0表示直接执行交换。
清除
任何D3D程序的最后一件事都是清除并释放你所创建的所有对象。比如,程序开始时,你创建了一个Direct3D对象,一个交换链和一个渲染目标视口。那么当程序结束时,你需要释放这些对象,让系统回收并重用资源。
COM对象保持着一个引用计数,用来在内存中移除对象时告诉系统。使用一次Rlease函数,引用计数就减小一。当引用计数变成0时,系统才会真正释放这些资源。
// 释放渲染目标
if (pRenderTargetView)
pRenderTargetView->Release();
// 释放交换链
if (pSwapChain)
pSwapChain->Release();
// 释放Direct3D设备
if (pD3DDevice)
pD3DDevice->Release();
if语句先检查对象是否存在,存在就执行释放函数。注意,释放对象的顺序应与创建对象的顺序相反,即最先创建的最后释放。
在调用Rlease函数前检查确认DirectX对象不为NULL是很好的习惯。企图释放一个不合法的指针会让你的游戏崩溃。
格式
有时候你会需要指定一个DXGI格式。
格式是用来形容一张图片的布局,比如每种颜色占的位数,顶点缓存中各顶点的安排。大部分情况下,DXGI格式都是用来描述交换链中的缓存布局。DXGI格式不是什么特殊的数据类型,仅仅是其来源的规格。
DXGI格式的一个例子,DXGI_FORMAT_R8G8B8A8_UNORM,意思是RGBA各分量使用8位。定义顶点的时候,DXGI_FORMAT_R32G32B32_FLOAT的意思是R,G,B每个分量使用32位可用空间。即使指定一个RGB格式,它也只是描述数据怎么安排,而不会指明数据的用途。
偶尔你会发现有些格式,即使每个分量都有相同位数,但仍有不同的扩展。例如DXGI_FORMAT_R32G32B32A32_FLOAT和DXGI_FORMAT_R32G32B32A32_UINT两种格式,他们的每个分量有相同的位数,但每一位的数据类型是不一样的(一个是FLOAT,一个是UINT)。所以这两个被看成两种格式类型。
那些没有声明类型的格式叫弱类型格式。它们的每个分量有相同的位数,但不关心包含的数据的类型。,比如DXGI_FORMAT_R32G32B32A32_TYPELESS。下面是一些常见格式的清单:
DXGI_FORMAT_R32G32B32A32_TYPELESS
DXGI_FORMAT_R32G32B32A32_FLOAT
DXGI_FORMAT_R32G32B32A32_UINT
DXGI_FORMAT_R32G32B32A32_SINT
DXGI_FORMAT_R8G8B8A8_TYPELESS
DXGI_FORMAT_R8G8B8A8_UINT
DXGI_FORMAT_R8G8B8A8_SINT
更新代码
现在你已经看过如何启用并运行DirectX了,是该你添加代码的时候了。这些代码将添加到先前创建的WinMain.cpp文件里。
写一个可用的DirectX程序的第一步是添加Direct3D头文件。
#include<d3d10.h>
#include<d3dx10.h>
下面三个变量要加到代码顶部的全局声明部分。
//D3D全局变量
ID3D10Device* pD3DDevice = NULL;
IDXGISwapChain* pSwapChain = NULL;
ID3D10RenderTargetView* pRenderTargetView = NULL;
这里创建了三个指针,一个D3D10设备,一个交换链,一个渲染目标视口。
接下来,你要添加调用 InitDirect3D函数的语句,这条语句放在WinMain函数里,调用InitWindow的语句之后。当然,InitDirect3D函数会在后面实现,而且该函数有三个参数:程序窗口的句柄、宽度和高度。
//在创建窗口后调用
if(!InitDiret3D(mianhWnd, width,height))
return 0;
如果函数出现任何错误,则程序终止。
改变消息循环
消息循环只需微小的改变。消息循环现在已经能够处理一般的程序消息,但还得稍加改动使得可以执行一些必要的游戏处理。
改动的地方就是添加一个调用Render函数的语句。这个函数放在后面来定义,用来处理屏幕上所有的绘制。
//主消息循环
MSG msg = {0};
while(WM_QUIT != msg.message){
while(PeekMessage(&msg,NULL,0,0,PM_REMOVE)==TRUE){
TranslateMessage(&msg);
DispatchMessage(&msg);
}
//调用render函数
Render();
}
Init函数
InitDirect3D函数创建D3D对象和设备
/*******************************************************************
* InitDirect3D
* Initializes Direct3D
* Inputs - Parent window handle - HWND,
Window width - int
Window height - int
* Outputs - true if successful, false if failed - bool
*******************************************************************/
bool InitDirect3D(HWND hWnd, int width, int height)
{
// Create the clear the DXGI_SWAP_CHAIN_DESC structure
DXGI_SWAP_CHAIN_DESC swapChainDesc;
ZeroMemory(&swapChainDesc, sizeof(swapChainDesc));
// Fill in the needed values
swapChainDesc.BufferCount = 1;
swapChainDesc.BufferDesc.Width = width;
swapChainDesc.BufferDesc.Height = height;
swapChainDesc.BufferDesc.Format = DXGI_FORMAT_R8G8B8A8_UNORM;
swapChainDesc.BufferDesc.RefreshRate.Numerator = 60;
swapChainDesc.BufferDesc.RefreshRate.Denominator = 1;
swapChainDesc.BufferUsage = DXGI_USAGE_RENDER_TARGET_OUTPUT;
swapChainDesc.OutputWindow = hWnd;
swapChainDesc.SampleDesc.Count = 1;
swapChainDesc.SampleDesc.Quality = 0;
swapChainDesc.Windowed = TRUE;
// Create the D3D device and the swap chain
HRESULT hr = D3D10CreateDeviceAndSwapChain(NULL,
D3D10_DRIVER_TYPE_HARDWARE,
NULL,
0,
D3D10_SDK_VERSION,
&swapChainDesc,
&pSwapChain,
&pD3DDevice);
// Error checking. Make sure the device was created
if (FAILED(hr))
{
MessageBox(hWnd, TEXT("A DX10 Compliant Video Card is Required"), TEXT("ERROR"), MB_OK);
return false;
}
// Get the back buffer from the swapchain
ID3D10Texture2D *pBackBuffer;
hr = pSwapChain->GetBuffer(0, __uuidof(ID3D10Texture2D), (LPVOID*)&pBackBuffer);
if (FAILED(hr))
{
return false;
}
// create the render target view
hr = pD3DDevice->CreateRenderTargetView(pBackBuffer, NULL, &pRenderTargetView);
// release the back buffer
pBackBuffer->Release();
// Make sure the render target view was created successfully
if (FAILED(hr))
{
return false;
}
// set the render target
pD3DDevice->OMSetRenderTargets(1, &pRenderTargetView, NULL);
// create and set the viewport
D3D10_VIEWPORT viewPort;
viewPort.Width = width;
viewPort.Height = height;
viewPort.MinDepth = 0.0f;
viewPort.MaxDepth = 1.0f;
viewPort.TopLeftX = 0;
viewPort.TopLeftY = 0;
pD3DDevice->RSSetViewports(1, &viewPort);
return true;
}
函数开始时,你得先创建并填充DXGI_SWAP_CHAIN_DESC结构。这个结构定义了由D3D10CreateDeviceAndSwapChain函数创建的交换链的属性。这个函数返回一个合法ID3D10Device对象和一个IDXGISwapChain对象。
接着,交换链中的后台缓存被当作渲染目标。最后,视口在光栅化阶段定义。
Render函数
Render函数才是实际绘制的地方。在前面可以看见,这个函数在主消息循环中每一帧都被调用。
/*******************************************************************
* Render
* All drawing happens in the Render function
* Inputs - void
* Outputs - void
*******************************************************************/
void Render(){
if (pD3DDevice != NULL){
// clear the target buffer
pD3DDevice->ClearRenderTargetView(pRenderTargetView, D3DXCOLOR(0.0f, 0.0f, 0.0f, 0.0f));
// All drawing will go here.
// display the next item in the swap chain
pSwapChain->Present(0, 0);
}
}
这是一个简单的Render函数例子。首先检查确认存在一个合法的ID3D10Device设备。如果这个对象在Render函数之前已经被释放,那后面的代码就别想执行了。
然后,你先使用ClearRenderTargetView函数,用定义好的颜色清空渲染目标。颜色是用D3DXCOLOR宏命令定义的。
接下来,你需要添加执行绘制的代码。上面的例子没有绘制任何东西,所以屏幕只是用指定色清空了。
最后,使用Present函数翻转交换链中的缓存。
ShutdownDirect3D函数
当然,程序结束时,你需要释放创建的所有对象。下面的代码是个例子:
/*******************************************************************
* ShutdownDirect3D
* Closes down and releases the resources for Direct3D
34 Chapter 2 n Your First DirectX Program
* Inputs - void
* Outputs - void
*******************************************************************/
void ShutdownDirect3D(){
// release the rendertarget
if (pRenderTargetView){
pRenderTargetView->Release();
}
// release the swapchain
if (pSwapChain){
pSwapChain->Release();
}
// release the D3D Device
if (pD3DDevice){
pD3DDevice->Release();
}
}
先检查对象是否合法(存在),合法才释放。这段函数得加到WinMain函数最后返回调用之前。
添加DirectX库
如今关于第一个DirectX程序的所有代码都有了。在编译运行之前,还有一件事要做:链接DirectX库。这个简单的例子只需要D3D10.lib这个文件。这个关于配置DirectX开发环境的问题我就不再赘述,大家自己网上搜吧。
环境配置好后,编译运行,运行后会显示一个黑色背景的空窗口。虽然结果没有表现出有关DirectX的深度,但这也是你开始DX世界的基础。
总结
这章信息有点多,包含了创建可用DirectX程序的基本流程与结构。从中应该了解一下内容:
- 如何创建Direct3D10对象
- 为每帧清屏的方法
- 改动标准消息循环以适用游戏程序
- 交换链的概念和使用方法
下一章会介绍一些2D概念,包括加载纹理和精灵动画
附上本章完整代码。再次建议初学者将代码手打一遍,看懂是骗人的。