(注:
【D3D11游戏编程】学习笔记系列由CSDN作者BonChoix所写,转载请注明出处:
http://blog.csdn.net/BonChoix
,谢谢~)
初次使用D3D11,先从它的初始化开始。不过在使用D3D之前,需要了解几个重要的概念:
1. 硬件能力:Hardware Capacity
熟悉D3D9的会很清楚,在初始化d3d9的一开始需要做的就是检测硬件的能力,以了解该用户机器支持哪些d3d特性,哪些不支持,以在运行期合理的调用API。除非使用d3d自带的“软件渲染引擎”,否则试图使用硬件不支持的特性是会出错的。但是在d3d11中,检测硬件能力这一步不再有必要。这对程序员来说省了一点功夫,但是额外的要求是,该硬件必须支持d3d11的全部特性!因此实际上,d3d11应用程序对硬件的要求更苛刻了。
2. 数据格式
D3D应用程序中,无论是纹理图片,还是创建的缓冲区,都有着特定的数据格式。D3D11支持有限的数据格式,以枚举变量形式存在,如下几种:
DXGI_FORMAT_R32G32B32_FLOAT: 3个32位单精度符点数组成,比如用于代表三维空间坐标,以及24位颜色;
DXGI_FORMAT_R16G16B16A16_UNORM: 4个16位数组成,每个成员位于[0,1.0f]之间,UNORM意指:unsigned normalized,即无符号,且归一化的;
DXGI_FORMAT_R32G32_UINT:2个32位数组成,每个成员为无符号整型(unsigned int);
DXGI_FORMAT_R8G8B8A8_UNORM:4个8位数组成,每个成员为[0,1.f]之间;
DXGI_FORMAT_R8G8B8A8_SNORM:4个8位数组成,每个成员为[-1.0f, 1.0f]之间,SNORM意指:signed normalized;
DXGI_FORMAT_R8G8B8A8_SINT:4个8位数组成,每个成员为有符号整型;
此外还有很多其他类型的数据格式,熟悉各部分的意义后对任何类型可以很快从名字中得知其意思,也很容易写出指定意义的数据格式对应的枚举变量。数据格式在程序中使用相当频繁,因此很有必要提前熟悉该枚举类型变量的特点。
3. DXGI
DXGI,即DirectX Graphics Infrastructure,是从d3d10开始出现的,封装了一些d3d底层、基础的机制,包括数据格式定义、交换链(SwapChain),枚举设备类型及检测设备信息等。由于这些机制适合于任何一代的D3D接口,而跟特定的d3d特性无关,更新远不及其他d3d特性频繁,因此微软把这些从direct3d中单独分离了出来。这部分对应的枚举变量及函数等都以DXGI开头。
4. 交换链:SwapChain
交换链不是d3d11特有的性质,而是任何2D、3D计算机动画最基本的机制之一。为了实现平滑的动画,至少需要两个缓冲区,一个前缓冲区用于显示,一个后缓冲区用于下一帧的绘制,每次绘制完一帧后通过交换前、后缓冲区对应的指针来显示新一帧,并在之前的前缓冲区(当前的后缓冲区)上开始继续绘制下一帧。交换链可以有3个或者更多缓冲欧,但绝大多数情况下,2个已经足够了。在d3d11中交换链对应的接口为IDXGISwapChain。
5. 深度/模板缓冲区:Depth/Stencil Buffer
深度缓冲区是与交换链缓冲区大小完全一样的一块显存区域,即每个像素在深度缓冲区中对应相应的位置。在渲染管线的最终的混合阶段(Output Merger Stage),每个片段(Fragment)都有一个深度值z,与深度缓冲区对应位置上的深度相比较,如果该片段z更小,则绘制该片段,并覆盖当前的尝试值,否则抛弃该片段。该缓冲区主要用于实现投影在屏幕上同一位置、远近不同的物体之间相同的遮挡效果。此外,灵活配置尝试缓冲区,可以实现很多种高级特效。
模板缓冲区与深度缓冲区共享同一块区域,每个深度值处对应一个模板值,模板值主要用于实现一些高级的特效,比如平面反射、阴影等。利用深度、模板缓冲区的不同的configuration实现各种特效在后面会有详细介绍的。
6. 视图(View)
视图是D3D11中的最重要的概念之一。以纹理为例,一个纹理可以被绑定到3D渲染管线的不同阶段以实现不同的用途。比如常见的纹理图片(d3d11中叫Shader Resource),另一方面也可用于渲染对象(Render Target),充当交换链中的缓冲区,用于绘制场景。同一个纹理可以同时被绑定到多个管线阶段(Dynamic Cube Mapping就是一个例子,后面会专门介绍),但在创建该纹理资源时需要指定其绑定的类型(Bind Flags),比如上述例子,绑定类型要指定为:D3D11_BIND_RENDER_TARGET | D3D11_SHADER_RESOURCE。
实际上,在d3d11中,绑定到管线某阶段的并不是纹理资源本身,而是对应的“视图”。对于使用该纹理的每一种方式,我们分别创建相应的视图,然后把视图绑定到管线特定的阶段。D3D11中针对每种视图相应的接口为:ID3D11ShaderResourceView和ID3D11RenderTargetView、ID3D11DepthStencilView。使用视图的具体细节在后面的过程中会慢慢体会到。
7. 多重采样抗锯齿:Multisampling Atialiasing
针对光栅化显示器抗锯齿的方法有多种,在d3d中采用的多重采样方法。即在每个像素点内部,设置多个采样点,绘制多边形边缘时,针对每个采样点判断是否被多边形覆盖,最终的颜色值从采样点中取均值,以对多边形的边缘进行“模糊化”,从而减轻锯齿效果。如下图所示,这是一个4重采样的例子,该像素最终的颜色值是多边形本身颜色值的3/4:
支持d3d11的硬件全部支持4重采样,因此我们在后面的程序中将普遍使用4个采样点。在d3d11中通过结构DXGI_SAMPLE_DESC来设置多重采样,其定义如下:
typedef struct DXGI_SAMPLE_DESC {
UINT Count;
UINT Quality;
} DXGI_SAMPLE_DESC, *LPDXGI_SAMPLE_DESC;
Count为我们设置的采样的个数,Quality为机器支持的不同的等级,初始化过程中我们会对Quality进行检测。
注意Multisampling区别于super sampling的关键点:不同采样点是分别计算颜色值(super sampling)还是共享同一颜色值(Multisampling)。详细情况有很多参考书介绍。
8. 特征等级:Feature Level
特征等级定义了一系列支持不同d3d功能的相应的等级,用意即如果一个用户的硬件不支持某一特征等级,程序可以选择较低的等级,以确保正确地运行。d3d11中定义了如下几个等级以代表不同的d3d版本:
typedef enum D3D_FEATURE_LEVEL {
D3D_FEATURE_LEVEL_9_1 = 0x9100,
D3D_FEATURE_LEVEL_9_2 = 0x9200,
D3D_FEATURE_LEVEL_9_3 = 0x9300,
D3D_FEATURE_LEVEL_10_0 = 0xa000,
D3D_FEATURE_LEVEL_10_1 = 0xa100,
D3D_FEATURE_LEVEL_11_0 = 0xb000
} D3D_FEATURE_LEVEL;
在初始化过程中,我们可以提供一组不同的特征等级,程序会从第一个开始逐个检测,碰到第一个合适的来创建设备。因此我们在数组中从高到低放置特征等级提供给初始化程序。
OK,重要的概念就是这些,现在开始初始化D3D~
D3D11的初始化主要有以下几个步骤:
1. 创建设备ID3D11Device和设备上下文ID3D11DeviceContext;
2. 检测多重采样支持的等级:CheckMultisampleQualityLevels
3. 创建交换链
4. 创建RenderTargetView
5. 创建DepthStencilView
6. 把上述两个视图绑定到渲染管线相应的阶段
7. 设置Viewport
下面从第一步开始:
1. 创建设备及上下文:
函数原型如下:
HRESULT D3D11CreateDevice(
__in IDXGIAdapter *pAdapter,
__in D3D_DRIVER_TYPE DriverType,
__in HMODULE Software,
__in UINT Flags,
__in const D3D_FEATURE_LEVEL *pFeatureLevels,
__in UINT FeatureLevels,
__in UINT SDKVersion,
__out ID3D11Device **ppDevice,
__out D3D_FEATURE_LEVEL *pFeatureLevel,
__out ID3D11DeviceContext **ppImmediateContext
);
pAdapter来选择相应的图形适配器,设为NULL以选择默认的适配器;
DriverType设置驱动类型,一般毫无疑问选择硬件加速,即D3D_DRIVER_TYPE_HARDWARE,此时下一个参数就是NULL;
Flags为可选参数,一般为NULL,可以设为D3D11_CREATE_DEVICE_DEBUG、D3D11_CREATE_DEVICE_SINGLETHREADED,或两者一起,前者让要用于调试时收集信息,后者在确定程序只在单线程下运行时设置为它,可以提高性能;
pFeatureLevels为我们提供给程序的特征等级的一个数组,下一个参数为数组中元素个数;
SDKVersion恒定为D3D11_SDK_VERSION;
ppDevice为设备指针的地址,注意设备是指针类型,这里传递的是指针的地址(二维指针,d3d程序中所有的接口都声明为指针类型!);
pFeatureLevel为最后程序选中的特征等级,我们定义相应的变量,传递它的地址进来;
ppImmediateContext为设备上下文指针的地址,要求同设备指针。
创建设备代码如下:
ID3D11Device *d3dDevice(NULL);
ID3D11DeviceContext *deviceContext(NULL);
D3D_FEATURE_LEVEL featureLevels[6] = {
D3D_FEATURE_LEVEL_11_0,
D3D_FEATURE_LEVEL_10_1,
D3D_FEATURE_LEVEL_10_0,
D3D_FEATURE_LEVEL_9_3,
D3D_FEATURE_LEVEL_9_2,
D3D_FEATURE_LEVEL_9_1};
D3D_FEATURE_LEVEL curLevel;
D3D11CreateDevice(
0, //默认适配器
D3D_DEVICE_TYPE_HARDWARE, //硬件加速设备类型
0,
0,
featureLevels, 6,
D3D11_SDK_VERSION,
&d3dDevice,
&curLevel,
&deviceContext);
创建完即可检测多重采样(我们只用4重采样)的等级:
m_d3dDevice->CheckMultisampleQualityLevels(DXGI_FORMAT_R8G8B8A8_UNORM,4,&x4MsaaQuality);
2. 创建交换链
交换链性质通过如下结构指定:
typedef struct DXGI_SWAP_CHAIN_DESC {
DXGI_MODE_DESC BufferDesc;
DXGI_SAMPLE_DESC SampleDesc;
DXGI_USAGE BufferUsage;
UINT BufferCount;
HWND OutputWindow;
BOOL Windowed;
DXGI_SWAP_EFFECT SwapEffect;
UINT Flags;
} DXGI_SWAP_CHAIN_DESC;
BufferDesc指定后缓冲区有关特性;
SampleDesc指定多重采样,前面说过;
BufferUsage,对于交换链,为DXGI_USAGE_RENDER_TARGET_OUTPUT;
BufferCount:我们只创建一个后缓冲区(双缓冲),因此为1;
OutputWindow:指定窗口句柄,Win32程序初始化完创建的主窗口;
Windowed:是否全屏;
DXGI_SWAP_EFFECT:通常为DXGI_SWAP_EFFECT_DISCARD;
Flags:可选,我们设为0;
DXGI_MODE_DESC结构定义如下:
typedef struct DXGI_MODE_DESC {
UINT Width;
UINT Height;
DXGI_RATIONAL RefreshRate;
DXGI_FORMAT Format;
DXGI_MODE_SCANLINE_ORDER ScanlineOrdering;
DXGI_MODE_SCALING Scaling;
} DXGI_MODE_DESC, *LPDXGI_MODE_DESC;
Width、Height为缓冲区大小,一般设为主窗口大小;
Format为缓冲区类型,一般作为渲染对象缓冲区类型为DXGI_FORMAT_R8G8B8A8_UNORM;
其他参数一般为固定的,参见代码。
此外,创建交换链需要获得接口IDXGIFactory,过程是固定的,创建交换链代码如下:
DXGI_SWAP_CHAIN_DESC scDesc = {0}; //填充结构,设置交换链相当属性
scDesc.BufferDesc.Format = DXGI_FORMAT_R8G8B8A8_UNORM; //缓冲区数据格式
scDesc.BufferDesc.Width = 640; //缓冲区大小
scDesc.BufferDesc.Height = 480;
scDesc.BufferDesc.RefreshRate.Numerator = 60; //刷新率,一般这样设定即可
scDesc.BufferDesc.RefreshRate.Denominator = 1;
scDesc.BufferDesc.Scaling = DXGI_MODE_SCALING_UNSPECIFIED; //固定参数
scDesc.BufferDesc.ScanlineOrdering = DXGI_MODE_SCANLINE_ORDER_UNSPECIFIED; //固定参数
scDesc.BufferCount = 1; //缓冲区个数
scDesc.BufferUsage = DXGI_USAGE_RENDER_TARGET_OUTPUT; //Usage为Render Target Output
scDesc.Flags = 0;
scDesc.OutputWindow = m_hWnd; //主窗口句柄
scDesc.SampleDesc.Count = 4; //4个采样点
scDesc.SampleDesc.Quality = x4MsaaQuality-1; //4重采样支持等级
scDesc.SwapEffect = DXGI_SWAP_EFFECT_DISCARD; //常用参数
scDesc.Windowed = true; //窗口模式
//通过如下三步获得接口IDXGIFactory,来创建交换链
IDXGIDevice *pDxgiDevice(NULL);
m_d3dDevice->QueryInterface(__uuidof(IDXGIDevice),reinterpret_cast<void**>(&pDxgiDevice));
IDXGIAdapter *pDxgiAdapter(NULL);
pDxgiDevice->GetParent(__uuidof(IDXGIAdapter),reinterpret_cast<void**>(&pDxgiAdapter));
IDXGIFactory *pDxgiFactory(NULL);
pDxgiAdapter->GetParent(__uuidof(IDXGIFactory),reinterpret_cast<void**>(&pDxgiFactory));
pDxgiFactory->CreateSwapChain(d3dDevice,&scDesc,&swapChain);
//释放接口
SafeRelease(pDxgiFactory);
SafeRelease(pDxgiAdapter);
SafeRelease(pDxgiDevice);
3. 创建RenderTargetView
D3D11中创建视图需要对应的资源,这里先获取后缓冲区地址,作为参数创建相应的视图:
ID3D11Texture2D *backBuffer(NULL);
//获取后缓冲区地址
swapChain->GetBuffer(0,__uuidof(ID3D11Texture2D),reinterpret_cast<void**>(&backBuffer));
//创建视图
d3dDevice->CreateRenderTargetView(backBuffer,0,&m_renderTargetView);
//释放后缓冲区引用
backBuffer->Release();
CreateRenderTargetView函数原型如下:
HRESULT CreateRenderTargetView(
ID3D11Resource *pResource, //视图对应资源
const D3D11_RENDER_TARGET_VIEW_DESC *pDesc, //视图描述
ID3D11RenderTargetView **ppRTView //要创建的视图(指针的地址)
);
d3d11中创建视图时需要用到视图的描述(第二个参数),但如果视图对应的资源类型已知,则一般不再需要描述,设为NULL。这里后缓冲区在创建时类型已给出(DXGI_FORMAT_R8G8B8A8_UNORM),因此这里不需要视图描述。资源创建时也可以指定为无类型(TYPELESS),在不同阶段以不同的类型进行解释,这时创建视图时就需要视图描述明确说明类型。
4. 创建深度、模板缓冲区及对应视图
创建缓冲区要即创建一个2维纹理,ID3D11Texture2D,创建它需要先给出描述D3D11_TEXTURE2D_DESC。定义如下:
typedef struct D3D11_TEXTURE2D_DESC {
UINT Width;
UINT Height;
UINT MipLevels; //这里不需要mipmap,设为1
UINT ArraySize; //纹理数组才用,这里为1
DXGI_FORMAT Format; //数据格式,一般为DXGI_FORMAT_D24_UNORM_S8_UINT,24位用于深度,8位用于模板
DXGI_SAMPLE_DESC SampleDesc; //多重采样,如前,前后务必保持一致!
D3D11_USAGE Usage; //Usage,对于只让GPU读、写,应为D3D11_USAGE_DEFAULT
UINT BindFlags; //绑定类型,为D3D11_BIND_DEPTH_STENCIL
UINT CPUAccessFlags; //CPU不可访问,设为0
UINT MiscFlags; //设为0
} D3D11_TEXTURE2D_DESC;
创建视图类型RenderTargetView,代码如下:
D3D11_TEXTURE2D_DESC dsDesc;
dsDesc.Format = DXGI_FORMAT_D24_UNORM_S8_UINT;
dsDesc.Width = 640;
dsDesc.Height = 480;
dsDesc.BindFlags = D3D11_BIND_DEPTH_STENCIL;
dsDesc.MipLevels = 1;
dsDesc.ArraySize = 1;
dsDesc.CPUAccessFlags = 0;
dsDesc.SampleDesc.Count = 4;
dsDesc.SampleDesc.Quality = x4MsaaQuality-1;
dsDesc.MiscFlags = 0;
dsDesc.Usage = D3D11_USAGE_DEFAULT;
d3dDevice->CreateTexture2D(&dsDesc,0,&depthStencilBuffer);
d3dDevice->CreateDepthStencilView(depthStencilBuffer,0,&depthStencilView);
创建完两个视图后当然就要绑定到渲染管线上去啦:
deviceContext->OMSetRenderTargets(1,&renderTargetView,depthStencilView);
该函数原型如下:
void OMSetRenderTargets(
[in] UINT NumViews, //RenderTarget个数,我们一般只用一个
[in] ID3D10RenderTargetView *const *ppRenderTargetViews, //RenderTarget数组,只一个,所以直接传其地址即可
[in] ID3D10DepthStencilView *pDepthStencilView //DepthStencil view
);
最后设置viewport,很简单,D3D11_VIEWPORT定义如下:
typedef struct D3D11_VIEWPORT {
FLOAT TopLeftX; //视口左上角在屏幕上x坐标,一般视口占满屏幕的,所以为0
FLOAT TopLeftY; //y坐标,同上
FLOAT Width; //视口宽度,一般与后缓冲区一致,以保持图像不变形
FLOAT Height; //高度,同上
FLOAT MinDepth; //最小深度值:0.0f
FLOAT MaxDepth; //最大深度值:1.0f
} D3D11_VIEWPORT;
设置视口代码:
D3D11_VIEWPORT viewPort;
viewPort.Width = static_cast<FLOAT>(clientWidth);
viewPort.Height = static_cast<FLOAT>(clientHeight);
viewPort.MaxDepth = 1.f;
viewPort.MinDepth = 0.f;
viewPort.TopLeftX = 0.f;
viewPort.TopLeftY = 0.f;
deviceContext->RSSetViewports(1,&viewPort);
Voila,初始化过程全部完成!
下面就可以进入主循环渲染函数绘制场景了,这次我们暂时什么也不画,只是简单地把屏幕清空为随意指定的颜色:
XMVECTORF32 color = {0.f, 1.f, 0.f, 1.6f};
g_deviceContext->ClearRenderTargetView(g_renderTargetView,reinterpret_cast<float*>(&color));
g_deviceContext->ClearDepthStencilView(g_depthStencilView,D3D11_CLEAR_DEPTH|D3D11_CLEAR_STENCIL,1.f,0);
g_swapChain->Present(0,0);
每帧开始渲染时,先清空RenderTargetView和深度缓冲区,然后就是全屏渲染内容了。绘制完整个场景后,最后调用让交换链Present来交换前后缓冲区,以显示新的一帧。清空RenderTarget很简单,把相应视图传进来,并给定任意背景颜色(4个float的数组,这里适合用XMVECTORF32)即可。对于清空深度、模板缓冲区,第一个参数一目了然,第二个用来指定清空的内容参数,一般我们要同时清空深度和模板,因此用两个枚举变量求并,最后两个参数分别指定清空深度和模板的默认值,深度清空为1.0f(最大),模板一般清空为0,就这样。
整个过程就结束啦,配合上次中的Win32程序,现在已经是一个完整的、可工作的d3d程序了,显示的即为一个空白的指定颜色的窗口。初始化程序就是这样,一个最简单的d3d应用程序。以后慢慢开始渲染更加炫丽的场景~
这次完整的初始化程序源代码如下: