目录
6.1 VERTICES AND INPUT LAYOUTS
6.2 VERTEX BUFFERS
6.3 INDICES AND INDEX BUFFERS
6.4 EXAMPLE VERTEX SHADER
6.4.1 Input Layout Description and Input Signature Linking
6.5 EXAMPLE PIXEL SHADER
6.6 CONSTANT BUFFERS
6.6.1 Creating Constant Buffers
6.6.2 Updating Constant Buffers
6.6.3 Upload Buffer Helper
6.6.4 Constant Buffer Descriptors
6.6.5 Root Signature and Descriptor Tables
6.7 Compiling Shaders
6.7.1 Offline Compilation
6.7.2 Generated Assembly
6.7.3 Using Visual Studio to Compile Shaders Offline
6.8 Rasterizer State
6.9 Pipeline State Object
6.10 Geometry Helper Structure
6.11 Box Demo
6.13 EXERCISES
本章回到关注Direct3D API接口和方法,来配置渲染管线、定义顶点和像素着色器以及向渲染管线提交几何图形以供绘制。
要创建自定义顶点格式,我们首先创建一个结构来保存我们选择的顶点数据。例如下面举例说明了两种不同的顶点格式:一个由位置和颜色组成,另一个由位置、法向量和两组二维纹理坐标组成:
struct Vertex1
{
XMFLOAT3 Pos;
XMFLOAT4 Color;
};
struct Vertex2
{
XMFLOAT3 Pos;
XMFLOAT3 Normal;
XMFLOAT2 Tex0;
XMFLOAT2 Tex1;
};
一旦我们定义了一个顶点结构,我们需要为Direct3D提供顶点结构的描述,以便它知道如何处理每个组件。该描述以input layout description的形式提供给Direct3D,由D3D12_INPUT_LAYOUT_DESC结构表示:
typedef struct D3D12_INPUT_LAYOUT_DESC
{
const D3D12_INPUT_ELEMENT_DESC *pInputElementDescs;
UINT NumElements;
} D3D12_INPUT_LAYOUT_DESC;
input layout description就是D3D12_INPUT_ELEMENT_DESC元素的数组,以及数组中的元素数量。
D3D12_INPUT_ELEMENT_DESC数组中的每个元素描述并对应于顶点结构中的一个组件。因此,如果顶点结构有两个组件,那么相应的D3D12_INPUT_ELEMENT_DESC数组将有两个元素。D3D12_INPUT_ELEMENT_DESC结构定义为:
typedef struct D3D12_INPUT_ELEMENT_DESC
{
LPCSTR SemanticName;
UINT SemanticIndex;
DXGI_FORMAT Format;
UINT InputSlot;
UINT AlignedByteOffset;
D3D12_INPUT_CLASSIFICATION InputSlotClass;
UINT InstanceDataStepRate;
} D3D12_INPUT_ELEMENT_DESC;
DXGI_FORMAT_R32_FLOAT // 1D 32-bit float scalar
DXGI_FORMAT_R32G32_FLOAT // 2D 32-bit float vector
DXGI_FORMAT_R32G32B32_FLOAT // 3D 32-bit float vector
DXGI_FORMAT_R32G32B32A32_FLOAT // 4D 32-bit float vector
DXGI_FORMAT_R8_UINT // 1D 8-bit unsigned integer scalar
DXGI_FORMAT_R16G16_SINT // 2D 16-bit signed integer vector
DXGI_FORMAT_R32G32B32_UINT // 3D 32-bit unsigned integer vector
DXGI_FORMAT_R8G8B8A8_SINT // 4D 8-bit signed integer vector
DXGI_FORMAT_R8G8B8A8_UINT // 4 8-bit unsigned integer vector
InputSlot:指定元素将来自的input slot索引。Direct3D支持16个input slot(0-15),通过它们来提供顶点数据。目前暂时我们将只使用输入槽0(所有顶点元素均来自同一输入槽)。
AlignedByteOffset:以字节为单位,从指定的input slot的c++顶点结构开始到顶点组件开始的偏移量。例如,在下面的顶点结构中,元素Pos是0字节的偏移量,因为它的开始与顶点结构的开始相一致。元素Normal有12字节的偏移量,因为我们必须跳过Pos的字节才能到达Normal的开始。
struct Vertex2
{
XMFLOAT3 Pos; // 0-byte offset
XMFLOAT3 Normal; // 12-byte offset
XMFLOAT2 Tex0; // 24-byte offset
XMFLOAT2 Tex1; // 32-byte offset
};
InputSlotClass:暂时指定D3D12_INPUT_CLASSIFICATION_PER_VERTEX_DATA(龙书写的是D3D12_INPUT_PER_VERTEX_DATA)。另一个选项用于instancing。
InstanceDataStepRate:现在指定0;其他值仅用于instancing。
对于前两个顶点结构示例Vertex1和Vertex2,相应的input layout description如下:
D3D12_INPUT_ELEMENT_DESC desc1[] =
{
{"POSITION", 0, DXGI_FORMAT_R32G32B32_FLOAT, 0, 0, D3D12_INPUT_CLASSIFICATION_PER_VERTEX_DATA, 0},
{"COLOR", 0, DXGI_FORMAT_R32G32B32A32_FLOAT, 0, 12, D3D12_INPUT_CLASSIFICATION_PER_VERTEX_DATA, 0}
};
D3D12_INPUT_ELEMENT_DESC desc2[] =
{
{"POSITION", 0, DXGI_FORMAT_R32G32B32_FLOAT, 0, 0, D3D12_INPUT_CLASSIFICATION_PER_VERTEX_DATA, 0},
{"NORMAL", 0, DXGI_FORMAT_R32G32B32_FLOAT, 0, 12, D3D12_INPUT_CLASSIFICATION_PER_VERTEX_DATA, 0},
{"TEXCOORD", 0, DXGI_FORMAT_R32G32_FLOAT, 0, 24, D3D12_INPUT_CLASSIFICATION_PER_VERTEX_DATA, 0},
{"TEXCOORD", 1, DXGI_FORMAT_R32G32_FLOAT, 0, 32, D3D12_INPUT_CLASSIFICATION_PER_VERTEX_DATA, 0}
};
为了让GPU访问一个顶点数组,需要将它们放在一个称为缓冲区的GPU资源(ID3D12Resource)中。我们称存储顶点的缓冲区为vertex buffer。缓冲区是比纹理更简单的资源:它们不是多维的,也没有mipmaps、过滤器或多采样支持。在我们需要为GPU提供诸如顶点的数据元素数组时将使用缓冲区。
正如我们在4.3.8中所做的,我们通过填写描述缓冲区资源的D3D12_RESOURCE_DESC结构来创建ID3D12Resource对象,然后调用ID3D12Device::CreateCommittedResource方法创建资源。Direct3D 12提供了一个c++封装类CD3DX12_RESOURCE_DESC(继承自D3D12_RESOURCE_DESC),并提供了方便的构造函数和方法。它提供了以下方法来简化描述缓冲区的D3D12_RESOURCE_DESC的构造:
static inline CD3DX12_RESOURCE_DESC Buffer(
UINT64 width,
D3D12_RESOURCE_FLAGS flags = D3D12_RESOURCE_FLAG_NONE,
UINT64 alignment = 0 )
{
return CD3DX12_RESOURCE_DESC(
D3D12_RESOURCE_DIMENSION_BUFFER,
alignment, width, 1, 1, 1,
DXGI_FORMAT_UNKNOWN, 1, 0,
D3D12_TEXTURE_LAYOUT_ROW_MAJOR, flags );
}
缓冲区的宽度指缓冲区中的字节数。例如,如果缓冲区存储64个浮点数,那么宽度就是64*sizeof(float)。
CD3DX12_RESOURCE_DESC类还提供了更为方便的用于构造描述纹理资源和查询资源信息的D3D12_RESOURCE_DESC的方法:
- CD3DX12_RESOURCE_DESC::Tex1D
- CD3DX12_RESOURCE_DESC::Tex2D
- CD3DX12_RESOURCE_DESC::Tex3D
Direct3D 12中的所有资源都由ID3D12Resource接口表示。这与Direct3D 11形成了对比,Direct3D 11为各种资源提供了不同的接口,如ID3D11Buffer和ID3D11Texture2D。资源的类型由D3D12_RESOURCE_DESC::D3D12_RESOURCE_DIMENSION字段指定。例如,缓冲区的尺寸为D3D12_RESOURCE_DIMENSION_BUFFER,二维纹理的尺寸为D3D12_RESOURCE_DIMENSION_TEXTURE2D。
对于静态的几何图形(每帧不改变的几何图形),我们将vertex buffer放在默认堆中(D3D12_HEAP_TYPE_DEFAULT)以获得最佳性能。一般来说游戏中的大多数几何体都是这样(如树,建筑,地形,角色)。vertex buffer初始化后,只有GPU需要从vertex buffer中读取来绘制几何图形,所以默认的堆是有意义的。既然CPU无法写入默认堆中的vertex buffer,那如何初始化我们的vertex buffer?
除了创建实际的vertex buffer资源外,我们还需要创建一个具有堆类型D3D12_HEAP_TYPE_UPLOAD的中间upload buffer资源。回顾4.3.8,当需要将数据从CPU复制到GPU内存时,我们将资源提交到upload heap。创建upload buffer之后,我们将顶点数据从系统内存复制到upload buffer,然后将顶点数据从upload buffer复制到实际的vertex buffer。
因为需要一个中间upload buffer来初始化默认缓冲区(堆类型为D3D12_HEAP_TYPE_DEFAULT的缓冲区)的数据,所以我们在d3dUtil.h/.cpp中构建了以下实用函数,以避免每次需要默认缓冲区时重复这项工作:
Microsoft::WRL::ComPtr d3dUtil::CreateDefaultBuffer(
ID3D12Device* device,
ID3D12GraphicsCommandList* cmdList,
const void* initData,
UINT64 byteSize,
Microsoft::WRL::ComPtr& uploadBuffer)
{
ComPtr defaultBuffer;
// Create the actual default buffer resource.
ThrowIfFailed(device->CreateCommittedResource(
&CD3DX12_HEAP_PROPERTIES(D3D12_HEAP_TYPE_DEFAULT),
D3D12_HEAP_FLAG_NONE,
&CD3DX12_RESOURCE_DESC::Buffer(byteSize),
D3D12_RESOURCE_STATE_COMMON,
nullptr,
IID_PPV_ARGS(defaultBuffer.GetAddressOf())));
// In order to copy CPU memory data into our default buffer, we need
// to create an intermediate upload heap.
ThrowIfFailed(device->CreateCommittedResource(
&CD3DX12_HEAP_PROPERTIES(D3D12_HEAP_TYPE_UPLOAD),
D3D12_HEAP_FLAG_NONE,
&CD3DX12_RESOURCE_DESC::Buffer(byteSize),
D3D12_RESOURCE_STATE_GENERIC_READ,
nullptr,
IID_PPV_ARGS(uploadBuffer.GetAddressOf())));
// Describe the data we want to copy into the default buffer.
D3D12_SUBRESOURCE_DATA subResourceData = {};
subResourceData.pData = initData;
subResourceData.RowPitch = byteSize;
subResourceData.SlicePitch = subResourceData.RowPitch;
// Schedule to copy the data to the default buffer resource.
// At a high level, the helper function UpdateSubresources
// will copy the CPU memory into the intermediate upload heap.
// Then, using ID3D12CommandList::CopySubresourceRegion,
// the intermediate upload heap data will be copied to mBuffer.
cmdList->ResourceBarrier(1,
&CD3DX12_RESOURCE_BARRIER::Transition(defaultBuffer.Get(),
D3D12_RESOURCE_STATE_COMMON,
D3D12_RESOURCE_STATE_COPY_DEST));
UpdateSubresources<1>(cmdList,
defaultBuffer.Get(), uploadBuffer.Get(),
0, 0, 1, &subResourceData);
cmdList->ResourceBarrier(1,
&CD3DX12_RESOURCE_BARRIER::Transition(defaultBuffer.Get(),
D3D12_RESOURCE_STATE_COPY_DEST,
D3D12_RESOURCE_STATE_GENERIC_READ));
// Note: uploadBuffer has to be kept alive after the above function
// calls because the command list has not been executed yet that
// performs the actual copy.
// The caller can Release the uploadBuffer after it knows the copy
// has been executed.
return defaultBuffer;
}
D3D12_SUBRESOURCE_DATA结构体定义如下:
typedef struct D3D12_SUBRESOURCE_DATA
{
const void *pData;
LONG_PTR RowPitch;
LONG_PTR SlicePitch;
} D3D12_SUBRESOURCE_DATA;
下面的代码展示了如何使用这个类来创建一个默认的缓冲区,该缓冲区存储一个立方体的8个顶点,每个顶点都有不同的颜色与之关联:
Vertex vertices[] =
{
{ XMFLOAT3(-1.0f, -1.0f, -1.0f), XMFLOAT4(Colors::White) },
{ XMFLOAT3(-1.0f, +1.0f, -1.0f), XMFLOAT4(Colors::Black) },
{ XMFLOAT3(+1.0f, +1.0f, -1.0f), XMFLOAT4(Colors::Red) },
{ XMFLOAT3(+1.0f, -1.0f, -1.0f), XMFLOAT4(Colors::Green) },
{ XMFLOAT3(-1.0f, -1.0f, +1.0f), XMFLOAT4(Colors::Blue) },
{ XMFLOAT3(-1.0f, +1.0f, +1.0f), XMFLOAT4(Colors::Yellow) },
{ XMFLOAT3(+1.0f, +1.0f, +1.0f), XMFLOAT4(Colors::Cyan) },
{ XMFLOAT3(+1.0f, -1.0f, +1.0f), XMFLOAT4(Colors::Magenta) }
};
const UINT64 vbByteSize = 8 * sizeof(Vertex);
ComPtr VertexBufferGPU = nullptr;
ComPtr VertexBufferUploader = nullptr;
VertexBufferGPU = d3dUtil::CreateDefaultBuffer(md3dDevice.Get(),
mCommandList.Get(), vertices, vbByteSize, VertexBufferUploader);
Vertex类型和颜色被定义如下 :
struct Vertex
{
XMFLOAT3 Pos;
XMFLOAT4 Color;
};
为了将vertex buffer绑定到管线,我们需要创建vertex buffer view到vertex buffer资源。与RTV(render target view)不同,我们不需要vertex buffer view的descriptor heap。vertex buffer view由D3D12_VERTEX_BUFFER_VIEW_DESC结构表示:
typedef struct D3D12_VERTEX_BUFFER_VIEW
{
D3D12_GPU_VIRTUAL_ADDRESS BufferLocation;
UINT SizeInBytes;
UINT StrideInBytes;
} D3D12_VERTEX_BUFFER_VIEW;
创建vertex buffer并创建view之后,可以将其绑定到管线的input slot,将顶点提供给管道的input assembler阶段。这可以通过以下方法实现:
void ID3D12GraphicsCommandList::IASetVertexBuffers(
UINT StartSlot,
UINT NumBuffers,
const D3D12_VERTEX_BUFFER_VIEW *pViews);
例子如下:
D3D12_VERTEX_BUFFER_VIEW vbv;
vbv.BufferLocation = VertexBufferGPU->GetGPUVirtualAddress();
vbv.StrideInBytes = sizeof(Vertex);
vbv.SizeInBytes = 8 * sizeof(Vertex);
D3D12_VERTEX_BUFFER_VIEW vertexBuffers[1] = { vbv };
mCommandList->IASetVertexBuffers(0, 1, vertexBuffers);
IASetVertexBuffers方法可能看起来有点复杂,因为它支持将vertex buffer数组设置为各种input slot。但是,目前我们将只使用一个input slot。
vertex buffer将一直绑定到input slot直到更改它。所以如果你使用一个以上的顶点缓冲区,你可以这样组织你的代码:
ID3D12Resource* mVB1; // stores vertices of type Vertex1
ID3D12Resource* mVB2; // stores vertices of type Vertex2
D3D12_VERTEX_BUFFER_VIEW_DESC mVBView1; // view to mVB1
D3D12_VERTEX_BUFFER_VIEW_DESC mVBView2; // view to mVB2
/*…Create the vertex buffers and views…*/
mCommandList->IASetVertexBuffers(0, 1, &VBView1);
/* …draw objects using vertex buffer 1… */
mCommandList->IASetVertexBuffers(0, 1, &mVBView2);
/* …draw objects using vertex buffer 2… */
将vertex buffer设置为input slot不会绘制它们,它只让顶点准备好被送入管道。实际绘制顶点的最后一步是使用ID3D12GraphicsCommandList:: DrawInstanced方法:
void ID3D12CommandList::DrawInstanced(
UINT VertexCountPerInstance,
UINT InstanceCount,
UINT StartVertexLocation,
UINT StartInstanceLocation);
其中两个参数VertexCountPerInstance和StartVertexLocation在顶点缓冲区中定义要绘制的顶点的连续子集:
DrawInstanced方法没有指定顶点定义的图元类型。参考5.5.2,图元拓扑状态是用ID3D12GraphicsCommandList::IASetPrimitiveTopology方法设置的。下面是一个例子:
cmdList->IASetPrimitiveTopology(D3D_PRIMITIVE_TOPOLOGY_TRIANGLELIST
与顶点类似,为了让GPU访问一个索引数组,它们需要被放置在一个GPU资源(ID3D12Resource)buffer中。我们称存储索引的缓冲区为index buffer。因为我们的d3dUtil::CreateDefaultBuffer函数通过void*处理通用数据,所以我们可以使用这个相同的函数来创建一个index buffer(或任何默认缓冲区)。
为了将index buffer绑定到管线,我们需要创建index buffer view到index buffer资源。与vertex buffer view一样,我们不需要index buffer view的descriptor heap。index buffer view由D3D12_INDEX_BUFFER_VIEW结构表示:
typedef struct D3D12_INDEX_BUFFER_VIEW
{
D3D12_GPU_VIRTUAL_ADDRESS BufferLocation;
UINT SizeInBytes;
DXGI_FORMAT Format;
} D3D12_INDEX_BUFFER_VIEW;
与vertex buffer和其他Direct3D资源一样,在使用它之前我们需要将它绑定到管线。使用ID3D12CommandList::SetIndexBuffer方法将index buffer绑定到输入汇编程序阶段。下面的代码演示了如何创建一个定义立方体三角形的index buffer,创建一个view,并将其绑定到管线:
std::uint16_t indices[] = {
// front face
0, 1, 2,
0, 2, 3,
// back face
4, 6, 5,
4, 7, 6,
// left face
4, 5, 1,
4, 1, 0,
// right face
3, 2, 6,
3, 6, 7,
// top face
1, 5, 6,
1, 6, 2,
// bottom face
4, 0, 3,
4, 3, 7
};
const UINT ibByteSize = 36 * sizeof(std::uint16_t);
ComPtr IndexBufferGPU = nullptr;
ComPtr IndexBufferUploader = nullptr;
IndexBufferGPU = d3dUtil::CreateDefaultBuffer(md3dDevice.Get(),
mCommandList.Get(), indices, ibByteSize, IndexBufferUploader);
D3D12_INDEX_BUFFER_VIEW ibv;
ibv.BufferLocation = IndexBufferGPU->GetGPUVirtualAddress();
ibv.Format = DXGI_FORMAT_R16_UINT;
ibv.SizeInBytes = ibByteSize;
mCommandList->IASetIndexBuffer(&ibv);
在使用索引时,我们必须使用ID3D12GraphicsCommandList:: DrawIndexedInstanced 方法,而不是DrawInstanced:
void ID3D12GraphicsCommandList::DrawIndexedInstanced(
UINT IndexCountPerInstance,
UINT InstanceCount,
UINT StartIndexLocation,
INT BaseVertexLocation,
UINT StartInstanceLocation);
假设我们有三个对象:球、盒子和圆柱体,每个对象都有自己的vertex buffer和index buffer,每个局部index buffer.中的索引都有对应的局部顶点缓冲区。假设将球、盒子和圆柱体的顶点和索引连接到一个全局vertex buffer和index buffer中,如下图所示。(因为在更改顶点和索引缓冲区时需要一些API开销,可以连接他们的vertex buffer和index buffer。一般这不会成为瓶颈,但如果有可以很容易合并的许多小的vertex buffer和index buffer,那么出于性能考虑这样做是值得的。)在连接之后,索引不再是正确的了,因为它们存储的索引位置相对于它们相应的局部vertex buffer,而不是全局的buffer,因此索引需要重新计算。
通常,通过将对象的基本顶点位置添加到每个索引中来计算对象的新索引。不必自己计算新的索引,我们可以让Direct3D通过将基本顶点位置传递给DrawIndexedInstanced的第四个参数来完成。
我们可以通过以下三个调用逐一绘制球、盒子和圆柱体:
mCmdList->DrawIndexedInstanced(numSphereIndices, 1, 0, 0, 0);
mCmdList->DrawIndexedInstanced(numBoxIndices, 1, firstBoxIndex, firstBoxVertexPos,0);
mCmdList->DrawIndexedInstanced(numCylIndices, 1, firstCylIndex, firstCylVertexPos,0);
总结一下vertex buffer和index buffer的步骤:
- 声明描述
- 声明Verter类型的变量,Beffur函数获取CD3DX12_RESOURCE_DESC实例。
- 声明uint16数组indices,Beffur函数获取CD3DX12_RESOURCE_DESC实例。
- 使用上述的两个信息利用CreateCommittedResource创建vertex/index buffer资源和upload buffer资源并且提交到对应的堆,CPU利用upload buffer来更新,然后拷贝到实际的vertex/index buffer上。
- IASetVertexBuffers创建了view,绑定到管线的input slot,并且将顶点和索引提供给管道的input assembler阶段。
- 用DrawInstanced或DrawIndexedInstanced开始绘制。
GPU不直接绑定资源,而是用过descriptor(view)对象来引用,而descriptor会放在我们创建的descriptor heap中,所以创建render target view,depth/stencil buffer view的函数都需要从descriptor heap中获取descriptor句柄参数。IASetVertexBuffers创建了view并且直接把资源绑定到管线,所以不需要有descriptor heap。
下面是一个简单顶点着色器的实现(参见5.6):
cbuffer cbPerObject : register(b0)
{
float4x4 gWorldViewProj;
};
void VS(float3 iPosL : POSITION,
float4 iColor : COLOR,
out float4 oPosH : SV_POSITION,
out float4 oColor : COLOR)
{
// Transform to homogeneous clip space.
oPosH = mul(float4(iPosL, 1.0f), gWorldViewProj);
// Just pass vertex color into the pixel shader.
oColor = iColor;
}
着色器是用一种叫做高级着色语言(HLSL)的语言编写的,它的语法类似于c++,因此很容易学习。随着本书的深入,我们将引入任何新的HLSL概念,以便实现手边的演示。着色器通常编写在基于文本的文件中,扩展名为.hlsl。
顶点着色器是被调用的函数VS,你可以给顶点着色器任何有效的函数名。这个顶点着色器有四个参数:前两个是输入参数,后两个是输出参数(由out关键字表示)。HLSL没有引用或指针,因此要从一个函数返回多个值,您需要使用结构或out参数。在HLSL中,函数总是内联的。
前两个输入参数形成了顶点着色器的输入签名,并对应于我们绘制时使用的自定义顶点结构中的数据成员。使用参数语义“:POSITION”和“:COLOR”将顶点结构中的元素映射到顶点着色器输入参数,如下图。
暂时不写HLSL。
constant buffer是GPU资源(ID3D12Resource)的一个例子,它的数据内容可以在着色程序中引用,纹理和其他类型的缓冲资源也可以在着色程序中引用。6.4中的顶点着色器示例代码有如下一段:
cbuffer cbPerObject : register(b0)
{
float4x4 gWorldViewProj;
};
这段代码引用了一个名为cbPerObject的cbuffer对象(constant buffer),在这个示例中,constant buffer存储一个4×4的矩阵gWorldViewProj。
与vertex buffer和index buffer不同,constant buffer通常被CPU每帧更新一次。比如如果摄像机正在移动每一帧,那么constant buffer将需要在每帧更新新的视图矩阵。因此,我们在upload堆中创建constant buffer而不是默认堆中,以便我们可以从CPU更新内容。
constant buffer还有一个特殊的硬件要求,它们的大小必须是硬件最小分配大小(256字节)的倍数。
我们经常需要多个相同类型的constant buffer。例如,上面的constant buffer cbPerObject存储每个对象都不同的常量,所以如果我们有n个对象,那么我们将需要n个这种类型的constant buffer。下面的代码显示了我们如何创建一个缓冲区,存储NumElements个constant buffer:
struct ObjectConstants
{
DirectX::XMFLOAT4X4 WorldViewProj = MathHelper::Identity4x4();
};
UINT elementByteSize = d3dUtil::CalcConstantBufferByteSize(sizeof(ObjectConstants));
ComPtr mUploadCBuffer;
device->CreateCommittedResource(
&CD3DX12_HEAP_PROPERTIES(D3D12_HEAP_TYPE_UPLOAD),
D3D12_HEAP_FLAG_NONE,
&CD3DX12_RESOURCE_DESC::Buffer(mElementByteSize * NumElements),
D3D12_RESOURCE_STATE_GENERIC_READ,
nullptr,
IID_PPV_ARGS(&mUploadCBuffer));
我们可以将mUploadCBuffer看作是存储ObjectConstants类型的constant buffer数组(使用填充使其成为256字节的倍数)。当需要绘制对象时,我们只需将constant buffer view(CBV)绑定到存储该对象常量的buffer的子区域。注意,我们通常将缓冲区mUploadCBuffer称为constant buffer,因为它存储了一个constant buffer数组。
功能函数d3dUtil::CalcConstantBufferByteSize做的算法是将缓冲区的字节大小四舍五入为最小硬件分配大小(256字节)的倍数:
UINT d3dUtil::CalcConstantBufferByteSize(UINT
byteSize)
{
// Constant buffers must be a multiple of the minimum hardware
// allocation size (usually 256 bytes). So round up to nearest
// multiple of 256. We do this by adding 255 and then masking off
// the lower 2 bytes which store all bits < 256.
// Example: Suppose byteSize = 300.
// (300 + 255) & ˜255
// 555 & ˜255
// 0x022B & ˜0x00ff
// 0x022B & 0xff00
// 0x0200
// 512
return (byteSize + 255) & ˜255;
}
即使我们以256的倍数分配常数数据,也不需要显式地填充HLSL结构中相应的常数数据,因为它是隐式完成的:
// Implicitly padded to 256 bytes. cbuffer cbPerObject : register(b0) { float4x4 gWorldViewProj; }; // Explicitly padded to 256 bytes. cbuffer cbPerObject : register(b0) { float4x4 gWorldViewProj; float4x4 Pad0; float4x4 Pad1; float4x4 Pad1; };
为了避免将constant buffer元素舍入为256字节的倍数,可以显式地将所有constant buffer结构填充为256字节的倍数。
Direct3D 12引入了着色器model 5.1。着色器model 5.1引入了另一种HLSL语法来定义一个常量缓冲区,如下图所示:
struct ObjectConstants
{
float4x4 gWorldViewProj;
uint matIndex;
};
ConstantBuffer gObjConstants : register(b0);
然后使用数据成员的语法在着色器中访问constant buffer的字段。
uint index = gObjConstants.matIndex;
因为我们使用了D3D12_HEAP_TYPE_UPLOAD创建了一个constant buffer,所以我们可以将数据从CPU上传到constant buffer资源。要做到这一点,我们首先必须获得一个指向资源数据的指针,这可以通过Map方法实现:
ComPtr mUploadBuffer;
BYTE* mMappedData = nullptr;
mUploadBuffer->Map(0, nullptr, reinterpret_cast(&mMappedData));
为了将数据从系统内存复制到constant buffer,我们可以做一个memcpy:
memcpy(mMappedData, &data, dataSizeInBytes);
当我们完成了一个constant buffer,我们应该在释放内存前Unmap:
if(mUploadBuffer != nullptr)
mUploadBuffer->Unmap(0, nullptr);
mMappedData = nullptr;
在upload buffer周围构建一个轻量级包装类非常方便。我们在UploadBuffer.h中定义了以下类,以便更容易地使用upload缓冲区。它为我们处理upload buffer资源的构造和销毁,处理资源的mapping和unmapping,并提供CopyData方法来更新缓冲区中的特定元素。我们使用CopyData方法当需要改变CPU上传缓冲区的内容时(如当view矩阵改变时)。注意这个类可以用于任何upload buffer,不一定是constant buffer。但是如果我们确实将它用于constant buffer,则需要通过isConstantBuffer构造函数参数说明这一点。如果它存储一个constant buffer,那么它将自动填充内存,使每个constant buffer为256字节的倍数。
template class UploadBuffer
{
public:
UploadBuffer(ID3D12Device* device, UINT elementCount, bool isConstantBuffer) :
mIsConstantBuffer(isConstantBuffer)
{
mElementByteSize = sizeof(T);
// Constant buffer elements need to be multiples of 256 bytes.
// This is because the hardware can only view constant data
// at m*256 byte offsets and of n*256 byte lengths.
// typedef struct D3D12_CONSTANT_BUFFER_VIEW_DESC {
// UINT64 OffsetInBytes; // multiple of 256
// UINT SizeInBytes; // multiple of 256
// } D3D12_CONSTANT_BUFFER_VIEW_DESC;
if(isConstantBuffer)
mElementByteSize = d3dUtil::CalcConstantBufferByteSize(sizeof(T));
ThrowIfFailed(device->CreateCommittedResource(
&CD3DX12_HEAP_PROPERTIES(D3D12_HEAP_TYPE_UPLOAD),
D3D12_HEAP_FLAG_NONE,
&CD3DX12_RESOURCE_DESC::Buffer(mElementByteSize*elementCount),
D3D12_RESOURCE_STATE_GENERIC_READ,
nullptr,
IID_PPV_ARGS(&mUploadBuffer)));
ThrowIfFailed(mUploadBuffer->Map(0, nullptr,
reinterpret_cast(&mMappedData)));
// We do not need to unmap until we are done with the resource.
// However, we must not write to the resource while it is in use by
// the GPU (so we must use synchronization techniques).
}
UploadBuffer(const UploadBuffer& rhs) = delete;
UploadBuffer& operator=(const UploadBuffer& rhs) = delete;
˜UploadBuffer()
{
if(mUploadBuffer != nullptr)
mUploadBuffer->Unmap(0, nullptr);
mMappedData = nullptr;
}
ID3D12Resource* Resource()const
{
return mUploadBuffer.Get();
}
void CopyData(int elementIndex, const T& data)
{
memcpy(&mMappedData[elementIndex*mElementByteSize], &data, sizeof(T));
}
private:
Microsoft::WRL::ComPtr mUploadBuffer;
BYTE* mMappedData = nullptr;
UINT mElementByteSize = 0;
bool mIsConstantBuffer = false;
};
在本章的演示中,我们允许用户用鼠标旋转和移动相机,并在Update函数的每一帧中使用新的视图矩阵更新合并的world-view-projection矩阵:
void BoxApp::OnMouseMove(WPARAM btnState, int x, int y)
{
if((btnState & MK_LBUTTON) != 0)
{
// Make each pixel correspond to a quarter of a degree.
float dx = XMConvertToRadians(0.25f*static_cast (x - mLastMousePos.x));
float dy = XMConvertToRadians(0.25f*static_cast (y - mLastMousePos.y));
// Update angles based on input to orbit camera around box.
mTheta += dx;
mPhi += dy;
// Restrict the angle mPhi.
mPhi = MathHelper::Clamp(mPhi, 0.1f, MathHelper::Pi - 0.1f);
}
else if((btnState & MK_RBUTTON) != 0)
{
// Make each pixel correspond to 0.005 unit in the scene.
float dx = 0.005f*static_cast(x - mLastMousePos.x);
float dy = 0.005f*static_cast(y - mLastMousePos.y);
// Update the camera radius based on input.
mRadius += dx - dy;
// Restrict the radius.
mRadius = MathHelper::Clamp(mRadius, 3.0f, 15.0f);
}
mLastMousePos.x = x;
mLastMousePos.y = y;
}
void BoxApp::Update(const GameTimer& gt)
{
// Convert Spherical to Cartesian coordinates.
float x = mRadius*sinf(mPhi)*cosf(mTheta);
float z = mRadius*sinf(mPhi)*sinf(mTheta);
float y = mRadius*cosf(mPhi);
// Build the view matrix.
XMVECTOR pos = XMVectorSet(x, y, z, 1.0f);
XMVECTOR target = XMVectorZero();
XMVECTOR up = XMVectorSet(0.0f, 1.0f, 0.0f, 0.0f);
XMMATRIX view = XMMatrixLookAtLH(pos, target, up);
XMStoreFloat4x4(&mView, view);
XMMATRIX world = XMLoadFloat4x4(&mWorld);
XMMATRIX proj = XMLoadFloat4x4(&mProj);
XMMATRIX worldViewProj = world*view*proj;
// Update the constant buffer with the latest
worldViewProj matrix.
ObjectConstants objConstants;
XMStoreFloat4x4(&objConstants.WorldViewProj,
XMMatrixTranspose(mObjectCB->CopyData(0, objConstants));
}
在4.1.6中,我们通过descriptor对象将资源绑定到渲染管线。到目前为止我们已经使用descriptors/views来实现render targets,depth/stencil buffers,vertex buffers和index buffers。我们还需要descriptor来将constant buffer绑定到管线。constant buffer descriptor位于D3D12_DESCRIPTOR_HEAP_TYPE_CBV_SRV_UAV类型的descriptor heap中。这样的堆可以混合存储constant buffer、着色器资源和无序访问资源三者的descriptor。为了存储这些新的descriptor,我们需要创建一个新的descriptor heap:
D3D12_DESCRIPTOR_HEAP_DESC cbvHeapDesc;
cbvHeapDesc.NumDescriptors = 1;
cbvHeapDesc.Type = D3D12_DESCRIPTOR_HEAP_TYPE_CBV_SRV_UAV;
cbvHeapDesc.Flags = D3D12_DESCRIPTOR_HEAP_FLAG_SHADER_VISIBLE;
cbvHeapDesc.NodeMask = 0;
ComPtr mCbvHeap;
md3dDevice->CreateDescriptorHeap(&cbvHeapDesc, IID_PPV_ARGS(&mCbvHeap));
和之前的descriptor heap区别是我们指定了D3D12_DESCRIPTOR_HEAP_FLAG_SHADER_VISIBLE标志来指示着色程序将访问这些descriptor。
通过填写D3D12_CONSTANT_BUFFER_VIEW_DESC实例并调用ID3D12Device::CreateConstantBufferView创建constant buffer view:
// Constant data per-object.
struct ObjectConstants
{
XMFLOAT4X4 WorldViewProj = MathHelper::Identity4x4();
};
// Constant buffer to store the constants of n object.
std::unique_ptr> mObjectCB = nullptr;
mObjectCB = std::make_unique>(md3dDevice.Get(), n, true);
UINT objCBByteSize = d3dUtil::CalcConstantBufferByteSize(sizeof(ObjectConstants));
// Address to start of the buffer (0th constant buffer).
D3D12_GPU_VIRTUAL_ADDRESS cbAddress = mObjectCB->Resource()->GetGPUVirtualAddress();
// Offset to the ith object constant buffer in the buffer.
int boxCBufIndex = i;
cbAddress += boxCBufIndex*objCBByteSize;
D3D12_CONSTANT_BUFFER_VIEW_DESC cbvDesc;
cbvDesc.BufferLocation = cbAddress;
cbvDesc.SizeInBytes = d3dUtil::CalcConstantBufferByteSize(sizeof(ObjectConstants));
md3dDevice->CreateConstantBufferView(
&cbvDesc,
mCbvHeap->GetCPUDescriptorHandleForHeapStart());
D3D12_CONSTANT_BUFFER_VIEW_DESC结构描述了要绑定到HLSL constant buffer结构的constant buffer资源的子集。如前所述,通常一个constant buffer为n个对象存储每个对象常量的数组,但是我们可以通过使用BufferLocation和SizeInBytes获得第i个对象常量数据的view。由于硬件需求,D3D12_CONSTANT_BUFFER_VIEW_DESC::SizeInBytes和D3D12_CONSTANT_BUFFER_VIEW_DESC::OffsetInBytes成员必须是256字节的倍数。例如,如果您指定64,那么您将得到以下调试错误:
D3D12 ERROR: ID3D12Device::CreateConstantBufferView: SizeInBytes of 64 is invalid. Device requires SizeInBytes be a multiple of 256.
D3D12 ERROR: ID3D12Device:: CreateConstantBufferView: OffsetInBytes of 64 is invalid. Device requires OffsetInBytes be a multiple of 256.
通常,在执行draw调用之前,不同的着色器程序会期望将不同的资源绑定到渲染管线。资源被绑定到特定的寄存器槽,在那里它们可以被着色程序访问。例如前面的顶点和像素着色器只期望一个constant buffer被绑定到寄存器b0。我们在本书后面使用一些更高级的顶点和像素着色器,几个常量缓冲区,纹理和采样器绑定到不同的寄存器槽:
// Texture resource bound to texture register slot 0.
Texture2D gDiffuseMap : register(t0);
// Sampler resources bound to sampler register slots 0-5.
SamplerState gsamPointWrap : register(s0);
SamplerState gsamPointClamp : register(s1);
SamplerState gsamLinearWrap : register(s2);
SamplerState gsamLinearClamp : register(s3);
SamplerState gsamAnisotropicWrap : register(s4);
SamplerState gsamAnisotropicClamp : register(s5);
// cbuffer resource bound to cbuffer register slots 0-2
cbuffer cbPerObject : register(b0)
{
float4x4 gWorld;
float4x4 gTexTransform;
};
// Constant data that varies per material.
cbuffer cbPass : register(b1)
{
float4x4 gView;
float4x4 gProj;
[…] // Other fields omitted for brevity.
};
cbuffer cbMaterial : register(b2)
{
float4 gDiffuseAlbedo;
float3 gFresnelR0;
float gRoughness;
float4x4 gMatTransform;
};
root signature定义了在执行draw调用之前,应用程序将绑定到渲染管线的资源,以及将这些资源映射到着色器输入寄存器的位置。root signature必须与它将与之一起使用的着色器兼容(在执行draw调用之前,root signature必须提供着色器期望绑定到渲染管线的所有资源)。这将在pipeline state object(PSO)创建时进行验证(6.9)。不同的draw call可能使用不同的着色程序集,这将需要不同的root signature。
如果我们认为着色器程序是一个函数,着色器期望的输入资源是函数参数,那么root signature可以被认为是定义了一个函数签名(因此命名为root signature)。通过绑定不同的资源作为参数,着色器输出将会不同。例如一个顶点着色器将取决于实际的顶点被输入到着色器,以及绑定的资源。
root signature在Direct3D中由ID3D12RootSignature接口表示。它是由一组根参数定义的,这些根参数描述了一次draw call着色器期望的资源。根参数可以是根常量、根descriptor或descriptors table。我们将在下一章讨论根常量和根descriptor;在本章中,我们将只使用descriptors table。descriptors table指定descriptors heap中连续的descriptors范围。
下面的代码创建一个root signature,它有个根参数是一个descriptors table,大到足以存储一个CBV(constant buffer view):
// Root parameter can be a table, root descriptor or root constants.
CD3DX12_ROOT_PARAMETER slotRootParameter[1];
// Create a single descriptor table of CBVs.
CD3DX12_DESCRIPTOR_RANGE cbvTable;
cbvTable.Init(
D3D12_DESCRIPTOR_RANGE_TYPE_CBV,
1, // Number of descriptors in table
0);// base shader register arguments are bound to for this root parameter
slotRootParameter[0].InitAsDescriptorTable(
1, // Number of ranges
&cbvTable); // Pointer to array of ranges
// A root signature is an array of root parameters.
CD3DX12_ROOT_SIGNATURE_DESC rootSigDesc(1,
slotRootParameter, 0, nullptr,
D3D12_ROOT_SIGNATURE_FLAG_ALLOW_INPUT_ASSEMBLER_INPUT_LAYOUT);
// create a root signature with a single slot which points to a
// descriptor range consisting of a single constant buffer.
ComPtr serializedRootSig = nullptr;
ComPtr errorBlob = nullptr;
HRESULT hr = D3D12SerializeRootSignature(&rootSigDesc,
D3D_ROOT_SIGNATURE_VERSION_1,
serializedRootSig.GetAddressOf(),
errorBlob.GetAddressOf());
ThrowIfFailed(md3dDevice->CreateRootSignature(
0,
serializedRootSig->GetBufferPointer(),
serializedRootSig->GetBufferSize(),
IID_PPV_ARGS(&mRootSignature)));
我们将在下一章详细描述CD3DX12_ROOT_PARAMETER和CD3DX12_DESCRIPTOR_RANGE,但是现在只需要理解这些代码:
CD3DX12_ROOT_PARAMETER slotRootParameter[1];
CD3DX12_DESCRIPTOR_RANGE cbvTable;
cbvTable.Init(
D3D12_DESCRIPTOR_RANGE_TYPE_CBV, // table type
1, // Number of descriptors in table
0);// base shader register arguments are bound to for this root parameter
slotRootParameter[0].InitAsDescriptorTable(
1, // Number of ranges
&cbvTable); // Pointer to array of ranges
创建一个根参数,该参数期望将一个CBV的descriptor table绑定到constant buffer寄存器0(在HLSL代码中的register(b0))。
root signature仅定义应用程序将绑定到渲染管线的资源,它实际上不做任何资源绑定。一旦使用命令列表设置了根签名,我们使用ID3D12GraphicsCommandList::SetGraphicsRootDescriptorTable将描述符表绑定到管道:
void ID3D12GraphicsCommandList::SetGraphicsRootDescriptorTable(
UINT RootParameterIndex,
D3D12_GPU_DESCRIPTOR_HANDLE BaseDescriptor);
下面的代码将root signature和CBV堆设置到命令列表,并设置descriptor table标识要绑定到管道的资源:
mCommandList->SetGraphicsRootSignature(mRootSignature.Get());
ID3D12DescriptorHeap* descriptorHeaps[] = { mCbvHeap.Get() };
mCommandList->SetDescriptorHeaps(_countof(descriptorHeaps), descriptorHeaps);
// Offset the CBV we want to use for this draw call.
CD3DX12_GPU_DESCRIPTOR_HANDLE cbv(mCbvHeap->GetGPUDescriptorHandleForHeapStart());
cbv.Offset(cbvIndex, mCbvSrvUavDescriptorSize);
mCommandList->SetGraphicsRootDescriptorTable(0, cbv);
为了提高性能,尽可能使root signature小一些,并尽量减少在每帧中更改root signature的次数。
应用程序绑定的root signature(descriptor tables、根常量和根descriptor)的内容在draw/dispatch调用之间更改任何内容时,D3D12驱动程序会自动进行版本控制。因此,每个draw/dispatch都获得一组惟一的root signature状态。
更改根签名会丢失所有现有绑定。也就是说,需要将所有资源重新绑定到新根签名期望的管线中。
在Direct3D中,着色程序必须首先编译成可移植的字节码。然后图形驱动程序将这个字节码再次编译成GPU最理想的本地指令 [ATI1]。在运行时我们可以用以下函数编译一个着色器:
HRESULT D3DCompileFromFile(
LPCWSTR pFileName,
const D3D_SHADER_MACRO *pDefines,
ID3DInclude *pInclude,
LPCSTR pEntrypoint,
LPCSTR pTarget,
UINT Flags1,
UINT Flags2,
ID3DBlob **ppCode,
ID3DBlob **ppErrorMsgs);
ID3DBlob类型只是一个普通的内存块,它有两个方法:
为了支持错误输出,我们在d3dutier .h/.cpp中实现以下函数来在运行时编译着色器:
ComPtr d3dUtil::CompileShader(
const std::wstring& filename,
const D3D_SHADER_MACRO* defines,
const std::string& entrypoint,
const std::string& target)
{
// Use debug flags in debug mode.
UINT compileFlags = 0;
#if defined(DEBUG) || defined(_DEBUG)
compileFlags = D3DCOMPILE_DEBUG | D3DCOMPILE_SKIP_OPTIMIZATION;
#endif
HRESULT hr = S_OK;
ComPtr byteCode = nullptr;
ComPtr errors;
hr = D3DCompileFromFile(filename.c_str(), defines,
D3D_COMPILE_STANDARD_FILE_INCLUDE,
entrypoint.c_str(), target.c_str(), compileFlags,
0, &byteCode, &errors);
// Output errors to debug window.
if(errors != nullptr)
OutputDebugStringA((char*)errors->GetBufferPointer());
ThrowIfFailed(hr);
return byteCode;
}
下面是调用它的一个例子:
ComPtr mvsByteCode = nullptr;
ComPtr mpsByteCode = nullptr;
mvsByteCode = d3dUtil::CompileShader(L”Shaders\color.hlsl”, nullptr, “VS”, “vs_5_0”);
mpsByteCode = d3dUtil::CompileShader(L”Shaders\color.hlsl”, nullptr, “PS”, “ps_5_0”);
HLSL的错误和警告会通过ppErrorMsgs参数返回。比如如果我们拼错了mul函数,那么我们会得到下面的错误输出:
Shaders\color.hlsl(29,14-55): error X3004: undeclared identifier ‘mu’
并非一定要将shader资源绑定到渲染管线才能编译一个着色器,我们将在6.9中看到如何做到这一点。
与在运行时编译着色器不同,我们可以在单独的步骤(例如一个构建步骤,或作为资产内容管线过程的一部分)中离线编译它们。这样做的理由如下:
编译的着色器通常使用.cso(compiled shader object)扩展。
为了离线编译着色器,我们使用了DirectX自带的FXC工具。这是一个命令行工具。分别编译在color.hlsl中以VS和PS为入口点的顶点和像素着色器,我们可以这样写调试版本:
fxc "color.hlsl" /Od /Zi /T vs_5_0 /E "VS" /Fo "color_vs.cso" /Fc "color_vs.asm"
fxc "color.hlsl" /Od /Zi /T ps_5_0 /E "PS" /Fo "color_ps.cso" /Fc "color_ps.asm"
发布版本:
fxc "color.hlsl" /T vs_5_0 /E "VS" /Fo "color_vs.cso" /Fc "color_vs.asm"
fxc "color.hlsl" /T ps_5_0 /E "PS" /Fo "color_ps.cso" /Fc "color_ps.asm"
如果你试图编译一个带有语法错误的着色器,FXC将输出错误/警告到命令窗口。例如如果我们在color.hlsl中错误地命名了一个变量:
// Should be gWorldViewProj, not worldViewProj!
vout.PosH = mul(float4(vin.Pos, 1.0f), worldViewProj);
我们得到了从这一个错误而来的相当多的错误(顶部的错误是要修复的关键):
color.hlsl(29,42-54): error X3004: undeclared identifier ‘worldViewProj’
color.hlsl(29,14-55): error X3013: ‘mul’: no matching 2 parameter intrinsic function
color.hlsl(29,14-55): error X3013: Possible intrinsic functions are:
color.hlsl(29,14-55): error X3013: mul(float|half…
在编译时获取错误消息要比在运行时方便得多。
我们已经展示了如何离线编译顶点和像素着色器到.cso文件。因此我们不再需要在运行时做这件事(我们不需要调用D3DCompileFromFile)。但是我们仍然需要从.cso文件加载编译后的shader对象字节码到我们的应用程序中。
ComPtr d3dUtil::LoadBinary(const std::wstring& filename)
{
std::ifstream fin(filename, std::ios::binary);
fin.seekg(0, std::ios_base::end);
std::ifstream::pos_type size = (int)fin.tellg();
fin.seekg(0, std::ios_base::beg);
ComPtr blob;
ThrowIfFailed(D3DCreateBlob(size,
blob.GetAddressOf()));
fin.read((char*)blob->GetBufferPointer(), size);
fin.close();
return blob;
}
…
ComPtr mvsByteCode = d3dUtil::LoadBinary(L”Shaders\color_vs.cso”);
ComPtr mpsByteCode = d3dUtil::LoadBinary(L”Shaders\color_ps.cso”);
FXC的/Fc可选参数生成可移植的汇编代码。不时查看着色器的程序集对于检查着色器指令数量和查看生成的代码类型很有用——有时可能与您期望的很不同。例如如果您的HLSL代码中有一个条件语句,那么您可能希望在汇编代码中有一个分支指令。在可编程gpu的早期,着色器中的分支过去是很昂贵的,因此有时编译器会通过计算两个分支来简化一个条件语句,然后在两个分支之间进行内插来选择正确的答案。下面的代码会给出相同结果:
因此这被展开的方法在没有任何分支的情况下给出了相同的结果,但是如果不查看汇编代码我们就不知道是否展开了还是生成了真正的分支指令。重点是有时你想看看程序集它到底发生了什么。下面是为顶点着色器生成的程序集的一个例子:
//
// Generated by Microsoft (R) HLSL Shader Compiler 6.4.9844.0
//
//
// Buffer Definitions:
//
// cbuffer cbPerObject
// {
//
// float4x4 gWorldViewProj; // Offset: 0 Size: 64
//
// }
//
//
// Resource Bindings:
//
// Name Type Format Dim Slot Elements
// –––––––––– –––- ––- –––— –- –––
// cbPerObject cbuffer NA NA 0 1
//
//
//
// Input signature:
//
// Name Index Mask Register SysValue Format Used
// ––––––— –— –– ––— ––— ––- ––
// POSITION 0 xyz 0 NONE float xyz
// COLOR 0 xyzw 1 NONE float xyzw
//
//
// Output signature:
//
// Name Index Mask Register SysValue Format Used
// ––––––— –— –– ––— ––— ––- ––
// SV_POSITION 0 xyzw 0 POS float xyzw
// COLOR 0 xyzw 1 NONE float xyzw
//
vs_5_0
dcl_globalFlags refactoringAllowed | skipOptimization
dcl_constantbuffer cb0[4], immediateIndexed
dcl_input v0.xyz
dcl_input v1.xyzw
dcl_output_siv o0.xyzw, position
dcl_output o1.xyzw
dcl_temps 2
//
// Initial variable locations:
// v0.x <- vin.PosL.x; v0.y <- vin.PosL.y; v0.z <- vin.PosL.z;
// v1.x <- vin.Color.x; v1.y <- vin.Color.y; v1.z <- vin.Color.z; v1.w <- vin.Color.w;
// o1.x <- .Color.x;
// o1.y <- .Color.y;
// o1.z <- .Color.z;
// o1.w <- .Color.w;
// o0.x <- .PosH.x;
// o0.y <- .PosH.y;
// o0.z <- .PosH.z;
// o0.w <- .PosH.w
//
#line 29 “color.hlsl”
mov r0.xyz, v0.xyzx
mov r0.w, l(1.000000)
dp4 r1.x, r0.xyzw, cb0[0].xyzw // r1.x <- vout.PosH.x
dp4 r1.y, r0.xyzw, cb0[1].xyzw // r1.y <- vout.PosH.y
dp4 r1.z, r0.xyzw, cb0[2].xyzw // r1.z <- vout.PosH.z
dp4 r1.w, r0.xyzw, cb0[3].xyzw // r1.w <- vout.PosH.w
#line 32
mov r0.xyzw, v1.xyzw // r0.x <- vout.Color.x; r0.y <- vout.Color.y;
// r0.z <- vout.Color.z; r0.w <- vout.Color.w
mov o0.xyzw, r1.xyzw
mov o1.xyzw, r0.xyzw
ret
// Approximately 10 instruction slots used
Visual Studio 2013为编译着色器程序提供了一些集成支持。您可以将.hlsl文件添加到项目中,Visual Studio (VS)将识别它们并提供编译选项(见下图)。这些选项为FXC参数提供了一个UI。当您将HLSL文件添加到VS项目时,它将成为构建过程的一部分,并且着色器将使用FXC编译。
使用VS集成HLSL支持的一个缺点是每个文件只支持一个着色器程序,你不能在一个文件中同时存储顶点和像素着色器。此外,有时我们希望用不同的预处理器指令编译相同的着色器程序,以获得不同的着色器变体,同样集成VS也不支持,因为它是每个.hlsl输出一个.cso。
虽然渲染管道的许多部分是可编程的,但有些部分是只能配置的。光栅化状态组由D3D12_RASTERIZER_DESC结构表示,用于配置渲染管道的光栅化阶段:
typedef struct D3D12_RASTERIZER_DESC {
D3D12_FILL_MODE FillMode; // Default: D3D12_FILL_SOLID
D3D12_CULL_MODE CullMode; // Default: D3D12_CULL_BACK
BOOL FrontCounterClockwise; // Default: false
INT DepthBias; // Default: 0
FLOAT DepthBiasClamp; // Default: 0.0f
FLOAT SlopeScaledDepthBias; // Default: 0.0f
BOOL DepthClipEnable; // Default: true
BOOL ScissorEnable; // Default: false
BOOL MultisampleEnable; // Default: false
BOOL AntialiasedLineEnable; // Default: false
UINT ForcedSampleCount; // Default: 0
// Default: D3D12_CONSERVATIVE_RASTERIZATION_MODE_OFF
D3D12_CONSERVATIVE_RASTERIZATION_MODE ConservativeRaster;
} D3D12_RASTERIZER_DESC;
这些成员大多数是高级特性或不经常使用。建议参考SDK文档了解每个成员的描述。这里只描述四个:
下面的代码展示了如何创建一个光栅化状态,打开线框图模式,并禁用背面剔除:
CD3DX12_RASTERIZER_DESC rsDesc(D3D12_DEFAULT);
rsDesc.FillMode = D3D12_FILL_WIREFRAME;
rsDesc.CullMode = D3D12_CULL_NONE;
CD3DX12_RASTERIZER_DESC是一个方便的类,它扩展了D3D12_RASTERIZER_DESC并添加了一些辅助构造函数。特别是有一个接受类型为CD3D12_DEFAULT的对象的构造函数,这只是一个用于重载的虚拟类型,用于指示光栅化状态成员应该初始化为默认值。CD3D12_DEFAULT和D3D12_DEFAULT的定义如下:
struct CD3D12_DEFAULT {};
extern const DECLSPEC_SELECTANY CD3D12_DEFAULT D3D12_DEFAULT;
D3D12_DEFAULT用于Direct3D的几个便利类中。
例如我们已经展示了如何描述input layout description,如何创建顶点和像素着色器,以及如何配置光栅化状态组。但是,我们还没有展示如何将这些对象绑定到图形管线以供实际使用。大多数控制图形管线状态的对象被指定为pipeline state object(PSO)的聚合,它由ID3D12PipelineState接口表示。为了创建一个PSO,我们首先通过填写一个D3D12_GRAPHICS_PIPELINE_STATE_DESC实例来描述它:
typedef struct D3D12_GRAPHICS_PIPELINE_STATE_DESC
{
ID3D12RootSignature *pRootSignature;
D3D12_SHADER_BYTECODE VS;
D3D12_SHADER_BYTECODE PS;
D3D12_SHADER_BYTECODE DS;
D3D12_SHADER_BYTECODE HS;
D3D12_SHADER_BYTECODE GS;
D3D12_STREAM_OUTPUT_DESC StreamOutput;
D3D12_BLEND_DESC BlendState;
UINT SampleMask;
D3D12_RASTERIZER_DESC RasterizerState;
D3D12_DEPTH_STENCIL_DESC DepthStencilState;
D3D12_INPUT_LAYOUT_DESC InputLayout;
D3D12_PRIMITIVE_TOPOLOGY_TYPE PrimitiveTopologyType;
UINT NumRenderTargets;
DXGI_FORMAT RTVFormats[8];
DXGI_FORMAT DSVFormat;
DXGI_SAMPLE_DESC SampleDesc;
} D3D12_GRAPHICS_PIPELINE_STATE_DESC;
typedef struct D3D12_SHADER_BYTECODE {
const BYTE *pShaderBytecode;
SIZE_T BytecodeLength;
} D3D12_SHADER_BYTECODE;
typedef struct D3D12_INPUT_LAYOUT_DESC
{
const D3D12_INPUT_ELEMENT_DESC *pInputElementDescs;
UINT NumElements;
} D3D12_INPUT_LAYOUT_DESC;
typedef enum D3D12_PRIMITIVE_TOPOLOGY_TYPE {
D3D12_PRIMITIVE_TOPOLOGY_TYPE_UNDEFINED = 0,
D3D12_PRIMITIVE_TOPOLOGY_TYPE_POINT = 1,
D3D12_PRIMITIVE_TOPOLOGY_TYPE_LINE = 2,
D3D12_PRIMITIVE_TOPOLOGY_TYPE_TRIANGLE = 3,
D3D12_PRIMITIVE_TOPOLOGY_TYPE_PATCH = 4
} D3D12_PRIMITIVE_TOPOLOGY_TYPE;
171/5000
在填写了D3D12_GRAPHICS_PIPELINE_STATE_DESC实例之后,我们使用ID3D12Device::CreateGraphicsPipelineState方法创建一个ID3D12PipelineState对象:
ComPtr mRootSignature;
std::vector mInputLayout;
ComPtr mvsByteCode;
ComPtr mpsByteCode;
…
D3D12_GRAPHICS_PIPELINE_STATE_DESC psoDesc;
ZeroMemory(&psoDesc, sizeof(D3D12_GRAPHICS_PIPELINE_STATE_DESC));
psoDesc.InputLayout = { mInputLayout.data(), (UINT)mInputLayout.size() };
psoDesc.pRootSignature = mRootSignature.Get();
psoDesc.VS =
{
reinterpret_cast(mvsByteCode->GetBufferPointer()),
mvsByteCode->GetBufferSize()
};
psoDesc.PS =
{
reinterpret_cast(mpsByteCode->GetBufferPointer()),
mpsByteCode->GetBufferSize()
};
psoDesc.RasterizerState = CD3D12_RASTERIZER_DESC(D3D12_DEFAULT);
psoDesc.BlendState = CD3D12_BLEND_DESC(D3D12_DEFAULT);
psoDesc.DepthStencilState = CD3D12_DEPTH_STENCIL_DESC(D3D12_DEFAULT);
psoDesc.SampleMask = UINT_MAX;
psoDesc.PrimitiveTopologyType = D3D12_PRIMITIVE_TOPOLOGY_TYPE_TRIANGLE;
psoDesc.NumRenderTargets = 1;
psoDesc.RTVFormats[0] = mBackBufferFormat;
psoDesc.SampleDesc.Count = m4xMsaaState ? 4 : 1;
psoDesc.SampleDesc.Quality = m4xMsaaState ? (m4xMsaaQuality - 1) : 0;
psoDesc.DSVFormat = mDepthStencilFormat;
ComPtr mPSO;
md3dDevice->CreateGraphicsPipelineState(&psoDesc, IID_PPV_ARGS(&mPSO)));
相当多的状态汇聚在一个ID3D12PipelineState对象。为了提高性能我们将所有这些对象指定为图形管线的集合。通过将它们指定为集合,Direct3D可以验证所有状态是否兼容,并且驱动程序可以预先生成所有代码来编写硬件状态。在Direct3D 11状态模型中,这些渲染状态块是分别设置的。然而这些状态是相关的,如果一个状态改变了,可能需要驱动程序为另一个依赖它的状态重新编写硬件程序。由于要更改许多状态来配置管线,所以硬件状态会进行冗余重编程。为了避免这种冗余,驱动程序通常会延迟对硬件状态的编程,直到发出一个draw call,此时整个管道状态都是已知的。但是这种延迟需要驱动程序在运行时进行额外的记帐工作,它需要跟踪哪些状态已经更改,然后生成在运行时对硬件状态进行编程的代码。在新的Direct3D 12模型中,驱动程序可以在初始化时生成编写管道状态所需的所有代码,因为我们将大多数管道状态指定为一个集合。
由于PSO的验证和创建非常耗时,所以应该在初始化时生成PSO。例外情况可能是在运行时根据需要在第一次引用PSO时创建PSO,然后将其存储在hash表之类的集合中,以便能够快速获取以供将来使用。
并不是所有的渲染状态都封装在PSO中。像viewport和scissor矩形这样的状态是独立于PSO指定的。这些状态可以有效地独立于其他管道状态进行设置,所以将它们包含在PSO中没有任何优势。
// Reset specifies initial PSO.
mCommandList->Reset(mDirectCmdListAlloc.Get(), mPSO1.Get())
/* …draw objects using PSO 1… */
// Change PSO
mCommandList->SetPipelineState(mPSO2.Get());
/* …draw objects using PSO 2… */
// Change PSO
mCommandList->SetPipelineState(mPSO3.Get());
/* …draw objects using PSO 3… */
换句话说,当一个PSO被绑定到命令列表后,直到覆盖它(或者重置命令列表)前它都不会改变。
为了提高性能,PSO应该保持最小状态的变化。将所有可以使用相同PSO的对象聚集在一起。不要每一次draw call就改变PSO!
创建一个将vertex buffer和index buffer分组在一起以定义一组几何图形的结构是很有帮助的。这个结构可以保持顶点和索引数据的系统内存支持,以便CPU可以读取这些数据。CPU会需要访问几何数据来做诸如拾取和碰撞检测之类的事情。此外,该结构缓存vertex buffer和index buffer的重要属性(如格式和步长),并提供将views返回给缓冲区的方法。我们在整本书中每当我们定义一个几何块,将使用MeshGeometry(在d3dUtil.h中定义)结构。
// Defines a subrange of geometry in a MeshGeometry. This is for when
// multiple geometries are stored in one vertex and index buffer. It
// provides the offsets and data needed to draw a subset of geometry
// stores in the vertex and index buffers so that we can implement the
// technique described by Figure 6.3.
struct SubmeshGeometry
{
UINT IndexCount = 0;
UINT StartIndexLocation = 0;
INT BaseVertexLocation = 0;
// Bounding box of the geometry defined by this submesh.
// This is used in later chapters of the book.
DirectX::BoundingBox Bounds;
};
struct MeshGeometry
{
// Give it a name so we can look it up by name.
std::string Name;
// System memory copies. Use Blobs because the vertex/index format can
// be generic.
// It is up to the client to cast appropriately.
Microsoft::WRL::ComPtr VertexBufferCPU = nullptr;
Microsoft::WRL::ComPtr IndexBufferCPU = nullptr;
Microsoft::WRL::ComPtr VertexBufferGPU = nullptr;
Microsoft::WRL::ComPtr IndexBufferGPU = nullptr;
Microsoft::WRL::ComPtr VertexBufferUploader = nullptr;
Microsoft::WRL::ComPtr IndexBufferUploader = nullptr;
// Data about the buffers.
UINT VertexByteStride = 0;
UINT VertexBufferByteSize = 0;
DXGI_FORMAT IndexFormat = DXGI_FORMAT_R16_UINT;
UINT IndexBufferByteSize = 0;
// A MeshGeometry may store multiple geometries in one vertex/index
// buffer.
// Use this container to define the Submesh geometries so we can draw
// the Submeshes individually.
std::unordered_map DrawArgs;
D3D12_VERTEX_BUFFER_VIEW VertexBufferView()const
{
D3D12_VERTEX_BUFFER_VIEW vbv;
vbv.BufferLocation = VertexBufferGPU->GetGPUVirtualAddress();
vbv.StrideInBytes = VertexByteStride;
vbv.SizeInBytes = VertexBufferByteSize;
return vbv;
}
D3D12_INDEX_BUFFER_VIEW IndexBufferView()const
{
D3D12_INDEX_BUFFER_VIEW ibv;
ibv.BufferLocation = IndexBufferGPU->GetGPUVirtualAddress();
ibv.Format = IndexFormat;
ibv.SizeInBytes = IndexBufferByteSize;
return ibv;
}
// We can free this memory after we finish upload to the GPU.
void DisposeUploaders()
{
VertexBufferUploader = nullptr;
IndexBufferUploader = nullptr;
}
};
详见官方案例。
第2题:使用两个vertex buffers(和两个输入槽)来为管道提供顶点,一个用于存储位置元素,另一个用于存储颜色元素。为此将使用两个顶点结构来存储分割数据:
struct VPosData
{
XMFLOAT3 Pos;
};
struct VColorData
{
XMFLOAT4 Color;
};
D3D12_INPUT_ELEMENT_DESC数组变为这样:
D3D12_INPUT_ELEMENT_DESC vertexDesc[] =
{
{“POSITION”, 0, DXGI_FORMAT_R32G32B32_FLOAT, 0, 0, D3D12_INPUT_PER_VERTEX_DATA, 0},
{“COLOR”, 0, DXGI_FORMAT_R32G32B32A32_FLOAT, 1, 0, D3D12_INPUT_PER_VERTEX_DATA, 0}
};
position元素连接到输入槽0,color元素连接到输入槽1,并因此D3D12_INPUT_ELEMENT_DESC::AlignedByteOffset两个元素都为0。然后使用ID3D12CommandList::IASetVertexBuffers将两个顶点缓冲区绑定到槽0和槽1上。Direct3D使用来自不同输入槽的元素来组装顶点来优化,需要较少的vertex buffers使用少的输入槽,从而节省数据带宽。为了提高性能,建议将输入槽的数量最小化到小于或等于3的小数量。
对于题目一种办法是可以建多个MeshGeometry分别存不同的vertex buffer,绘制时IASetVertexBuffers把不同的vertex buffer绑定到不同的输入槽。