Direct3D初始化后,就该开始进行一些实际的渲染。渲染本身非常容易,但涉及到一些准备工作。在本课程中,我们将一遍又一遍的设置所有内容以渲染一个空白帧。涉及到的设置非常简单,主要有两件事要做:首先,我们需要告诉GPU在内存中的哪个位置创建最终图像(对我们来说,是在后台缓冲区);其次,我们需要告诉GPU应该在后台缓冲区上的哪个位置进行绘制。一旦完成这两个步骤,我们就可以渲染了,而渲染很简单。
设定渲染目标
首先我们需要确定渲染到哪里,你可能会想当然说“后缓冲区”。然而,Direct3D实际上并不知道这一点,你可能也不想立即渲染到后缓冲区。例如,许多游戏渲染到模型的表面,然后将该模型渲染到后台缓冲区,这项技术可以产生多种效果。如果你玩过《传送门》这个游戏,你将看到下面的场景。在《传送门》中,游戏引擎首先渲染到传送门上,然后渲染包含传送门图像在内的整个场景。
在Direct3D中进行渲染时,必须建立渲染目标,这是一个简单的COM对象,它在显存中维护一个位置,供你渲染。在大多数情况下,这个位置就是后缓冲区。下面就是我们的方法:
ID3D11RenderTargetView *backbuffer; // 全局声明
// 此函数初始化并准备Direct3D以供使用
void InitD3D(HWND hWnd)
{
// Direct3D初始化
// ...
// 获取后缓冲区的地址
ID3D11Texture2D *pBackBuffer;
swapchain->GetBuffer(0, __uuidof(ID3D11Texture2D), (LPVOID*)&pBackBuffer);
// 使用后缓冲区地址来创建渲染目标
dev->CreateRenderTargetView(pBackBuffer, NULL, &backbuffer);
pBackBuffer->Release();
// 将渲染目标设置为后缓冲区
devcon->OMSetRenderTargets(1, &backbuffer, NULL);
}
这里要做三件事:首先,我们确定后缓冲区的地址;其次,我们使用该地址创建一个COM对象来表示渲染目标;最后,我们将该对象设置为活动渲染目标。下面,我们逐一介绍这些新代码:
代码 | 描述 |
---|---|
ID3D11RenderTargetView *backbuffer; |
该变量是指向一个COM对象的指针,该对象保存了有关渲染目标的所有信息。我们将渲染到后缓冲区,因此我们将此变量称为“backbuffer” |
ID3D11Texture2D *pBackBuffer; |
在3D渲染中,纹理是图像的一个别称。ID3D11Texture2D 是存储平面图像的对象。就像任何COM对象一样,我们首先定义指针,然后再由一个函数为我们创建该对象。 |
swapchain->GetBuffer(0, __uuidof(ID3D11Texture2D), (LPVOID*)&pBackBuffer); |
GetBuffer()函数的作用是在交换链上找到后缓冲区,并使用它创建pBackBuffer 纹理对象。第一个参数是要获取后缓冲区的编号,我们仅在该链上使用一个后缓冲区,它就是#0后缓冲区,因此,第一个参数设置为0;第二个参数是标识ID3D11Texture2D COM对象的编号,每种类型的COM对象都有其自己的唯一ID,该ID用于获取有关它的信息。要获得此ID,我们必须使用_uuidof()运算符。这个运算符确切的细节并不重要,我们只需要知道这样做可以让GetBuffer()函数知道应该创建哪种类型的对象;第三个参数是一个void指针,这个void填充了ID3D11Texture2D 对象的位置。这个指针的类型必须是void,因为我们可能需要放置其他类型的对象 |
dev->CreateRenderTargetView(pBackBuffer, NULL, &backbuffer); |
这个函数创建渲染目标对象,我们在程序顶部为该对象创建了指针。第一个参数是指向纹理的指针,对于我们的程序而言,它应该是“pBackBuffer ”;第二个参数是描述渲染目标的结构,我们不需要为后缓冲区填写此内容,因此设置为NULL;第三个参数是对象指针的地址,因此我们将使用“&backbuffer ”。 |
pBackBuffer->Release(); |
Release()函数释放所有内存并关闭COM对象使用的所有线程。我们已经使用完了pBackBuffer对象,因此对其进行Release()。注意,这不会破坏后缓冲区,只会关闭我们曾经访问的纹理对象。 |
devcon->OMSetRenderTargets(1, &backbuffer, NULL); |
最后一个函数实际上是设置渲染目标,更确切的说,它设置了多个渲染目标。第一个参数是要设置的渲染目标的数量,通常为1,但某些情况下会更大;第二个参数是指向渲染目标视图对象列表的指针,我们只有一个渲染目标视图对象,所以只需要将渲染目标视图对象的地址传入即可;第三个参数是高级的,后面的课程将会讲到,这里设为NULL就行。 |
这部分代码非常重要,确保你理解了所有的内容,因为在Direct3D编程中将多次用到并进行修改。
设置ViewPort
viewport
是一种将像素坐标转换为归一化坐标的方法,下图显示了两者之间的区别:
在左图中,像素坐标从0开始,0坐标位于左上角,并依次增加一个像素。在右图中,无论后缓冲区的大小如何,归一化坐标都是从(-1,-1)增加到(1,1),归一化是指将值调整为等于1。(-1,-1)和(1,1)等于什么由viewport决定。viewport是一种结构体,可让我们设置(-1,-1)和(1,1)在像素坐标中的位置。设置viewport的代码如下所示:
// this function initializes and prepares Direct3D for use
void InitD3D(HWND hWnd)
{
// Direct3D初始化
// ...
// 设置渲染目标
// ...
// 设置viewport
D3D11_VIEWPORT viewport;
ZeroMemory(&viewport, sizeof(D3D11_VIEWPORT));
viewport.TopLeftX = 0;
viewport.TopLeftY = 0;
viewport.Width = 800;
viewport.Height = 600;
devcon->RSSetViewports(1, &viewport);
}
这里唯一需要说明的就是最后一行代码。RSSetViewports()
是一个激活viewport结构的函数,第一个参数是使用的viewport的数量,第二个参数是指向viewport结构的指针列表的地址。在某些高级情况下,使用多个viewport非常方便,但在这里我们不进行讲述。目前使用1和"&viewport"即可。
渲染框架
接下来,我们将创建一个呈现单个帧的函数。目前该框架非常简单,仅包含蓝色背景,当然,你可以根据需要更改颜色。这是此函数的代码:
// 这是用于渲染单个帧的函数
void RenderFrame(void)
{
// 将后缓冲区清除为深蓝色
devcon->ClearRenderTargetView(backbuffer, D3DXCOLOR(0.0f, 0.2f, 0.4f, 1.0f));
// 在这里进行后缓冲区上的3D渲染
// 切换后缓冲区和前缓冲区
swapchain->Present(0, 0);
}
devcon->ClearRenderTargetView()
这将使用特定颜色填充渲染目标缓冲区。在我们的场景下,将填充后缓冲区。函数共两个参数,第一个参数是渲染目标对象的地址,我们在这里传入“backbuffer”。第二个参数是用来填充后缓冲区的颜色,为此,我们使用一个称为D3DXCOLOR的简单结构,四个构造函数参数用于构建颜色,前三个时红色、绿色和蓝色,第四个是alpha值(在后缓冲区中没有意义,但却有必要写上)。
swapchain->Present()
接下来,我们调用Present()函数,此函数实际上是显示后缓冲区上的所有内容,它的工作本质上就是在交换链中执行“交换”,以使后缓冲区成为前缓冲区,它的两个参数都设为0。
强制清理
在上一节中,我们通过释放创建的每个COM对象来关闭Direct3D。大多是COM对象都有这个Release()函数,并且这些COM对象都必须在完成之后调用Release()函数,渲染目标对象也不例外。
// 这个函数时为了清理Ditrect3D和COM
void CleanD3D()
{
// 关闭并释放所有现有的COM对象
swapchain->Release();
backbuffer->Release();
dev->Release();
devcon->Release();
}
最终的程序
我们已经运行了Direct3D并且在窗口中进行了简单的空白帧渲染。
// include the basic windows header files and the Direct3D header files
#include <windows.h>
#include <windowsx.h>
#include <d3d11.h>
#include <d3dx11.h>
#include <d3dx10.h>
// 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
ID3D11RenderTargetView *backbuffer; // the pointer to our back buffer
// function prototypes
void InitD3D(HWND hWnd); // sets up and initializes Direct3D
void RenderFrame(void); // renders a single frame
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;
}
RenderFrame();
}
// 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 = 1; // how many multisamples
scd.SampleDesc.Quality = 0; // multisample quality level
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);
// get the address of the back buffer
ID3D11Texture2D *pBackBuffer;
swapchain->GetBuffer(0, __uuidof(ID3D11Texture2D), (LPVOID*)&pBackBuffer);
// use the back buffer address to create the render target
dev->CreateRenderTargetView(pBackBuffer, NULL, &backbuffer);
pBackBuffer->Release();
// set the render target as the back buffer
devcon->OMSetRenderTargets(1, &backbuffer, NULL);
// Set the viewport
D3D11_VIEWPORT viewport;
ZeroMemory(&viewport, sizeof(D3D11_VIEWPORT));
viewport.TopLeftX = 0;
viewport.TopLeftY = 0;
viewport.Width = 800;
viewport.Height = 600;
devcon->RSSetViewports(1, &viewport);
}
// this is the function used to render a single frame
void RenderFrame(void)
{
// clear the back buffer to a deep blue
devcon->ClearRenderTargetView(backbuffer, D3DXCOLOR(0.0f, 0.2f, 0.4f, 1.0f));
// do 3D rendering on the back buffer here
// switch the back buffer and the front buffer
swapchain->Present(0, 0);
}
// this is the function that cleans up Direct3D and COM
void CleanD3D(void)
{
// close and release all existing COM objects
swapchain->Release();
backbuffer->Release();
dev->Release();
devcon->Release();
}
现在我们有了帧渲染,会得到一个蓝色的窗口。