目录
6.1 VERTICES AND INPUT LAYOUTS
6.4.1 Input Layout Description and Input Signature Linking
6.6.1 Creating Constant Buffers
6.6.2 Updating Constant Buffers
6.6.4 Constant Buffer Descriptors
6.6.5 Root Signature and Descriptor Tables
6.7.3 Using Visual Studio to Compile Shaders Offline
6.10 Geometry Helper Structure
本章回到关注Direct3D API接口和方法,来配置渲染管线、定义顶点和像素着色器以及向渲染管线提交几何图形以供绘制。
6.1 VERTICES AND INPUT LAYOUTS
要创建自定义顶点格式,我们首先创建一个结构来保存我们选择的顶点数据。例如下面举例说明了两种不同的顶点格式:一个由位置和颜色组成,另一个由位置、法向量和两组二维纹理坐标组成:
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;
- SemanticName:要与元素关联的字符串。这可以是任何有效的变量名。语义用于将顶点结构中的元素映射到顶点着色器输入签名中的元素。见下图。
- SemanticIndex:附加到语义上的索引。例如上图,一个顶点结构可能有不止一组纹理坐标。因此,与其引入一个新的语义名称,不如只需在末尾附加一个索引来区分两个纹理坐标集。在着色器代码中没有指定索引的语义默认为索引0。例如上图中POSITION等价于POSITION0。
- Format:DXGI_FORMAT枚举类型的成员,为Direct3D指定顶点元素的格式(数据类型)。以下是一些常用格式的例子:
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}
};
6.2 VERTEX BUFFERS
为了让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<ID3D12Resource> d3dUtil::CreateDefaultBuffer(
ID3D12Device* device,
ID3D12GraphicsCommandList* cmdList,
const void* initData,
UINT64 byteSize,
Microsoft::WRL::ComPtr<ID3D12Resource>& uploadBuffer)
{
ComPtr<ID3D12Resource> 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;
- pData:指向系统内存数组的指针,其中包含要初始化缓冲区的数据。如果缓冲区可以存储n个顶点,那么系统数组必须包含至少n个顶点,才能使整个缓冲区可以初始化。
- RowPitch:对于缓冲区,我们以字节为单位复制的数据的大小。
- SlicePitch:对于缓冲区,我们以字节为单位复制的数据的大小。
下面的代码展示了如何使用这个类来创建一个默认的缓冲区,该缓冲区存储一个立方体的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<ID3D12Resource> VertexBufferGPU = nullptr;
ComPtr<ID3D12Resource> 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;
- BufferLocation:要创建view的vertex buffer资源的虚拟地址。我们可以使用ID3D12Resource::GetGPUVirtualAddress方法来获取这个。
- SizeInBytes:从BufferLocation开始的vertex buffer中要查看的字节数。
- StrideInBytes:以字节为单位的每个顶点元素的大小。
创建vertex buffer并创建view之后,可以将其绑定到管线的input slot,将顶点提供给管道的input assembler阶段。这可以通过以下方法实现:
void ID3D12GraphicsCommandList::IASetVertexBuffers(
UINT StartSlot,
UINT NumBuffers,
const D3D12_VERTEX_BUFFER_VIEW *pViews);
- StartSlot:开始绑定vertex buffer的input slot。从0-15索引有16个input slot。
- NumBuffers:我们绑定到input slot的vertex buffer的数量。
- pViews:指向vertex buffer view数组的第一个元素的指针。
例子如下:
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
D3D