在前几篇博客中,我们主要关注渲染管道的概念和数学方面。 反过来,从本篇博客开始重点介绍配置渲染管道,定义顶点和像素着色器以及将几何图形提交到渲染管道以进行绘制所需的Direct3D API接口和方法。 学习配置渲染管道,对于学习Unity的自定义渲染管线有很大帮助,它们的原理类似的,做到举一反三。
1、掌握用于定义,存储和绘制几何数据的Direct3D接口方法。
2、学习如何编写基本顶点和像素着色器。
3、了解如何使用管道状态对象配置渲染管道。
4、了解如何创建常量缓冲区数据并将其绑定到管道。
struct Vertex1
{
XMFLOAT3 Pos;
XMFLOAT4 Color;
};
struct Vertex2
{
XMFLOAT3 Pos;
XMFLOAT3 Normal;
XMFLOAT2 Tex0;
XMFLOAT2 Tex1;
};
一旦我们定义了顶点结构,需要为Direct3D提供顶点结构的描述,此描述以输入布局描述的形式提供给Direct3D,由D3D12_INPUT_LAYOUT_DESC结构表示:
typedef struct D3D12_INPUT_LAYOUT_DESC
{
const D3D12_INPUT_ELEMENT_DESC *pInputElementDescs;
UINT NumElements;
} D3D12_INPUT_LAYOUT_DESC;
输入布局描述仅仅是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;
看看下图中我们定义的D3D12_INPUT_ELEMENT_DESC与Shader的关系如下所示:
我们在声明数据结构时也要考虑它们在内存中的布局,记得刚毕业时,面试题就有关于这方面的,在这里也是跟读者回顾一下,在下面的顶点结构中,元素Pos具有0字节的偏移,因为它的开始与顶点结构的开始位置是一致的; Normal元素有一个12字节的偏移量,因为我们必须跳过Pos的字节才能达到Normal; 元素Tex0有一个24字节的偏移量,因为我们需要跳过Pos和Normal的字节来到Tex0; 元素Tex1有一个32字节的偏移量,因为我们需要跳过Pos,Normal和Tex0的字节来到Tex1。
struct Vertex2
{
XMFLOAT3 Pos; // 0-byte offset
XMFLOAT3 Normal; // 12-byte offset
XMFLOAT2 Tex0; // 24-byte offset
XMFLOAT2 Tex1; // 32-byte offset
};
相对应的输入描述如下所示:
D3D12_INPUT_ELEMENT_DESC desc1[] =
{
{"POSITION", 0, DXGI_FORMAT_R32G32B32_FLOAT, 0, 0,
D3D12_INPUT_PER_VERTEX_DATA, 0},
{"COLOR", 0, DXGI_FORMAT_R32G32B32A32_FLOAT, 0, 12,
D3D12_INPUT_PER_VERTEX_DATA, 0}
};
D3D12_INPUT_ELEMENT_DESC desc2[] =
{
{"POSITION", 0, DXGI_FORMAT_R32G32B32_FLOAT, 0, 0,
D3D12_INPUT_PER_VERTEX_DATA, 0},
{"NORMAL", 0, DXGI_FORMAT_R32G32B32_FLOAT, 0, 12,
D3D12_INPUT_PER_VERTEX_DATA, 0},
{"TEXCOORD", 0, DXGI_FORMAT_R32G32_FLOAT, 0, 24,
D3D12_INPUT_PER_VERTEX_DATA, 0}
{"TEXCOORD", 1, DXGI_FORMAT_R32G32_FLOAT, 0, 32,
D3D12_INPUT_PER_VERTEX_DATA, 0}
};
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)。
对于静态几何(即,基于每帧不改变的几何),我们将顶点缓冲区放在默认堆(D3D12_HEAP_TYPE_DEFAULT)中以获得最佳性能。 通常,游戏中的大多数几何形状将是这样的(例如,树木,建筑物,地形,角色)。 初始化顶点缓冲区后,只有GPU需要从顶点缓冲区读取以绘制几何图形,因此默认堆是有意义的。 但是,如果CPU无法写入默认堆中的顶点缓冲区,我们如何初始化顶点缓冲区?
除了创建实际的顶点缓冲区资源之外,我们还需要创建堆类型为D3D12_HEAP_TYPE_UPLOAD的中间上传缓冲区资源。 当我们需要将数据从CPU复制到GPU内存时,我们将资源提交到上传堆。 在我们创建上传缓冲区之后,我们将顶点数据从系统内存复制到上传缓冲区,然后我们将顶点数据从上传缓冲区复制到实际的顶点缓冲区。
因为需要一个中间上传缓冲区来初始化默认缓冲区(堆类型为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个顶点,以便可以初始化整个缓冲区。
PowPitch,对于缓冲区,我们以字节为单位复制的数据大小。
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 VertexBufferGPU = nullptr;
ComPtr VertexBufferUploader = nullptr;
VertexBufferGPU = d3dUtil::CreateDefaultBuffer(md3dDevice.Get(),
mCommandList.Get(), vertices, vbByteSize, VertexBufferUploader);
其中Vertex类型和颜色定义如下:
struct Vertex
{
XMFLOAT3 Pos;
XMFLOAT4 Color;
};
为了将顶点缓冲区绑定到管道,我们需要为顶点缓冲区资源创建顶点缓冲区视图。与RTV(渲染目标视图)不同,我们不需要顶点缓冲区视图的描述符堆。 顶点缓冲区视图由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,我们要创建视图的顶点缓冲区资源的虚拟地址, 可以使用ID3D12Resource :: GetGPUVirtualAddress方法来实现这一点。
SizeInBytes,从BufferLocation开始在顶点缓冲区中查看的字节数。
strideInBytes,每个顶点元素的大小,以字节为单位。
在创建顶点缓冲区并创建了一个视图后,我们可以将它绑定到管道的输入槽,以将顶点提供给管道的输入组合程序阶段, 这可以通过以下方法完成:
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方法可能看起来有点复杂,因为它支持将顶点缓冲区数组设置为各种输入槽。 但是,我们只使用一个输入槽。 后面提供了使用两个输入槽的一点经验。
如果使用多个顶点缓冲区,则可以像这样构造代码:
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… */
将顶点缓冲区设置为输入槽不会绘制它们; 它只能使顶点准备好送入管道。 实际绘制顶点的最后一步是使用ID3D12GraphicsCommandList :: DrawInstanced方法完成的:
void ID3D12CommandList::DrawInstanced(
UINT VertexCountPerInstance,
UINT InstanceCount,
UINT StartVertexLocation,
UINT StartInstanceLocation);
VertexCountPerInstance,要绘制的顶点数(每个实例)。
InstanceCount, 用于称为实例化的高级技术; 现在,将其设置为1,因为我们只绘制一个实例。
StartVertexLocation,指定顶点缓冲区中第一个顶点的索引(从零开始)以开始绘制。
StartInstanceLocation, 用于称为实例化的高级技术; 现在,将其设置为0。
以上参数具体也可以参考帮助文档。。。。。。。
VertexCountPerInstance和StartVertexLocation这两个参数定义了要绘制的顶点缓冲区中的一个连续顶点子集;如下图所示:
StartVertexLocation指定顶点缓冲区中第一个顶点的索引(从零开始)以开始绘制。
VertexCountPerInstance指定要绘制的顶点数。
下面再给读者介绍索引和索引缓冲
为了将索引缓冲区绑定到管道,我们需要为索引缓冲区资源创建索引缓冲区视图, 与顶点缓冲区视图一样,我们不需要索引缓冲区视图的描述符堆。 索引缓冲区视图由D3D12_INDEX_BUFFER_VIEW结构表示:
typedef struct D3D12_INDEX_BUFFER_VIEW
{
D3D12_GPU_VIRTUAL_ADDRESS BufferLocation;
UINT SizeInBytes;
DXGI_FORMAT Format;
} D3D12_INDEX_BUFFER_VIEW;
BufferLocation:我们要创建视图的顶点缓冲区资源的虚拟地址, 可以使用ID3D12Resource :: GetGPUVirtualAddress方法来实现它。
SizeInBytes:从BufferLocation开始在索引缓冲区中查看的字节数。
Format:索引的格式,对于16位索引必须是DXGI_FORMAT_R16_UINT,对于32位索引必须是DXGI_FORMAT_R32_UINT。 您应该使用16位索引来减少内存和带宽,如果您的索引值需要额外的32位范围,则只能使用32位索引。
与顶点缓冲区和其他Direct3D资源一样,在我们使用它之前,我们需要将它绑定到管道, 索引缓冲区使用ID3D12CommandList :: SetIndexBuffer方法绑定到输入组合程序阶段。 以下代码显示如何创建定义多维数据集三角形的索引缓冲区,为其创建视图,并将其绑定到管道:
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);
IndexCountPerInstance:要绘制的索引数(每个实例)。
InstanceCount:用于称为实例化的高级技术; 现在,将其设置为1,因为我们只绘制一个实例。
StartIndexLocation:索引缓冲区中元素的索引,用于标记开始读取索引的起始点。
BaseVertexLocation:在获取顶点之前添加到此绘制调用中使用的索引的整数值。
StartInstanceLocation:用于称为实例化的高级技术; 现在,将其设置为0。
为了说明这些参数,假设我们有三个对象:球体,盒子和圆柱体。首先,每个对象都有自己的顶点缓冲区和自己的索引缓冲区,每个本地索引缓冲区中的索引相对于相应的本地顶点缓冲区,现在假设我们将球体,盒子和圆柱体的顶点和索引连接成一个全局顶点和索引缓冲区,如下图所示。 (有人可能会连接顶点和索引缓冲区,因为在更改顶点和索引缓冲区时会有一些API开销。性能没损耗,但如果你有许多小的顶点和索引缓冲区可以很容易地合并,它可能是值得这样做是出于性能原因。)在此连接之后,以前的索引不再正确,因为它们存储相对于其对应的本地顶点缓冲区的索引位置,而不是全局索引位置;因此需要重新计算索引以正确地索引到全局顶点缓冲区中。
以前的存储索引是这样的:
0, 1, …, numBoxVertices-1
合并后的索引是这样的:
firstBoxVertexPos,
firstBoxVertexPos+1,
…,
firstBoxVertexPos+numBoxVertices-1
因此,要更新索引, 让我们调用一个对象的第一个顶点相对于全局顶点缓冲区的位置,它的基本顶点位置。 通常,通过将其基本顶点位置添加到每个索引来计算对象的新索引,Direct3D通过将基本顶点位置传递给DrawIndexedInstanced的第四个参数来实现它,而不必自己计算新索引。
然后我们可以通过以下三个调用逐个绘制球体,盒子和圆柱体:
mCmdList->DrawIndexedInstanced(
numSphereIndices, 1, 0, 0, 0);
mCmdList->DrawIndexedInstanced(
numBoxIndices, 1, firstBoxIndex, firstBoxVertexPos, 0);
mCmdList->DrawIndexedInstanced(
numCylIndices, 1, firstCylIndex, firstCylVertexPos, 0);
在下篇博客中我们继续介绍顶点Shader。。。。。。。