代码工程地址:
https://github.com/jiabaodan/Direct12BookReadingNotes
学习目标
- 熟悉Direct3D接口的定义,保存和绘制几何数据 ;
- 学习编写基本的顶点和像素着色器;
- 学习使用渲染流水线状态对象来配置渲染流水线;
- 理解如何创建常数缓存数据(constant buffer data),并且熟悉根签名?(root signature);
1 顶点和输入布局
下面的代码定义了2类顶点:
struct Vertex1
{
XMFLOAT3 Pos;
XMFLOAT4 Color;
};
struct Vertex2
{
XMFLOAT3 Pos;
XMFLOAT3 Normal;
XMFLOAT2 Tex0;
XMFLOAT2 Tex1;
};
当我们定义完一个顶点结构以后,我们需要提供一个描述来让Direct3D知道每个组件是什么作用,这个描述由Direct3D结构体D3D12_INPUT_LAYOUT_DESC通过输入布局描述(input layout description)的形式提供:
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结构体定义如下:
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:关联到语义上的索引,使相同的语义可以多次使用,以索引区分,如上图;
- Format:由DXGI_FORMAT枚举类型定义的类型,下面是一些常用的值:
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 // 4D 8-bit unsigned integer vector
- InputSlot:定义元素传进来的输入槽,Direct3D支持16个输入槽(0~15);
- AlignedByteOffset:每个元素的偏移量,单位是字节:
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_PER_VERTEX_DATA,其他值用以实例化技术;
- InstanceDataStepRate:目前暂时都设置为0,其他值用以实例化技术;
对于之前描述的两个顶点结构,其对应的输入布局描述如下:
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}
};
2 顶点缓冲(VERTEX BUFFERS)
为了让GPU访问到顶点数组,我们需要将它们保存在一个叫缓冲(buffer)的GPU资源中(ID3D12Resource),用来保存顶点数据的缓冲叫做顶点缓冲。
如之前4.3.8,我们通过填写一个D3D12_RESOURCE_DESC结构体,创建一个ID3D12Resource对象来描述缓冲资源,然后调用ID3D12Device::CreateCommittedResource方法。Direct3D 12提供了一个C++封装的类CD3DX12_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 );
}
with代表了缓冲中的字节数。
对于静态的几何体,我们将顶点缓冲放到默认堆中(default heap)((D3D12_HEAP_TYPE_DEFAULT)用以优化性能;为了创建实际的顶点缓冲资源,我们需要创建一个类型为D3D12_HEAP_TYPE_UPLOAD的上传缓冲(upload buffer)资源。
因为中间的上传缓冲需要在默认缓冲(default buffer)中初始化,所以我们在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.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.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:对于缓冲来说,是我们要复制的数据的大小;
下面的代码展示了该类使用的一个例子:
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);
顶点的定义如下:
struct Vertex
{
XMFLOAT3 Pos;
XMFLOAT4 Color;
};
为了绑定顶点缓冲到渲染管线,我们需要创建一个顶点缓冲描述(vertex buffer view)。和RTV(render target view)不同,我们不需要为顶点缓冲描述创建描述堆(descriptor heap),它可以通过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);
- StartSlot:输入槽的序号(0~15);
- NumBuffers:需要绑定的顶点缓冲的数量,如果开始序号是k,要绑定n个,那么绑定的序号为k,k+1…;
- 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);
一个顶点缓冲将会保持在输入的槽,知道它被改变,所以你的代码应该类似下面:
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参数定义了绘制那些顶点:
DrawInstanced方法中并没有指明拓扑结构,它由下面的方法中指定:
cmdList->IASetPrimitiveTopology(D3D_PRIMITIVE_TOPOLOGY_TRIANGLELIST);
3 索引和索引缓冲(INDEX BUFFERS)
和顶点一样,为了让GPU能访问到索引数据,我们需要创建一个索引缓冲和上传缓冲(Upload Buffer)。因为d3dUtil::CreateDefaultBuffer函数的数据是void*类型,所以可以直接使用它;为了绑定索引缓冲到渲染流水线,我们需要创建一个缓冲描述,和顶点一样,不需要描述堆;一个索引缓冲描述可以使用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:DXGI_FORMAT_R16_UINT或者DXGI_FORMAT_R32_UINT,正常情况下使用16位来减少内存和带宽,除非真的有需要使用32位;
和顶点一样,使用前需要绑定到流水线,可以使用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<ID3D12Resource> IndexBufferGPU = nullptr;
ComPtr<ID3D12Resource> IndexBufferUploader = nullptr;
IndexBufferGPU = d3dUtil::CreateDefaultBuffer(md3dDevice.Get(),
mCommandList.Get(), indices), ibByteSize,
Index