游戏开发笔记二十七 Direct3D 11入门级知识介绍
作者:毛星云 邮箱: [email protected] 期待着与志同道合的朋友们相互交流
上一节里我们介绍了在迈入DirectX 11的学习旅程之后第一个demo创建的全过程。但由于知识衔接的需要,我们的第一个demo里面涉及到的大部分知识都是关于Win32的。而为了使之前讲解的Blank Win32 Window Demo蜕变成我们期望的Direct3D的模样,我们将在这节的笔记里面对Direct3D的入门级的基础知识做一个详细的介绍,以便在下节笔记里轻车熟路地写出属于我们的第一个完整的Direct3D11 Demo。
入门知识的第一步当然是进行DirectX开发环境的配置,这在笔记二十五里面有详细介绍,详情请移步:
【Visual C++】游戏开发笔记二十五 最简化的DirectX 11开发环境的配置
下面就开始正题,我们将分八个部分对入门级的Direct3D知识进行一个讲解。
一、 Direct3D的初始化
初始化Direct3D,我们需要完成以下四个步骤:
1.定义我们需要检查的设备类型(device types)和特征级别(feature levels)
2.创建Direct3D设备,渲染设备(context)和交换链(swap chain)。
3.创建渲染目标(render target)。
4.设置视口(viewport)
这里只是给大家一个框架的概念,各个部分下面会详细展开讲解。
二、驱动设备类型与特征等级
在Direct3D 11中我们能使用的设备有硬件设备(hardware device),参考设备(reference device),软件驱动设备(software driver device), 以及WARP设备 (WARP device)。
硬件设备(hardware device)是一个运行在显卡上的D3D设备,在所有设备中运行速度是最快的。这将是我们日后讨论最多的一种类型。
参考设备(reference device)是用于没有可用的硬件支持时在CPU上进行渲染的设备。
简言之,参考设备就是利用软件,在CPU对硬件渲染设备的一个模拟。但是不幸的是,这种方式非常的低效,所以在开发过程中,没有其他可用选择的时候,我们才采用这种方式。比如新一代的DirectX发布了,市面上还没有支持这种新版本DirectX的硬件,我们在开发过程中就只能采用这种方式来跑了。
软件驱动设备(software driverdevice)是开发人员自己编写的用于Direct3D的渲染驱动软件。这种方式通常不推荐用于高性能或者对性能要求苛刻的应用程序,下面介绍的WARP设备将是更好的选择。
WARP设备(WARPdevice)是一种高效的CPU渲染设备,可以模拟现阶段所有的Direct3D特性。WARP使用了Windows Vista /Windows 7/Winodws 8中的Windows Graphic 运行库中高度优化过的代码作为支撑,这让这种方式出类拔萃,相比与上文提到的参考设备(reference device)模式更加优秀。WARP设备在配置不高的机器上面可以达到化腐朽为神奇的功效。在我们的硬件不支持实时应用程序(real-time application)的情况下,用WARP设备作为替补是一个明智的选择,因为相比而言,参考设备(reference device)的执行效率实在是无法令人恭维。即便如此,WARP设备的执行效率还是不能和硬件设备同日而语,毕竟它依旧是对硬件的一种模拟,即使这种模拟是非常高效的。
注意:这不是对设备类型一个完整的列举,还有很多细枝末节的设备类型,在这里没必要一一列举
Direct3D的特征等级用于指定需要设定的设备目标。在这个专栏之中,我们将针对三种设备,第一种当然是我们的Direct3D 11设备,第二种为Direct3D 10.1设备,第三种为Direct3D 10.0设备。再这三种设备都无法支持的情况下,我们再选择WARP设备或者参考设备作为后援。
下面贴出来的代码段1为后面我们需要用到的驱动类型和特征级别的一个声明。通过创建各种类型的数组,我们可以使用循环来尝试首先创建我们最需要的设备,然后若执行失败则继续创建其他的设备类型。浅墨记得我们之前提到过,Win32宏ARRAYSIZE能够用来返回一个数组的大小,Win32函数GetClientRect可以用来计算应用程序客户区的大小。算出来的值会用于设置之后的D3D设备渲染的宽度和高度。
另外,需要记住Win32应用程序是分客户区和非客户区的,我们仅能在客户区上进行渲染。
代码段1 指明驱动设备类型和特征等级
RECT dimensions; GetClientRect( hwnd, &dimensions ); unsigned int width = dimensions.right - dimensions.left; unsigned int height = dimensions.bottom - dimensions.top; D3D_DRIVER_TYPE driverTypes[] = { D3D_DRIVER_TYPE_HARDWARE, D3D_DRIVER_TYPE_WARP,D3D_DRIVER_TYPE_SOFTWARE }; unsigned int totalDriverTypes = ARRAYSIZE( driverTypes ); D3D_FEATURE_LEVEL featureLevels[] = { D3D_FEATURE_LEVEL_11_0, D3D_FEATURE_LEVEL_10_1, D3D_FEATURE_LEVEL_10_0 }; unsigned int totalFeatureLevels = ARRAYSIZE( featureLevels );
三、设备与交换链的创建
下一步便是创建一个交换链,交换链在Direct3D中为一个设备渲染目标的集合。每一个设备都有至少一个交换链,而多个交换链能够被多个设备所创建。一个交换目标可以为一个渲染和显示到屏幕上的颜色缓存(在后面会讨论),等等。
通常在游戏中有,有两种颜色缓存,分别叫做主缓存和辅助缓存,他们一起被称为前后台缓存组合。主缓存中的内容(前台缓存)会显示在屏幕上,而辅助缓存(后台缓存)用于绘制下一帧。
渲染的发生非常之快,屏幕的一部分可以在显示器完成显示更新之前,在先前的结果为基础上进行绘制。缓存之间的切换,可以进行一个良性的运作,前台在显示图像,后台正在为前台准备下一刻将要显示的图像,这样做可以避免很多棘手的问题,提高了效率。
这种技术在计算机图形学中叫做双缓冲(doublebuffering),或者叫页面翻转(page flipping)(这种技术我们之前的一系列Win32 GDI demo中使用得比较勤,研究了之前的demo的朋友们应该已经耳濡目染了吧)。一个交换链能拥有一个或者多个这样的缓冲。
代码段2中列出了创建一个交换链的代码。一个交换链的描述用来定义和创建符合我们需要的交换链。
代码段2 对交换链的设置
DXGI_SWAP_CHAIN_DESC swapChainDesc; ZeroMemory( &swapChainDesc, sizeof( swapChainDesc ) ); 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.Windowed = true; swapChainDesc.SampleDesc.Count = 1; swapChainDesc.SampleDesc.Quality = 0;
这个范例中定义了D3D的多种取样属性,多重取样(Multisampling)是一种用于采样和平衡渲染像素的创建亮丽色彩变化之间的平滑过渡的一种技术。
缓存的使用和交换链的描述有大量的成员需要设置,但这些设置都是非常简单的。缓存的对交换链的使用是设置下DXGI_USAGE_RENDER_TARGET_OUTPUT,以便交换链能够用于输出,或者换句话说,它能被渲染。
下一步是创建渲染上下文,渲染设备,以及我们拥有的交换链描述。D3D设备一般都是设备本身和硬件之间的通信,而D3D上下文是一种描述设备如何绘制的渲染设备上下文,这也包含了渲染状态和其他的绘图信息。
正如我们讨论过的,交换链是设备和上下文将要绘制的渲染目标。
创建设备上下文,渲染上下文和交换链所需的代码在代码段3中详细列出了,.这段代码为下次内容即将展示的Direct3D 11 BlankWindows Demo的一个片段。
代码段3 Direct3D设备,设备上下文,以及交换链的创建
ID3D11Device device_; ID3D11Context d3dContext_; IDXGISwapChain swapChain_; unsigned int creationFlags = 0; #ifdef _DEBUG creationFlags |= D3D11_CREATE_DEVICE_DEBUG; #endif HRESULT result; unsigned int driver = 0; for( driver = 0; driver < totalDriverTypes; ++driver ) { result = D3D11CreateDeviceAndSwapChain( 0, driverTypes[driver],0, creationFlags, featureLevels, totalFeatureLevels, D3D11_SDK_VERSION, &swapChainDesc, &swapChain_, &d3dDevice_, &featureLevel_, &d3dContext_ ); if( SUCCEEDED( result ) ) { driverType_ = driverTypes[driver]; break; } } if( FAILED( result ) ) { DXTRACE_MSG( "Failed to create the Direct3D device!"); return false; }
交换链,设备和渲染上下文可以在单独的Direct3D函数调用中被创建,或者通过特定对象的Direct3D来调用(例如用CreateSwapChain函数来专门创建一个交换链)。
这个函数为D3D11CreateDeviceAndSwapChain。在代码段2中我们在每个驱动类型中循环,试图创建一个合适得设备,或为一个硬件设备,或为一个WARP设备,抑或一个参考设备(reference device)。因为如果创建失败,我们就无法初始化我们的Direct3D。
D3D11CreateDeviceAndSwapChain函数中包含了特征等级作为其参数, 所以如果至少有一个这样的特征等级存在,而且若我们的设备类型也存在,这个函数才会执行成功。
其中D3D11CreateDeviceAndSwapChain函数具有如下的函数原型:
HRESULT D3D11CreateDeviceAndSwapChain( IDXGIAdapter *pAdapter, D3D_DRIVER_TYPEDriverType, HMODULE Software, UINT Flags, const D3D_FEATURE_LEVEL*pFeatureLevels, UINT FeatureLevels, UINT SDKVersion, const DXGI_SWAP_CHAIN_DESC *pSwapChainDesc, IDXGISwapChain **ppSwapChain, ID3D11Device **ppDevice, D3D_FEATURE_LEVEL*pFeatureLevel, ID3D11DeviceContext**ppImmediateContext );
四、 创建渲染目标视图
一个渲染目标视图是一个由Output MergerStage读取的D3D资源。为了output merger能渲染一个后台缓存的交换链,我们为其创建一个渲染目标视图。
由于纹理的概念说来话长,目前我们将纹理理解为一副图像就行了,后面中我们将展开讨论纹理的很多细节内容,。交换链的主缓存和辅助缓存为彩色的图像,为了获得它们的指针,我们一般会调用交换链中的函数GetBuffer。
得到指向缓存的指针后,我们调用Direct3D中的函数CreateRenderTargetView,来创建一个渲染目标视图(rendertarget view.)。渲染目标视图含有ID3D11RenderTargetView类型,而CreateRenderTargetView函数将创建我们视图的2D纹理,渲染目标描述,我们创建的ID3D11RenderTargetView的对象地址为其函数变量。将渲染目标描述变量设为空给我们所有的表面的MIP映射水平都为0级,MIP映射水平也将在后面进行详细讨论。
我们完成渲染目标的创建之后,就能够释放指针到交换链的后台缓存了。因为得到了COM对象的一个引用,我们必须调用COM中的Release函数来减少引用的数量。这样做会避免内存的泄露,因为我们不想应用程序退出后,系统仍然保留着这里内存,这将导致系统资源的浪费,而这种浪费是不科学的。
在每次我们想渲染一个特定的渲染目标的时候,必须在所有的绘制的函数调用之前对它进行设置。这个重任就交给了我们的OMSetRenderTarget函数,这个函数隶属于output merger,在之后会讲到。
代码段4 渲染目标视图的创建和绑定
ID3D11RenderTargetView* backBufferTarget_; ID3D11Texture2D* backBufferTexture; HRESULT result = swapChain_->GetBuffer( 0, __uuidof(ID3D11Texture2D ), ( LPVOID* )&backBufferTexture ); if( FAILED( result ) ) { DXTRACE_MSG( "Failed to get the swap chain backbuffer!" ); return false; } result = d3dDevice_->CreateRenderTargetView(backBufferTexture, 0, &backBufferTarget_ ); if( backBufferTexture ) backBufferTexture->Release( ); if( FAILED( result ) ) { DXTRACE_MSG( "Failed to create the render targetview!" ); return false; } d3dContext_->OMSetRenderTargets( 1, &backBufferTarget_, 0);
在代码段4中你会注意到我们采用了一个叫做DXTRACE_MSG的宏。这个宏用作debugging来用。在之后将进行更详细的讲解。
五、 视口
Direct3D中 的一个重点同时也是难点在于创建和设置视口。
视口定义了我们渲染到屏幕上的面积。在单人或者非分割画面的多人游戏中一般都为全屏,所以我们设置视口的宽度和高度即为交换链的宽度和高度。对于分屏游戏,我们可以创建两个视口,一个视口定义在屏幕上方,另一个定义在屏幕下方。为了渲染分屏视口,我们可以分别以两位不同玩家的角度来渲染。
视点的创建由填充D3D11_VIEWPORT函数和设置调用上下文的RSSetViewports函数将其设置到渲染上下文中来完成。RSSetViewports函数需要我们设置的视口数量和视口对象的列举。全屏视口的创建和设置的相关代码在代码段五中有列举,其中X和Y标明左侧和顶部屏幕的位置,最小和最大深度是0到1之间的值,表明了视口深度的最小和最大值。
代码段5 全屏视口的创建和设置
D3D11_VIEWPORT viewport; viewport.Width = static_cast<float>(width); viewport.Height = static_cast<float>(height); viewport.MinDepth = 0.0f; viewport.MaxDepth = 1.0f; viewport.TopLeftX = 0.0f; viewport.TopLeftY = 0.0f; d3dContext_->RSSetViewports( 1, &viewport );
六、清除与显示屏幕
渲染到屏幕需要几个不同的步骤。第一步通常是清除相关渲染目标的表面。在大部分游戏中这一步包含了深度缓存等一系列内容。在下一节即将呈现的demo中我们将在本章稍后实施,我们将清除渲染目标视图的颜色缓冲区到一种特定的颜色。这由调用D3D中的ClearRenderTargetView函数来完成。ClearRenderTargetView拥有如下的函数原型:
void ClearRenderTargetView( ID3D11RenderTargetView*pRenderTargetView, const FLOAT ColorRGBA[4] );
注:目前的大部分商业游戏中在渲染之前清除颜色缓存并不是必须的,因为像天空这样的环境图形要确保每个像素都会被颜色缓存所覆盖着。
ClearRenderTargetView函数以将被清理的渲染目标视图作为其变量。为了清除屏幕,我们设定某种颜色作为我们需要的背景阴影的颜色。这种颜色可以是红色,绿色,蓝色,和透明色Alpha数组中任意指定的0.0到1.0之间的颜色。这里0.0表示强度为0,而1.0表示完全饱满的强度。若对应于字节,1.0对应255。如果为红绿蓝颜色组合都为1.0,则会得到纯白的颜色。下一步就是绘制场景的几何形状了,最后一步是调用交换链的Present函数在屏幕上显示渲染缓冲区的内容。
Present函数具有以下的声明:
HRESULT IDXGISwapChain::Present( UINT SyncInterval, UINT Flags);
对Present函数的参数一个简单的理解:syncinterval 同步间隔,
flags 演示的标志。
在第n个垂直空白之后,Syncinterval能被设置为0,1,2,3,4来显示。垂直空白是当前帧的最后一列更新时间与下一帧的第一列更新时间的时间差。像电脑显示器这样的设备显示更新像素为垂直的,一列一列进行更新的。
Present函数的flags值可被设为0,表示输出到每一个缓冲区,设为DXGI_PRESENT_ TEST时则表示测试时不进行输出,或为DXGI_PRESENT_DO_ NOT_SEQUENCE表示不进行排序地利用垂直空白同步输出来显示输出。为达到预期的目的,我们可以只是传递0到Present函数来显示我们的渲染结果。
代码段五 展示了一个清屏和显示视图的例子。在后面我们将深入探究颜色缓存,深度存,使画面流畅无比的双缓冲等等。
代码段6 清除渲染目标然后显示显得渲染场景
loat clearColor[4] = { 0.0f, 0.0f, 0.25f, 1.0f }; d3dContext_->ClearRenderTargetView( backBufferTarget_,clearColor ); swapChain_->Present( 0, 0 );
七、关于格式
有时候我们需要创建指定的DXGI格式。格式可以用于描述一张图像的布局,每种颜色的位数,或者顶点缓存的布局(后面会讲到)。大多数情况下,DXGI格式用于描述交换链中的顶点布局。
举个例子,DXGI_FORMAT_R8G8B8A8_UNORM,它表示我们定义的每一个RGBA组成部分的数据都为8位。
没指名类型的格式我们称作无类型格式(typeless formats)。他们为每个部分保存相同的位数,但是并不注重包含了什么类型的数据。如DXGI_FORMAT_R32G32B32A32_TYPELESS。常用的清单类型在下面中列出了。
常用的数据格式类型清单:
DXGI_FORMAT_R32G32B32A32_TYPELESS 128位RGBA无类型格式
DXGI_FORMAT_R32G32B32A32_FLOAT 128位RGBA浮点型格式
DXGI_FORMAT_R32G32B32A32_UINT 128位RGBA无符号整型格式
DXGI_FORMAT_R32G32B32A32_SINT 128位RGBA带符号整型格式
DXGI_FORMAT_R8G8B8A8_TYPELESS 32位RGBA无类型格式
DXGI_FORMAT_R8G8B8A8_UINT 32位RGBA无符号整型格式
DXGI_FORMAT_R8G8B8A8_SINT 32位RGBA带符号整型格式
当定义顶点格式的时候,比如DXGI_FORMAT_R32G32B32_FLOAT格式,就是说RGB值都支持是32位的数据类型。有时候,我们会看到特殊的为每一部分指定相同位数的格式,但是他们有不同的扩展名。
举个例子,DXGI_FORMAT_R32G32B32A32_FLOAT 和DXGI_FORMAT_R32G32B32A32_UINT类型的各个部分的位数都是相同的,不同的各个位数上一个是32位的浮点型,一个是32位的无符号整型。
八、 善后工作
Direct3D应用程序中要做的最后一件事情,就是清除和释放我们创建的对象。举个例子,在应用程序开头,我们要创建一个D3D的设备,一个D3D的渲染上下文,一个交换链,以及一个要渲染的目标。当这个应用程序关闭的时候,我们需要释放这些对象,以将这些资源返还给系统。
COM对象保持一个引用计数,告知系统什么时候从内存中移除这些对象是安全的。通过运用Release函数,我们减少了一个对象的引用数量。当引用数量达到0,系统便会回收这些资源。
下面是一个释放D3D对象的范例。用首先用if条件句来确保对象不为null,然后调用Release函数。通常我们以和创建时相反的顺序来释放这些对象。
代码段7 释放Direct3D 11 main对象
if( backBufferTarget_ )backBufferTarget_->Release( ); if( swapChain_ ) swapChain_->Release( ); if( d3dContext_ ) d3dContext_->Release( ); if( d3dDevice_ ) d3dDevice_->Release( );
心得:在释放对象前,我们经常通过检查来确保DirectX对象不为null。因为试图释放一个非法的指针是非常不科学的,这会使我们游戏程序的稳定性荡然无存,经常各种无故崩溃。
本篇文章到这里就结束了,谢谢欣赏。
感谢一直支持【Visual C++】游戏开发笔记系列专栏的朋友们。
【Visual C++】游戏开发 系列文章才刚刚展开一点而已,因为游戏世界实在是太博大精深了~
但我们不能着急,得慢慢打好基础。做学问最忌好高骛远,不是吗?
浅墨希望看到大家的留言,希望与大家共同交流,希望得到睿智的评论(即使是批评)。
你们的支持是我写下去的动力~
精通游戏开发的路还很长很长,非常希望能和大家一起交流,共同学习,共同进步。
大家看过后觉得值得一看的话,可以顶一下这篇文章,你们的支持是我继续写下去的动力~
如果文章中有什么疏漏的地方,也请大家指正。也希望大家可以多留言来和我探讨相关的问题。
最后,谢谢你们一直的支持~~~
――――――――――浅墨于2012年7月1日