本部分主要记录下使用D3D入门所涉及到的API,大致流程与vulkan类似(相对于vulkan来说您应该能够更好的理解和使用DX),应该说是所有图形API整体流程大致类似吧,废话不多说,直接记录供后期学习使用(数据参照龙书实现)。
一、Direct3D初始化
Direct3D初始化阶段首先需要创建D3D设备和D3D设备上下文
- D3D设备(ID3D11Device) 通常代表一个显示适配器(即显卡),它最主要的功能是用于创建各种所需资源,最常用的资源有:资源类(ID3D11Resource, 包含纹理和缓冲区),视图类以及着色器。此外,D3D设备还能够用于检测系统环境对功能的支持情况。
- D3D设备上下文(ID3D11DeviceContext) 可以看做是一个渲染管线。通常我们在创建D3D设备的同时也会附赠一个立即设备上下文(Immediate Context)。一个D3D设备仅对应一个D3D立即设备上下文,并且只要我们拥有其中一方,就能通过各自的方法获取另一方(即ID3D11Device::GetImmediateContext 和 ID3D11DeviceContext::GetDevice)。渲染管线主要负责渲染和计算工作,它需要绑定来自与它关联的D3D设备所创建的各种资源、视图和着色器才能正常运转,除此之外,它还能够负责对资源的直接读写操作。
如果你的系统支持Direct3D 11.1的话,则对应的接口类为:ID3D11Device1、ID3D11DeviceContext1,它们分别继承自上面的两个接口类,区别在于额外提供了少数新的接口,并且接口方法的实现可能会有所区别。
1.1 D3D设备与D3D设备上下文的创建
创建D3D设备、D3D设备上下文使用如下函数:
HRESULT WINAPI D3D11CreateDevice(
IDXGIAdapter* pAdapter, // [In_Opt]适配器
D3D_DRIVER_TYPE DriverType, // [In]驱动类型
HMODULE Software, // [In_Opt]若上面为D3D_DRIVER_TYPE_SOFTWARE则这里需要提供程序模块
UINT Flags, // [In]使用D3D11_CREATE_DEVICE_FLAG枚举类型
D3D_FEATURE_LEVEL* pFeatureLevels, // [In_Opt]若为nullptr则为默认特性等级,否则需要提供特性等级数组
UINT FeatureLevels, // [In]特性等级数组的元素数目
UINT SDKVersion, // [In]SDK版本,默认D3D11_SDK_VERSION
ID3D11Device** ppDevice, // [Out_Opt]输出D3D设备
D3D_FEATURE_LEVEL* pFeatureLevel, // [Out_Opt]输出当前应用D3D特性等级
ID3D11DeviceContext** ppImmediateContext ); //[Out_Opt]输出D3D设备上下文
1.2 DXGI交换链
DXGI交换链(IDXGISwapChain) 缓存了一个或多个表面(2D纹理),它们都可以称作后备缓冲区(backbuffer)。后备缓冲区则是我们主要进行渲染的场所,我们可以将这些缓冲区通过合适的手段成为渲染管线的输出对象。
DXGI交换链的创建需要通过IDXGIFactory::CreateSwapChain方法进行。
HRESULT IDXGIFactory::CreateSwapChain(
IUnknown *pDevice, // [In]D3D设备
DXGI_SWAP_CHAIN_DESC *pDesc, // [In]交换链描述
IDXGISwapChain **ppSwapChain); // [Out]输出交换链对象
1.3 DXGI交换链与Direct3D设备的交互
在创建好上述对象后,如果窗口的大小是固定的,则需要经历下面的步骤:
- 获取交换链后备缓冲区的ID3D11Texture2D接口对象
- 为后备缓冲区创建渲染目标视图ID3D11RenderTargetView
- 通过D3D设备创建一个ID3D11Texture2D用作深度/模板缓冲区,要求与后备缓冲区等宽高
- 创建深度/模板视图ID3D11DepthStrenilView,绑定刚才创建的2D纹理
- 通过D3D设备上下文,在渲染管线的输出合并阶段设置渲染目标
- 在渲染管线的光栅化阶段设置好渲染的视口区域
接下来需要快速了解一遍上述步骤所需要用到的API。
1.3.1 获取交换链的后备缓冲区
由于此前我们创建好的交换链已经包含1个后备缓冲区了,我们可以通过IDXGISwapChain::GetBuffer方法直接获取后备缓冲区的ID3D11Texture2D接口:
HRESULT IDXGISwapChain::GetBuffer(
UINT Buffer, // [In]缓冲区索引号,从0到BufferCount - 1
REFIID riid, // [In]缓冲区的接口类型ID
void **ppSurface); // [Out]获取到的缓冲区
1.3.2 为后备缓冲区创建渲染目标视图
渲染目标视图用于将渲染管线的运行结果输出给其绑定的资源,很明显它也只能够设置给输出合并阶段。渲染目标视图要求其绑定的资源是允许GPU读写的,因为在作为管线输出时会通过GPU写入数据,并且在以后进行混合操作时还需要在GPU读取该资源。通常渲染目标是一个二维的纹理,但它依旧可能会绑定其余类型的资源。这里不做讨论。
现在我们需要将后备缓冲区绑定到渲染目标视图,使用ID3D11Device::CreateRenderTargetView方法来创建:
HRESULT ID3D11Device::CreateRenderTargetView(
ID3D11Resource *pResource, // [In]待绑定到渲染目标视图的资源
const D3D11_RENDER_TARGET_VIEW_DESC *pDesc, // [In]忽略
ID3D11RenderTargetView **ppRTView); // [Out]获取渲染目标视图
1.3.3 创建深度/模板缓冲区
除了渲染目标视图外,我们还需要创建深度/模板缓冲区用于深度测试。深度/模板缓冲区也是一个2D纹理,要求其宽度和高度必须要和窗口宽高保持一致。
这时我们就可以用方法ID3D11Device::CreateTexture2D来创建2D纹理:
HRESULT ID3D11Device::CreateTexture2D(
const D3D11_TEXTURE2D_DESC *pDesc, // [In] 2D纹理描述信息
const D3D11_SUBRESOURCE_DATA *pInitialData, // [In] 用于初始化的资源
ID3D11Texture2D **ppTexture2D); // [Out] 获取到的2D纹理
1.3.4 创建深度/模板视图
有了深度/模板缓冲区后,就可以通过ID3D11Device::CreateDepthStencilView方法将创建好的2D纹理绑定到新建的深度/模板视图:
HRESULT ID3D11Device::CreateDepthStencilView(
ID3D11Resource *pResource, // [In] 需要绑定的资源
const D3D11_DEPTH_STENCIL_VIEW_DESC *pDesc, // [In] 深度缓冲区描述,这里忽略
ID3D11DepthStencilView **ppDepthStencilView); // [Out] 获取到的深度/模板视图
1.3.5 为渲染管线的输出合并阶段设置渲染目标
ID3D11DeviceContext::OMSetRenderTargets方法要求同时提供渲染目标视图和深度/模板视图,不过这时我们都已经准备好了:
void ID3D11DeviceContext::OMSetRenderTargets(
UINT NumViews, // [In] 视图数目
ID3D11RenderTargetView *const *ppRenderTargetViews, // [In] 渲染目标视图数组
ID3D11DepthStencilView *pDepthStencilView) = 0; // [In] 深度/模板视图
1.3.6 视口设置
最终我们还需要决定将整个视图输出到窗口特定的范围。我们需要使用D3D11_VIEWPORT来设置视口。
ID3D11DeviceContext::RSSetViewports方法将设置1个或多个视口:
void ID3D11DeviceContext::RSSetViewports(
UINT NumViewports, // 视口数目
const D3D11_VIEWPORT *pViewports); // 视口数组
完成了这六个步骤后,基本的初始化就完成了。但是,如果涉及到窗口大小变化的情况,那么前面提到的后备缓冲区、深度/模板缓冲区、视口都需要重新调整大小。
二、顶点/像素着色器的创建、顶点缓冲区
下图展示了DirectX11的渲染管线完整流程:
2.1 HLSL代码
// Triangle.hlsli
struct VertexIn
{
float3 pos : POSITION;
float4 color : COLOR;
};
struct VertexOut
{
float4 posH : SV_POSITION;
float4 color : COLOR;
};
Triangle_VS.hlsl文件,用于存放顶点着色器代码:
#include "Triangle.hlsli"
// 顶点着色器
VertexOut VS(VertexIn vIn)
{
VertexOut vOut;
vOut.posH = float4(vIn.pos, 1.0f);
vOut.color = vIn.color; // 这里alpha通道的值默认为1.0
return vOut;
}
Triangle_PS.hlsl文件,用于存放像素着色器代码:
#include "Triangle.hlsli"
// 像素着色器
float4 PS(VertexOut pIn) : SV_Target
{
return pIn.color;
}
然后具体讲述一下变量名后面的语义:
2.2 输入布局(Input Layout)
2.2.1 ID3D11Device::CreateInputLayout方法–创建输入布局:
在HLSL中,用于输入的结构体为:
struct VertexIn
{
float3 pos : POSITION;
float4 color : COLOR;
};
该项目与之对应的C++结构体为:
struct VertexPosColor
{
DirectX::XMFLOAT3 pos;
DirectX::XMFLOAT4 color;
static const D3D11_INPUT_ELEMENT_DESC inputLayout[2];
};
由于顶点缓冲区的本质是二进制流,为了能够建立C++结构体与HLSL结构体的对应关系,需要使用ID3D11InputLayout输入布局来描述每一个成员的用途、语义、大小等信息。
还要留意的是,其中inputLayout并不是结构体VertexPosColor的内部成员,而是静态成员,不占用该结构体的空间。我们使用D3D11_INPUT_ELEMENT_DESC结构体来描述待传入结构体中每个成员的具体信息,定义如下:
typedef struct D3D11_INPUT_ELEMENT_DESC
{
LPCSTR SemanticName; // 语义名
UINT SemanticIndex; // 语义索引
DXGI_FORMAT Format; // 数据格式
UINT InputSlot; // 输入槽索引(0-15)
UINT AlignedByteOffset; // 初始位置(字节偏移量)
D3D11_INPUT_CLASSIFICATION InputSlotClass; // 输入类型
UINT InstanceDataStepRate; // 忽略
} D3D11_INPUT_ELEMENT_DESC;
inputLayout的初始化信息如下,描述了C++对应到HLSL的两个成员的信息:
const D3D11_INPUT_ELEMENT_DESC VertexPosColor::inputLayout[2] = {
{ "POSITION", 0, DXGI_FORMAT_R32G32B32_FLOAT, 0, 0, D3D11_INPUT_PER_VERTEX_DATA, 0 },
{ "COLOR", 0, DXGI_FORMAT_R32G32B32A32_FLOAT, 0, 12, D3D11_INPUT_PER_VERTEX_DATA, 0}
};
其中,语义名要与HLSL结构体中的语义名相同,若有多个相同的语义名,则语义索引就是另外一种区分。相同的语义按从上到下所以分别为0,1,2…
然后,DXGI_FORMAT在这里通常描述数据的存储方式、大小。用DXGI_FORMAT_R32G32B32_FLOAT仅仅是解释为3个float类型的值;而用DXGI_FORMAT_R32G32B32A32_FLOAT在这里是说明颜色按RGBA存储,并且为4个float类型的值
输入槽这里只使用1个,即索引为0的输入槽。
初始位置则指的是该成员的位置与起始成员所在的字节偏移量。
输入类型有两种:D3D11_INPUT_PER_VERTEX_DATA为按每个顶点数据输入,D3D11_INPUT_PER_INSTANCE_DATA则是按每个实例数据输入。
通过语义、数据类型和起始偏移量,我们就可以建立起C++顶点缓冲区数据和HLSL顶点之间的联系。
接下来就可以使用ID3D11Device::CreateInputLayout方法创建一个输入布局:
HRESULT ID3D11Device::CreateInputLayout(
const D3D11_INPUT_ELEMENT_DESC *pInputElementDescs, // [In]输入布局描述
UINT NumElements, // [In]上述数组元素个数
const void *pShaderBytecodeWithInputSignature, // [In]顶点着色器字节码
SIZE_T BytecodeLength, // [In]顶点着色器字节码长度
ID3D11InputLayout **ppInputLayout); // [Out]获取的输入布局
2.2.2 ID3D11DeviceContext::IASetInputLayout方法–输入装配阶段设置输入布局
下面的方法可以让我们使用刚创建好的输入布局:
void ID3D11DeviceContext::IASetInputLayout(
ID3D11InputLayout *pInputLayout); // [In]输入布局
2.3 顶点/像素着色器的创建
2.3.1 ID3D11Device::CraeteXXXXShader方法–创建着色器
从D3D设备可以创建出6种着色器:
这些方法的输入形参都是一致的,只是输出的是不同的着色器,以创建顶点着色器的方法为例:
HRESULT ID3D11Device::CreateVertexShader(
const void *pShaderBytecode, // [In]着色器字节码
SIZE_T BytecodeLength, // [In]字节码长度
ID3D11ClassLinkage *pClassLinkage, // [In_Opt]忽略
ID3D11VertexShader **ppVertexShader); // [Out]获取顶点着色器
2.3.2 顶点缓冲区(Vertex Buffer)
顶点缓冲区的作用是,将顶点数组以缓冲区ID3D11Buffer的形式提供给输入装配阶段。
- ID3D11Device::CreateBuffer方法–创建一个缓冲区
要创建顶点缓冲区,首先需要填充好缓冲区描述D3D11_BUFFER_DESC:
typedef struct D3D11_BUFFER_DESC
{
UINT ByteWidth; // 数据字节数
D3D11_USAGE Usage; // CPU和GPU的读写权限相关
UINT BindFlags; // 缓冲区类型的标志
UINT CPUAccessFlags; // CPU读写权限的指定
UINT MiscFlags; // 忽略
UINT StructureByteStride; // 忽略
} D3D11_BUFFER_DESC;
在这里需要详细讲述一下D3D11_USAGE枚举类型对应的读写关系:
对于D3D11_USAGE_DEFAULT类型的缓冲区,应当使用 ID3D11DeviceContext::UpdateSubresource方法来更新缓冲区资源,它的原理是将内存中的某段数据传递到显存中,然后再将该显存中的数据复制到在显存中的缓冲区。这种更新方式我们是无法直接访问缓冲区的内容的。在绘制完成/开始前调用可以比较快地更新显存中的数据。
而对于D3D11_USAGE_DYNAMIC类型的缓冲区,则应当使用ID3D11DeviceContext::Map和ID3D11DeviceContext::Unmap方法,将显存中的数据映射到内存中,然后修改该片内存的数据,最后将修改好的数据映射回显存中。这种更新方式我们是可以直接获取来自显存的数据的,但代价就是更新的效率会比上面的方式更低一些。
有了缓冲区描述,还需要使用D3D11_SUBRESOURCE_DATA结构体来指定要用来初始化的数据:
typedef struct D3D11_SUBRESOURCE_DATA
{
const void *pSysMem; // 用于初始化的数据
UINT SysMemPitch; // 忽略
UINT SysMemSlicePitch; // 忽略
} D3D11_SUBRESOURCE_DATA;
最后通过ID3D11Device::CreateBuffer来创建一个顶点缓冲区:
HRESULT ID3D11Device::CreateBuffer(
const D3D11_BUFFER_DESC *pDesc, // [In]顶点缓冲区描述
const D3D11_SUBRESOURCE_DATA *pInitialData, // [In]子资源数据
ID3D11Buffer **ppBuffer); // [Out] 获取缓冲区
- ID3D11DeviceContext::IASetVertexBuffers方法–渲染管线输入装配阶段设置顶点缓冲区
创建好顶点缓冲区后,就可以在渲染管线输入装配阶段设置该顶点缓冲区了
void ID3D11DeviceContext::IASetVertexBuffers(
UINT StartSlot, // [In]输入槽索引
UINT NumBuffers, // [In]缓冲区数目
ID3D11Buffer *const *ppVertexBuffers, // [In]指向缓冲区数组的指针
const UINT *pStrides, // [In]一个数组,规定了对所有缓冲区每次读取的字节数分别是多少
const UINT *pOffsets); // [In]一个数组,规定了对所有缓冲区的初始字节偏移量
只要绘制的内容不变,该部分的设置则只需要进行一次即可,因为渲染管线中各个部分的设置方法一经调用就会立即生效。然而如果需要绘制不同的内容或者效果,则需要在绘制前给渲染管线绑定好各种所需的资源。
- ID3D11DeviceContext::IASetPrimitiveTopology方法–渲染管线输入装配阶段设置图元类型
void ID3D11DeviceContext::IASetPrimitiveTopology(
D3D11_PRIMITIVE_TOPOLOGY Topology); // [In]图元类型
- ** ID3D11DeviceContext::#SSetShader方法–给渲染管线某一着色阶段设置对应的着色器 **
这里的#可以是V, H, D, C, G, P,对应六个可编程渲染管线阶段,除了第一个参数提供的着色器类型不同外,其余参数一致。
以ID3D11DeviceContext::VSSetShader为例:
void ID3D11DeviceContext::VSSetShader(
ID3D11VertexShader *pVertexShader, // [In]顶点着色器
ID3D11ClassInstance *const *ppClassInstances, // [In_Opt]忽略
UINT NumClassInstances); // [In]忽略
- 注意: 类似给渲染管线绑定资源的一切方法,在绑定之后就会一直生效,而不是说仅能够使用一次。所以,以后如果你需要用别的特效去绘制当前物体,就要重新绑定好渲染管线所需要的一切资源。
2.4 绘制
- ID3D11DeviceContext::Draw方法–根据已经绑定的顶点缓冲区进行绘制
该方法不需要提供索引缓冲区:
void ID3D11DeviceContext::Draw(
UINT VertexCount, // [In]需要绘制的顶点数目
UINT StartVertexLocation); // [In]起始顶点索引
调用该方法后,从输入装配阶段开始,该绘制的进行将会经历一次完整的渲染管线阶段,直到输出合并阶段为止。
通过指定VertexCount和StartVertexLocation的值我们可以按顺序绘制从索引StartVertexLocation到StartVertexLocation + VertexCount - 1的顶点
三、索引缓冲区、常量缓冲区
3.1 索引缓冲区(Index Buffer)
然后顶点缓冲区的创建和使用和之前一样(贴出一套代码温习下):
顶点缓冲区创建与使用:
// ******************
// 设置立方体顶点
// 5________ 6
// /| /|
// /_|_____/ |
// 1|4|_ _ 2|_|7
// | / | /
// |/______|/
// 0 3
VertexPosColor vertices[] =
{
{ XMFLOAT3(-1.0f, -1.0f, -1.0f), XMFLOAT4(0.0f, 0.0f, 0.0f, 1.0f) },
{ XMFLOAT3(-1.0f, 1.0f, -1.0f), XMFLOAT4(1.0f, 0.0f, 0.0f, 1.0f) },
{ XMFLOAT3(1.0f, 1.0f, -1.0f), XMFLOAT4(1.0f, 1.0f, 0.0f, 1.0f) },
{ XMFLOAT3(1.0f, -1.0f, -1.0f), XMFLOAT4(0.0f, 1.0f, 0.0f, 1.0f) },
{ XMFLOAT3(-1.0f, -1.0f, 1.0f), XMFLOAT4(0.0f, 0.0f, 1.0f, 1.0f) },
{ XMFLOAT3(-1.0f, 1.0f, 1.0f), XMFLOAT4(1.0f, 0.0f, 1.0f, 1.0f) },
{ XMFLOAT3(1.0f, 1.0f, 1.0f), XMFLOAT4(1.0f, 1.0f, 1.0f, 1.0f) },
{ XMFLOAT3(1.0f, -1.0f, 1.0f), XMFLOAT4(0.0f, 1.0f, 1.0f, 1.0f) }
};
// 设置顶点缓冲区描述
D3D11_BUFFER_DESC vbd;
ZeroMemory(&vbd, sizeof(vbd));
vbd.Usage = D3D11_USAGE_IMMUTABLE;
vbd.ByteWidth = sizeof vertices;
vbd.BindFlags = D3D11_BIND_VERTEX_BUFFER;
vbd.CPUAccessFlags = 0;
// 新建顶点缓冲区
D3D11_SUBRESOURCE_DATA InitData;
ZeroMemory(&InitData, sizeof(InitData));
InitData.pSysMem = vertices;
HR(m_pd3dDevice->CreateBuffer(&vbd, &InitData, m_pVertexBuffer.GetAddressOf()));
// 输入装配阶段的顶点缓冲区设置
UINT stride = sizeof(VertexPosColor); // 跨越字节数
UINT offset = 0; // 起始偏移量
m_pd3dImmediateContext->IASetVertexBuffers(0, 1, m_pVertexBuffer.GetAddressOf(), &stride, &offset);
索引缓冲区创建与使用:
先索引数组的初始化如下:
// 索引数组
WORD indices[] = {
// 正面
0, 1, 2,
2, 3, 0,
// 左面
4, 5, 1,
1, 0, 4,
// 顶面
1, 5, 6,
6, 2, 1,
// 背面
7, 6, 5,
5, 4, 7,
// 右面
3, 2, 6,
6, 7, 3,
// 底面
4, 0, 3,
3, 7, 4
};
// 设置索引缓冲区描述
D3D11_BUFFER_DESC ibd;
ZeroMemory(&ibd, sizeof(ibd));
ibd.Usage = D3D11_USAGE_IMMUTABLE;
ibd.ByteWidth = sizeof indices;
ibd.BindFlags = D3D11_BIND_INDEX_BUFFER;
ibd.CPUAccessFlags = 0;
// 新建索引缓冲区
InitData.pSysMem = indices;
HR(m_pd3dDevice->CreateBuffer(&ibd, &InitData, m_pIndexBuffer.GetAddressOf()));
// 输入装配阶段的索引缓冲区设置
m_pd3dImmediateContext->IASetIndexBuffer(m_pIndexBuffer.Get(), DXGI_FORMAT_R32_UINT, 0);
其中 ID3D11DeviceContext::IASetIndexBuffer 方法–渲染管线输入装配阶段设置索引缓冲区
void ID3D11DeviceContext::IASetIndexBuffer(
ID3D11Buffer *pIndexBuffer, // [In]索引缓冲区
DXGI_FORMAT Format, // [In]数据格式
UINT Offset); // [In]字节偏移量
在装配的时候你需要指定每个索引所占的字节数:
3.2 索引绘制
ID3D11DeviceContext::DrawIndexed方法–根据顶点和索引缓冲区进行绘制
在输入装配阶段指定好了顶点缓冲区、索引缓冲区和原始拓补类型后,再绑定常量缓冲区到顶点着色阶段,最后就可以使用ID3D11DeviceContext::DrawIndexed方法来绘制:
void ID3D11DeviceContext::DrawIndexed(
UINT IndexCount, // 索引数目
UINT StartIndexLocation, // 起始索引位置
INT BaseVertexLocation); // 起始顶点位置
eg. 如果按下述方式调用:
m_pd3dImmediateContext->DrawIndexed(6, 6, 4);
假设顶点缓冲区有12个顶点,索引缓冲区有12个索引,存放的值为{11, 10, 9, 8, 7, 6, 5, 4, 3, 2, 1, 0},
上面的调用意味着顶点缓冲区我们从索引4作为我们的基准索引,然后索引缓冲区则是使用了索引6到11对应的索引值,即{5, 4, 3, 2, 1, 0},然后加上基准索引值4为最终取得的原来顶点缓冲区索引为{9, 8, 7, 6, 5, 4}的顶点。
3.3 常量缓冲区(Constant Buffer)
在HLSL中,常量缓冲区的变量类似于C++这边的全局常量,供着色器代码使用。下面是一个HLSL常量缓冲区示例:
cbuffer ConstantBuffer : register(b0)
{
matrix g_World;
matrix g_View;
matrix g_Proj;
}
cbuffer 用于声明一个常量缓冲区
matrix 等价于 float4x4,同样有vector等价于float4.其中D3D中的矩阵默认是行主矩阵形式,但是到了HLSL的matrix默认是列主矩阵形式。
register(b0) 指的是该常量缓冲区位于寄存器索引为0的缓冲区
而在C++应用层,常量缓冲区的对应结构体可以为:
struct ConstantBuffer
{
XMMATRIX world;
XMMATRIX view;
XMMATRIX proj;
};
目前常量缓冲区有两种运行时更新方式:
- 在创建资源的时候指定Usage为D3D11_USAGE_DEFAULT,可以允许常量缓冲区从GPU写入,需要用ID3D11DeviceContext::UpdateSubresource方法更新。
- 在创建资源的时候指定Usage为D3D11_USAGE_DYNAMIC、CPUAccessFlags为D3D11_CPU_ACCESS_WRITE,允许常量缓冲区从CPU写入,首先通过ID3D11DeviceContext::Map方法获取内存映射,然后再更新到映射好的内存区域,最后通过ID3D11DeviceContext::Unmap方法解除占用。
不仅常量缓冲区,一般的缓冲区和纹理资源更新都可以使用上述两种方式。前者适合更新不频繁(隔一段时间更新),或者仅一次更新的数据。而后者更适合于需要频繁更新,如每几帧更新一次,或每帧更新一次或多次的资源。
tips:在创建常量缓冲区时,描述参数ByteWidth必须为16的倍数,因为HLSL的常量缓冲区本身以及对它的读写操作需要严格按16字节对齐。
现在来了解更新缓冲区的两种方法所需要用到的函数。
3.3.1 DYNAMIC更新
- ID3D11DeviceContext::Map[1]函数–获取指向缓冲区中数据的指针并拒绝GPU对该缓冲区的访问
HRESULT ID3D11DeviceContext::Map(
ID3D11Resource *pResource, // [In]包含ID3D11Resource接口的资源对象
UINT Subresource, // [In]缓冲区资源填0
D3D11_MAP MapType, // [In]D3D11_MAP枚举值,指定读写相关操作
UINT MapFlags, // [In]填0,CPU需要等待GPU使用完毕当前缓冲区
D3D11_MAPPED_SUBRESOURCE *pMappedResource // [Out]获取到的已经映射到缓冲区的内存
);
D3D11_MAP枚举值类型的成员如下:
最后映射出来的内存我们可以通过memcpy_s函数来更新。
默认情况下,若待访问资源仍在被GPU使用,CPU会阻塞直到能够访问该资源。
- 注意:千万不要对只支持写操作的映射内存区域进行读取操作!否则这会招致十分显著的性能损失。即便是像这样的C++代码:((int)MappedResource.pData)
= 0都会引发读操作从而触发性能损失。这在x86汇编表示为AND DWORD PTR [EAX], 0。
- ID3D11DeviceContext::Unmap函数–让指向资源的指针无效并重新启用GPU对该资源的访问权限
void ID3D11DeviceContext::Unmap(
ID3D11Resource *pResource, // [In]包含ID3D11Resource接口的资源对象
UINT Subresource // [In]缓冲区资源填0
);
现在需要利用mBuffer结构体变量用于更新常量缓冲区,其中view和proj矩阵需要预先进行一次转置以抵消HLSL列主矩阵的转置,至于world矩阵已经是单位矩阵就不需要了:
eg.
m_CBuffer.world = XMMatrixIdentity(); // 单位矩阵的转置是它本身
m_CBuffer.view = XMMatrixTranspose(XMMatrixLookAtLH(
XMVectorSet(0.0f, 0.0f, -5.0f, 0.0f),
XMVectorSet(0.0f, 0.0f, 0.0f, 0.0f),
XMVectorSet(0.0f, 1.0f, 0.0f, 0.0f)
));
m_CBuffer.proj = XMMatrixTranspose(XMMatrixPerspectiveFovLH(XM_PIDIV2, AspectRatio(), 1.0f, 1000.0f));
D3D11_MAPPED_SUBRESOURCE mappedData;
HR(m_pd3dImmediateContext->Map(m_pConstantBuffer.Get(), 0, D3D11_MAP_WRITE_DISCARD, 0, &mappedData));
memcpy_s(mappedData.pData, sizeof(m_CBuffer), &m_CBuffer, sizeof(m_CBuffer));
m_pd3dImmediateContext->Unmap(m_pConstantBuffer.Get(), 0);
3.3.2 DEFAULT更新
该方法可以用于更新允许GPU写入的资源:
void ID3D11DeviceContext::UpdateSubresource(
ID3D11Resource *pDstResource, // [In]需要更新的常量缓冲区
UINT DstSubresource, // [In]缓冲区资源填0
const D3D11_BOX *pDstBox, // [In]忽略,填nullptr
const void *pSrcData, // [In]用于更新的数据源
UINT SrcRowPitch, // [In]忽略,填0
UINT SrcDepthPitch); // [In]忽略,填0
该方法仅可以用于以D3D11_USAGE_DEFAULT或D3D11_USAGE_STAGE方式创建的资源,并且不能用于深度模板缓冲区和支持多采样的缓冲区。
ID3D11DeviceContext::UpdateSubresource的性能表现取决于是否出现与待更新缓冲区的资源竞争。例如,GPU正在执行绘制操作时占用了该缓冲区,然后CPU发出了对同一个缓冲区的UpdateSubresource操作。
- 当出现资源竞争时,UpdateSubresource会对源数据进行2次拷贝。第一次是CPU拷贝一份资源在临时的内存空间让GPU命令缓冲能够访问它,发生在该方法被返回之前。然后第二次由GPU从内存拷贝到不可映射的显存区域。第二次拷贝通常是异步发生的,因为这是在GPU命令缓冲被刷新后执行的。
- 若没有出现资源竞争,UpdateSubresource的行为取决于CPU认为怎样会更快:像第一步那样执行,或者直接从CPU拷贝到最终的显存资源位置。具体行为还是要依赖于系统。
该方法本身涉及到CPU的拷贝操作,CPU运行开销会比较大一些,而且还需要留有足够的显存空间。
tip:如果用于更新着色器的常量缓冲区,不能对其中的数据部分更新,必须完整地进行数据的更新。
eg. 使用该方法只需要一句代码就可以更新:
m_CBuffer.world = XMMatrixIdentity(); // 单位矩阵的转置是它本身
m_CBuffer.view = XMMatrixTranspose(XMMatrixLookAtLH(
XMVectorSet(0.0f, 0.0f, -5.0f, 0.0f),
XMVectorSet(0.0f, 0.0f, 0.0f, 0.0f),
XMVectorSet(0.0f, 1.0f, 0.0f, 0.0f)
));
m_CBuffer.proj = XMMatrixTranspose(XMMatrixPerspectiveFovLH(XM_PIDIV2, AspectRatio(), 1.0f, 1000.0f));
m_pd3dImmediateContext->UpdateSubresource(m_pConstantBuffer.Get(), 0, nullptr, &m_CBuffer, 0, 0);
3.3.3 设置常量缓冲区
ID3D11DeviceContext::#SSetConstantBuffers方法–渲染管线某一着色阶段设置常量缓冲区
这里的#可以是V, H, D, C, G, P六种可编程渲染管线阶段,函数的形参都基本一致。
现在更新了数据后,我们还需要给顶点着色阶段设置常量缓冲区供使用。
void ID3D11DeviceContext::VSSetConstantBuffers(
UINT StartSlot, // [In]放入缓冲区的起始索引,例如上面指定了b0,则这里应为0
UINT NumBuffers, // [In]设置的缓冲区数目
ID3D11Buffer *const *ppConstantBuffers); // [In]用于设置的缓冲区数组
eg.绑定常量缓冲区的操作通常只需要调用一次即可:
m_pd3dImmediateContext->VSSetConstantBuffers(0, 1, m_pConstantBuffer.GetAddressOf());
3.4 HLSL代码
//Cube.hlsli
cbuffer ConstantBuffer : register(b0)
{
matrix World;
matrix View;
matrix Proj;
}
struct VertexIn
{
float3 posL : POSITION;
float4 color : COLOR;
};
struct VertexOut
{
float4 posH : SV_POSITION;
float4 color : COLOR;
};
// Cube_VS.hlsl
#include "Cube.hlsli"
VertexOut VS(VertexIn vIn)
{
VertexOut vOut;
vOut.posH = mul(float4(vIn.posL, 1.0f), gWorld); // mul 才是矩阵乘法, 运算符*要求操作对象为
vOut.posH = mul(vOut.posH, gView); // 行列数相等的两个矩阵,结果为
vOut.posH = mul(vOut.posH, gProj); // Cij = Aij * Bij
vOut.color = vIn.color; // 这里alpha通道的值默认为1.0
return vOut;
}
// Cube_PS.hlsl
#include "Cube.hlsli"
float4 PS(VertexOut pIn) : SV_Target
{
return pIn.color;
}
tip: 在HLSL中,矩阵乘法不能用*运算符,该运算符要求两个矩阵行列数相同,运算的结果也是一个同行列数的矩阵,运算过程为:Cij = Aij * Bij。应该使用mul函数进行替代。
四、光栅化状态
Direct3D是基于状态机的,我们可以通过修改这些状态来修改渲染管线的当前行为。有三种状态值得我们现在以及后续留意:
- 光栅化状态(光栅化阶段)
- 混合状态(输出合并阶段)
- 深度/模板状态(输出合并阶段)
光栅化阶段尽管是不可编程的,但在该阶段需要完成许多任务:
-
顶点着色器、几何着色器完成了顶点输出后,光栅化阶段会负责对前面传来的顶点数据,尤其是对4D位置向量(SV_POSITION)进行透视除法,判断顶点是否在NDC空间内。
-
其次,根据光栅化状态来决定顶点顺(逆)时针排布的三角形是否通过。
-
它还能指定额外的裁剪区域,即矩形区域外的三角形(或者部分)被裁剪掉,仅留下剩余在矩形区域内的像素片元传递到像素着色器中进行处理。
-
根据三角形的三个顶点,光栅化就可以通过线性插值法对顶点内的所有成员(如位置,颜色,法向量等)进行插值运算,并根据视口信息计算出位于三角形内的所有像素顶点以传递给像素着色器,也正因为如此你才能在第二章看到高洛德着色(Gouraud
Shading)呈现的渐变效果。 -
由于光栅化阶段会进行视口变换,在像素着色器中,SV_POSITION的x分量和y分量都已经经过视口变换成为最终的屏幕坐标,且带有小数点0.5,这是因为要取到像素的中心位置,即对于800x600的视口区域,实际上的屏幕坐标取值范围为[0.5,
799.5]x[0.5, 599.5],z分量取值范围为[0, 1]。这一点读者可以修改像素着色器使得SV_POSITION与像素颜色结果有关联,然后进入调试以验证。
4.1 创建光栅化状态
- ID3D11Device::CreateRasterizerState方法–创建光栅化状态
在创建光栅化状态前,我们需要先填充D3D11_RASTERIZER_DESC结构体来描述光栅化状态
typedef struct D3D11_RASTERIZER_DESC
{
D3D11_FILL_MODE FillMode; // 填充模式
D3D11_CULL_MODE CullMode; // 裁剪模式
BOOL FrontCounterClockwise; // 是否三角形顶点按逆时针排布时为正面
INT DepthBias; // 忽略
FLOAT DepthBiasClamp; // 忽略
FLOAT SlopeScaledDepthBias; // 忽略
BOOL DepthClipEnable; // 是否允许深度测试将范围外的像素进行裁剪,默认TRUE
BOOL ScissorEnable; // 是否允许指定矩形范围的裁剪,若TRUE,则需要在RSSetScissor设置像素保留的矩形区域
BOOL MultisampleEnable; // 是否允许多重采样
BOOL AntialiasedLineEnable; // 是否允许反走样线,仅当多重采样为FALSE时才有效
} D3D11_RASTERIZER_DESC;
对于枚举类型D3D11_FILL_MODE有如下枚举值:
枚举类型D3D11_CULL_MODE有如下枚举值:
光栅化创建的方法如下:
HRESULT ID3D11Device::CreateRasterizerState(
const D3D11_RASTERIZER_DESC *pRasterizerDesc, // [In]光栅化状态描述
ID3D11RasterizerState **ppRasterizerState) = 0; // [Out]输出光栅化状态
4.2 设置光栅化状态
ID3D11DeviceContext::RSSetState方法–设置光栅化状态
void ID3D11DeviceContext::RSSetState(
ID3D11RasterizerState *pRasterizerState); // [In]光栅化状态,若为nullptr,则使用默认光栅化状态
默认光栅化状态如下:
FillMode = D3D11_FILL_SOLID;
CullMode = D3D11_CULL_BACK;
FrontCounterClockwise = FALSE;
DepthBias = 0;
SlopeScaledDepthBias = 0.0f;
DepthBiasClamp = 0.0f;
DepthClipEnable = TRUE;
ScissorEnable = FALSE;
MultisampleEnable = FALSE;
AntialiasedLineEnable = FALSE;
五、纹理映射与采样器状态
5.1 纹理读取
1). DDS位图和WIC位图
DDS是一种图片格式,是DirectDraw Surface的缩写,它是DirectX纹理压缩(DirectX Texture Compression,简称DXTC)的产物。由NVIDIA公司开发。大部分3D游戏引擎都可以使用DDS格式的图片用作贴图,也可以制作法线贴图。
WIC(Windows Imaging Component)是一个可以扩展的平台,为数字图像提供底层API,它可以支持bmp、dng、ico、jpeg、png、tiff等格式的位图。
2). DDSTextureLoader和WICTextureLoader库
要使用这两个库,有两种方案。
第一种:在DirectXTex中找到DDSTextureLoader文件夹和WICTextureLoader文件夹中分别找到对应的头文件和源文件(不带12的),并加入到你的项目中
第二种:将DirectXTK库添加到你的项目中,这里不再赘述
这之后就可以包含DDSTextureLoader.h和WICTextureLoader.h进项目中了。
- CreateDDSTextureFromFile函数–从文件读取DDS纹理
现在读取DDS纹理的操作变得更简单了:
HRESULT CreateDDSTextureFromFile(
ID3D11Device* d3dDevice, // [In]D3D设备
const wchar_t* szFileName, // [In]dds图片文件名
ID3D11Resource** texture, // [Out]输出一个指向资源接口类的指针,也可以填nullptr
ID3D11ShaderResourceView** textureView, // [Out]输出一个指向着色器资源视图的指针,也可以填nullptr
size_t maxsize = 0, // [In]忽略
DDS_ALPHA_MODE* alphaMode = nullptr); // [In]忽略
eg.下面是一个调用的例子:
// 初始化木箱纹理
HR(CreateDDSTextureFromFile(m_pd3dDevice.Get(), L"Texture\\WoodCrate.dds", nullptr, m_pWoodCrate.GetAddressOf()));
- CreateWICTextureFromFile函数–从文件读取WIC纹理
函数原型如下:
HRESULT CreateWICTextureFromFile(
ID3D11Device* d3dDevice, // [In]D3D设备
const wchar_t* szFileName, // [In]wic所支持格式的图片文件名
ID3D11Resource** texture, // [Out]输出一个指向资源接口类的指针,也可以填nullptr
ID3D11ShaderResourceView** textureView, // [Out]输出一个指向着色器资源视图的指针,也可以填nullptr
size_t maxsize = 0); // [In]忽略
eg.下面是一个调用的例子:
HR(CreateWICTextureFromFile(m_pd3dDevice.Get(), L"Texture\\Fire001.bmp", nullptr, m_pFireAnims[i - 1].GetAddressOf()));
这里我们只需要创建着色器资源视图,而不是纹理资源。
5.2 纹理采样
所谓采样,就是根据纹理坐标取出纹理对应位置最为接近的像素,在HLSL的写法如下:
g_Tex.Sample(g_SamLinear, pIn.Tex);
但大多数时候绘制出的纹理会比所用的纹理大或小,这样就还涉及到了采样器使用什么方式(如常量插值法、线性插值法、各向异性过滤)来处理图片放大、缩小的情况。
#include "LightHelper.hlsli"
Texture2D gTex : register(t0);
SamplerState gSamLinear : register(s0);
其中Texture2D类型保存了2D纹理的信息,在这是全局变量。而register(t0)对应起始槽索引0.
SamplerState类型确定采样器应如何进行采样,同样也是全局变量,register(s0)对应起始槽索引0.
上述两种变量都需要在C++应用层中初始化和绑定后才能使用。
Texture2D类型拥有Sample方法,需要提供采样器状态和2D纹理坐标方可使用,然后返回一个包含RGBA信息的float4向量。
[unroll]属性用于展开循环,避免不必要的跳转,但可能会产生大量的指令.
5.3 设置着色器资源
ID3D11DeviceContext::SSetShaderResources方法–设置着色器资源
需要注意的是,纹理并不能直接绑定到着色器中,需要为纹理创建对应的着色器资源视图才能够给着色器使用。上面打意味着渲染管线的所有可编程着色器阶段都有该方法。
此外,着色器资源视图不仅可以绑定纹理资源,还可以绑定缓冲区资源。有关缓冲区资源绑定到着色器资源视图的应用。
目前在DDSTextureLoader和WICTextureLoader中,我们只需要用到纹理的着色器资源。这里以ID3D11DeviceContext::PSSetShaderResources为例:
void ID3D11DeviceContext::PSSetShaderResources(
UINT StartSlot, // [In]起始槽索引,对应HLSL的register(t*)
UINT NumViews, // [In]着色器资源视图数目
ID3D11ShaderResourceView * const *ppShaderResourceViews // [In]着色器资源视图数组
);
eg. 然后调用方法如下:
m_pd3dImmediateContext->PSSetShaderResources(0, 1, m_pWoodCrate.GetAddressOf());
这样在HLSL里对应regisgter(t0)的g_Tex存放的就是木箱表面的纹理了。
5.4 创建采样器
ID3D11Device::CreateSamplerState方法–创建采样器状态
在C++代码层中,我们只能通过D3D设备创建采样器状态,然后绑定到渲染管线中,使得在HLSL中可以根据过滤器、寻址模式等进行采样。
在创建采样器状态之前,需要先填充结构体D3D11_SAMPLER_DESC来描述采样器状态:
typedef struct D3D11_SAMPLER_DESC
{
D3D11_FILTER Filter; // 所选过滤器
D3D11_TEXTURE_ADDRESS_MODE AddressU; // U方向寻址模式
D3D11_TEXTURE_ADDRESS_MODE AddressV; // V方向寻址模式
D3D11_TEXTURE_ADDRESS_MODE AddressW; // W方向寻址模式
FLOAT MipLODBias; // mipmap等级偏移值,最终算出的mipmap等级会加上该偏移值
UINT MaxAnisotropy; // 最大各向异性等级(1-16)
D3D11_COMPARISON_FUNC ComparisonFunc; // 这节不讨论
FLOAT BorderColor[ 4 ]; // 边界外的颜色,使用D3D11_TEXTURE_BORDER_COLOR时需要指定
FLOAT MinLOD; // 若mipmap等级低于MinLOD,则使用等级MinLOD。最小允许设为0
FLOAT MaxLOD; // 若mipmap等级高于MaxLOD,则使用等级MaxLOD。必须比MinLOD大
} D3D11_SAMPLER_DESC;
最后就是ID3D11Device::CreateSamplerState方法:
HRESULT ID3D11Device::CreateSamplerState(
const D3D11_SAMPLER_DESC *pSamplerDesc, // [In]采样器状态描述
ID3D11SamplerState **ppSamplerState); // [Out]输出的采样器
eg.接下来演示了如何创建采样器状态;
// 初始化采样器状态描述
D3D11_SAMPLER_DESC sampDesc;
ZeroMemory(&sampDesc, sizeof(sampDesc));
sampDesc.Filter = D3D11_FILTER_MIN_MAG_MIP_LINEAR;
sampDesc.AddressU = D3D11_TEXTURE_ADDRESS_WRAP;
sampDesc.AddressV = D3D11_TEXTURE_ADDRESS_WRAP;
sampDesc.AddressW = D3D11_TEXTURE_ADDRESS_WRAP;
sampDesc.ComparisonFunc = D3D11_COMPARISON_NEVER;
sampDesc.MinLOD = 0;
sampDesc.MaxLOD = D3D11_FLOAT32_MAX;
HR(m_pd3dDevice->CreateSamplerState(&sampDesc, m_pSamplerState.GetAddressOf()));
5.5 像素着色阶段设置采样器状态
void ID3D11DeviceContext::PSSetSamplers(
UINT StartSlot, // [In]起始槽索引
UINT NumSamplers, // [In]采样器状态数目
ID3D11SamplerState * const * ppSamplers); // [In]采样器数组
根据前面的HLSL代码,samLinear使用了索引为0起始槽,所以需要这样调用:
// 像素着色阶段设置好采样器
m_pd3dImmediateContext->PSSetSamplers(0, 1, m_pSamplerState.GetAddressOf());
这样在HLSL中便设置好了采样状态,如果以上您都可以理解的话即可完成开头旋转的立方体贴图效果了。