绘制
Note
写在前面:
对于后续提到的数据,尽管c++支持在类体中直接初始化,但直接在类体初始化会导致阅读和管理困难,且DX12项目中的初始化往往依赖D3D12提供的接口,在极端情况下可能存在相关接口还没完成初始化的时序问题。
因此,在通常实践中,应该将初始化语法写在具体的方法内,并统一调用。譬如,下文提到的顶点与输入布局描述就可以在BuildPSO方法中初始化。
Note
对于任何代码中创建的任何数据和对象,最初都只存在于CPU中,为了使GPU能够访问和处理这些数据,需要手动将数据送到GPU中,即在初始化渲染器时大段的代码都是在执行”创建数据-创建上传缓冲区和资源缓冲区-创建资源缓冲区视图-拷贝数据到上传缓冲区-从上传缓冲区拷贝到资源缓冲区->销毁上传缓冲区“这一过程。
约定以下命名:
上传缓冲区/上传堆(upload Buffer):CPU无法将资源直接送进GPU用于计算的区域,只能将它暂存在RAM或GPU划分出来用于和CPU通讯的区域内。称这些区域为上传缓冲区/上传堆,是CPU可读写、GPU可读但速度较慢的区域。
资源缓冲区/默认堆(Default Buffer):GPU内部存储资源和计算的区域,按照对应的资源进行进一步的命名划分,如顶点缓冲区、视图缓冲区,其本质是连续内存的二进制数据,需要根据描述符来确定具体的读取方式。
其中,常量缓冲区是例外,由于每帧都有大量的变化,通常直接写在上传缓冲区内。
顶点与输入布局描述
1. 定义顶点结构体,声明顶点中存储的具体数据。譬如,一个包含位置和颜色信息的顶点以如下方式进行定义:
顶点结构体和顶点类型绑定,有多少顶点类型就需要创建多少顶点结构体。
| struct Vertex
{
XMFLOAT3 Pos;
XMFLOAT4 Color;
};
|
2. 为每个顶点结构体创建D3D12_INPUT_ELEMENT_DESC数组,用于描述顶点结构体中每个元素。
以\(1.\)中的Vertex为例,创建D3D12_INPUT_ELEMENT_DESC的语句如下:
| D3D12_INPUT_ELEMENT_DESC elementDescs[] = {
{
"POSITION", // 语义名:必须与 HLSL 中的冒号后名称一致
0, // 语义索引:如果有两个坐标,这里写 0 和 1
DXGI_FORMAT_R32G32B32_FLOAT, // 格式:对应 XMFLOAT3 (3个32位浮点数)
0, // 输入槽:通常设为 0
0, // 偏移量:Pos 是结构体第一个成员,偏移量为 0
D3D12_INPUT_CLASSIFICATION_PER_VERTEX_DATA, // 类别:按顶点数据处理
0 // 实例步进:非实例化渲染设为 0
},
{
"COLOR", // 语义名:对应 HLSL 中的 COLOR
0, // 语义索引
DXGI_FORMAT_R32G32B32A32_FLOAT, // 格式:对应 XMFLOAT4 (4个32位浮点数)
0, // 输入槽:同一个 Buffer,槽位相同
12, // 偏移量:跳过前面的 Pos (3*4=12字节)
D3D12_INPUT_CLASSIFICATION_PER_VERTEX_DATA,
0
}
};
|
D3D12_INPUT_ELEMENT_DESC包含\(7\)个元素,每个元素有固定含义,描述如下:
D3D12_INPUT_ELEMENT_DESC结构体定义及含义
| typedef struct D3D12_INPUT_ELEMENT_DESC {
LPCSTR SemanticName; // 1. 语义名
UINT SemanticIndex; // 2. 语义索引
DXGI_FORMAT Format; // 3. 数据格式
UINT InputSlot; // 4. 输入槽
UINT AlignedByteOffset; // 5. 对齐字节偏移
D3D12_INPUT_CLASSIFICATION InputSlotClass; // 6. 输入分类
UINT InstanceDataStepRate; // 7. 实例步进速率
} D3D12_INPUT_ELEMENT_DESC;
|
Format:数据格式,即顶点结构体中对应的元素的数据格式,但这里不写在顶点结构体中声明元素时的类型名,应该写DXGI_FORMAT枚举。
InputSlot:输入槽索引,指定传递元素所以所用的输入槽(顶点缓冲区),值范围为0-15,只有一个时默认设为0。
-
InputSlotClass:输入分类,表明数据按顶点还是实例读取,有以下两个选项:
D3D12_INPUT_CLASSIFICATION_PER_VERTEX_DATA:每个顶点读取一次数据。最常用的选项,画普通物体时使用。
D3D12_INPUT_CLASSIFICATION_PER_INSTANCE_DATA:用于实例化渲染。
InstanceDataStepRate:实例步进速率,默认设为0,开启实例化渲染时设为1。
3. 声明D3D12_INPUT_LAYOUT_DESC对象,作为D3D12_INPUT_ELEMENT_DESC的清单,并将D3D12_INPUT_ELEMENT_DESC数组绑定到该对象上。
以\(1.\)和\(2.\)声明的顶点结构体为例,该步骤的语句如下:
| D3D12_INPUT_LAYOUT_DESC inputLayoutDesc; //声明对象
inputLayoutDesc.pInputElementDescs = elementDescs.data(); // 指向描述顶点结构体的D3D12_INPUT_ELEMENT_DESC数组
inputLayoutDesc.NumElements = _countof(elementDescs); // 告诉 GPU 顶点结构体中有两个元素
|
其中,data()方法是标准STL容器提供的方法,返回指向容器首个元素的原始指针
Note
D3D12_INPUT_LAYOUT_DESC对象是一个过程对象,仅用于初始化渲染管线,因此不必将其声明为类的成员。
顶点缓冲区和顶点缓冲区视图
顶点缓冲区视图本质是描述符,但不需要放进描述符堆中。
1. 使用上文中的顶点类型创建顶点数组。
vertices
| Vertex vertieces[]={
{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)}
};
|
2. 根据顶点数组的大小计算缓冲区大小,创建上传缓冲区和顶点缓冲区,创建顶点缓冲区视图,将顶点数组通过上传缓冲区拷贝到顶点缓冲区。
实现代码
| //根据顶点数量计算box对应的顶点缓冲区字节大小
const UINT64 vbByteSize=sizeof(vertieces);
//创建顶点缓冲区和上传缓冲区的指针
ComPtr<ID3D12Resource>VertexBufferGPU=nullptr;
ComPtr<ID3D12Resource>VertexBufferUpload=nullptr;
//创建上传缓冲区和顶点缓冲区,创建顶点缓冲区视图,将顶点数组通过上传缓冲区拷贝到顶点缓冲区。
//该方法由龙书作者进行封装
VertexBufferGPU=d3dUtil::CreateDefaultBuffer(md3dDevice.Get(),mCommandList.Get(),vertieces,vbByteSize,VertexBufferUpload);
|
其中,d3dUtil::CreateDefaultBuffer是由龙书作者封装的工具类,其实现如下:
d3dUtil::CreateDefaultBuffer()
| //方法签名,该方法接收多个参数,返回一个实现了ID3DResource接口的对象
Microsoft::WRL::ComPtr<ID3D12Resource> d3dUtil::CreateDefaultBuffer(
ID3D12Device* device, //Device 原始指针
ID3D12GraphicsCommandList* cmdList, //命令列表 原始指针
const void* initData, //初始化数据的指针,即之后要拷贝到缓冲区的资源。这里需要传指针,如果传的是C风格数组可以不用指针,数组名会自动退化为首元素指针
UINT64 byteSize, //缓冲区的字节大小
Microsoft::WRL::ComPtr<ID3D12Resource>& uploadBuffer) //输出参数,用于存放上传缓冲区的com指针的引用,在调用时传指针的名字即可
{
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), //资源类型:大小为byteSize的缓冲区
D3D12_RESOURCE_STATE_COMMON, //初始化状态为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), //物理位置:CPU上传堆(内存
D3D12_HEAP_FLAG_NONE,
&CD3DX12_RESOURCE_DESC::Buffer(byteSize),
D3D12_RESOURCE_STATE_GENERIC_READ, //初始化状态为GENERIC_READ,意为准备好让CPU拷贝数据到此/GPU读取并拷贝
nullptr,
IID_PPV_ARGS(uploadBuffer.GetAddressOf())));
// Describe the data we want to copy into the default buffer.
//原始数据拷贝到GPU需要被包装成统一的格式,因此需要描述怎么读取被包装后的数据
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));
//拷贝,1是模板类,表示这次拷贝的资源数量
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.
//现在只是完成了录制,而没有实际执行
//不可以销毁上传缓冲区
//返回默认堆的com指针
return defaultBuffer;
}
|
索引和索引缓冲区
和顶点类似,如果需要GPU可以访问索引,就需要索引缓冲区和对应的视图(描述)。索引缓冲区视图同样不需要放进描述符堆。
1. 创建几何体的索引数组,此处以立方体为例
indices
| //定义box的索引数据
std::uint16_t indices[]={
//前表面
0,1,2,
0,2,3,
//后表面
4,6,5,
4,7,6,
//左表面
4,5,1,
4,1,0,
//右表面
3,2,6,
3,6,7,
//上表面
1,5,6,
1,6,2,
//下表面
4,0,3,
4,3,7
};
|
2. 根据顶点数组的大小计算缓冲区大小,创建上传缓冲区和顶点缓冲区,创建顶点缓冲区视图,将顶点数组通过上传缓冲区拷贝到顶点缓冲区。
顶点着色器
DX12使用高级着色器语言(HLSL)编写着色器。
常量缓冲区
常量缓冲区由CPU每帧更新,位于上传堆而非默认堆,大小必须为硬件最小分配空间(256B)的整数倍,多数时候需要多个常量缓冲区。
根签名和描述符表
将根签名称为来源标记会更好理解。
流水线状态对象
Pipeline State Object
帧资源
在通常情况下,CPU必须等待GPU执行完当前帧的命令后才会开始下一帧的计算,以避免提前销毁GPU执行命令队列中的命令所需用到的资源。这使得在每帧内GPU和CPU各自存在一些闲置的时间,造成相当程度的性能浪费。
帧资源是一种有效的解决方案:通过创建数个包含每帧需要使用的管线对象的结构体数组,使CPU在GPU绘制期间可以进行下一帧或数帧的计算。
基本的帧资源结构体至少应该包含以下成员:
- 命令分配器
- 常量缓冲区(全局、对象、材质)
- 围栏值
- 指向各个常量缓冲区的指针
以下为一个基础的帧资源结构体的结构:
FrameResource.h
| #pragma once
#include "stdafx.h"
#include "DXSampleHelper.h"
#include "VertexData.h"
#include "d3dUtil.h"
using Microsoft::WRL::ComPtr;
class FrameResource {
public:
UINT64 m_fenceValue=0;
ComPtr<ID3D12CommandAllocator> m_commandAllocator;
//Constant buffer resource for each frame.
ComPtr<ID3D12Resource> m_passCB; //pass constant buffer
ComPtr<ID3D12Resource> m_objectCB; //object constant buffer
ComPtr<ID3D12Resource> m_materialCB; //material constant buffer
//ptr to mapped data for each frame.
UINT8* mp_passCBMapped = nullptr;
UINT8* mp_objectCBMapped = nullptr;
UINT8* mp_materialCBMapped = nullptr;
public:
FrameResource(ID3D12Device* device,UINT passCBElementNums,UINT objectCBElementNums,UINT materialCBElementNums);
~FrameResource();
private:
FrameResource() = delete;
};
|
FrameResource.cpp
| #include "FrameResource.h"
FrameResource::FrameResource(ID3D12Device* device, UINT passCBElementNums, UINT objectCBElementNums, UINT materialCBElementNums)
{
//Create command allocator for each frame.
ThrowIfFailed(device->CreateCommandAllocator(D3D12_COMMAND_LIST_TYPE_DIRECT, IID_PPV_ARGS(&m_commandAllocator)));
/* todo: constant buffer */
//当前假定所有常量缓冲区中存储的元素都是ObjectConstants
UINT passCBByteSize = d3dUtil::CalcConstantBufferByteSize(sizeof(ObjectConstants)) * passCBElementNums;
UINT objectCBByteSize = d3dUtil::CalcConstantBufferByteSize(sizeof(ObjectConstants)) * objectCBElementNums;
UINT materialCBByteSize = d3dUtil::CalcConstantBufferByteSize(sizeof(ObjectConstants)) * materialCBElementNums;
//Create constant buffer resource for each frame.
//Constant Buffer real is upload heap;
{
auto heapProperties = CD3DX12_HEAP_PROPERTIES(D3D12_HEAP_TYPE_UPLOAD);
auto resourceDesc = CD3DX12_RESOURCE_DESC::Buffer(passCBByteSize);
ThrowIfFailed(device->CreateCommittedResource(
&heapProperties,
D3D12_HEAP_FLAG_NONE,
&resourceDesc,
D3D12_RESOURCE_STATE_GENERIC_READ,
nullptr,
IID_PPV_ARGS(&m_passCB)));
}
{
auto heapProperties = CD3DX12_HEAP_PROPERTIES(D3D12_HEAP_TYPE_UPLOAD);
auto resourceDesc = CD3DX12_RESOURCE_DESC::Buffer(objectCBByteSize);
ThrowIfFailed(device->CreateCommittedResource(
&heapProperties,
D3D12_HEAP_FLAG_NONE,
&resourceDesc,
D3D12_RESOURCE_STATE_GENERIC_READ,
nullptr,
IID_PPV_ARGS(&m_objectCB)));
}
{
auto heapProperties = CD3DX12_HEAP_PROPERTIES(D3D12_HEAP_TYPE_UPLOAD);
auto resourceDesc = CD3DX12_RESOURCE_DESC::Buffer(materialCBByteSize);
ThrowIfFailed(device->CreateCommittedResource(
&heapProperties,
D3D12_HEAP_FLAG_NONE,
&resourceDesc,
D3D12_RESOURCE_STATE_GENERIC_READ,
nullptr,
IID_PPV_ARGS(&m_materialCB)));
}
ThrowIfFailed(m_passCB->Map(0, nullptr, reinterpret_cast<void**>(&mp_passCBMapped)));
ThrowIfFailed(m_objectCB->Map(0, nullptr, reinterpret_cast<void**>(&mp_objectCBMapped)));
ThrowIfFailed(m_materialCB->Map(0, nullptr, reinterpret_cast<void**>(&mp_materialCBMapped)));
}
FrameResource::~FrameResource() {
if (m_passCB) {
m_passCB->Unmap(0, nullptr);
}
if (m_objectCB) {
m_objectCB->Unmap(0, nullptr);
}
if (m_materialCB) {
m_materialCB->Unmap(0, nullptr);
}
}
|
在渲染器中使用帧资源时,应注意在LoadPipeline()方法后、LoadAsset()方法前创建帧资源,这是因为创建命令队列等资源需要使用到命令分配器。
初始化帧资源数组:
| void SampleTriangle::BuildFrameResources() {
//create frame resource and add to vector.
for (int i = 0; i < gNumFrameResources; i++) {
m_frameResources.push_back(std::make_unique<FrameResource>(m_device.Get(), 1, 1, 1));
}
//mark curr frame resource ptr.
m_currFrameResourceIndex = 0;
m_currFrameResource = m_frameResources[m_currFrameResourceIndex].get();
}
|
其中,gNumFrameResources为帧资源数组的元素数量,m_currFrameResourceIndex为当前使用的帧资源在帧资源中的索引,m_currFrameResource为指向当前正在录制的帧资源的指针。
由于需要每帧更换帧资源,因此要在每帧更新当前使用的帧资源及其索引,这些逻辑应该在onUpdate方法中执行而非OnRender,因为它们并不涉及任何GPU相关的渲染。
onUpdate()
| void SampleTriangle::OnUpdate(){
//update curr index and curr ptr.
m_currFrameResourceIndex = (m_currFrameResourceIndex + 1) % gNumFrameResources;
m_currFrameResource = m_frameResources[m_currFrameResourceIndex].get();
//check curr frame resource is still in use by gpu or not.
//if fenceValue==0 ,it is program's first frame and not need wait.
if(m_currFrameResource->m_fenceValue != 0 && m_fence->GetCompletedValue() < m_currFrameResource->m_fenceValue){
ThrowIfFailed(m_fence->SetEventOnCompletion(m_currFrameResource->m_fenceValue, m_fenceEvent));
WaitForSingleObject(m_fenceEvent, INFINITE);
}
//...other logic...
}
|
关于引入帧资源到渲染器中的详细变动见Frame Resource.imgui base · SchlingEvens/UmbraForward@9028c9c,此处略去细节说明。
不同形状的几何体