龙书第7章开篇,即是介绍如何通过帧资源和渲染项来优化程序,但是这几部分因为涉及多物体及多帧绘制,开始就封装那么多东西,会不太好理解。所以我尝试先将这两块封装放一边,而剥离并实现另一块核心代码“设置多缓冲区”,并且还是单物体绘制。搞明白这些内容,再在这基础上去加上帧资源和渲染项的代码,并在最后封装。我觉得,这样才是学习理解的最佳途径。
之前的案例中,我们只使用了一个常量缓冲区,所以CBV数量也是一个。但是在实际绘制中,我们会基于资源的更新频率对常量数据进行分组。比如说一个静态物体,它的世界矩阵只需设置一次即可,但是观察矩阵和投影矩阵会在改变摄像机或者改变窗口时发生变化,所以需要多次更新,这样我们就可以将世界矩阵和观察投影矩阵分开存储在两个常量缓冲区中,来进行优化。所以我们这次案例效果还是之前的一个立方体,但是会将常量缓冲区设置成2个。
1.拆分常量数据结构体
首先将原来的worldViewProj矩阵拆开,分别置入两个常量数据结构体中。
struct ObjectConstants
{
XMFLOAT4X4 world = MathHelper::Identity4x4();
};
struct PassConstants
{
XMFLOAT4X4 viewProj = MathHelper::Identity4x4();
};
相应的也要声明两个常量资源上传堆。
std::unique_ptr<UploadBufferResource<ObjectConstants>> objCB = nullptr;
std::unique_ptr<UploadBufferResource<PassConstants>> passCB = nullptr;
2.创建CBV
虽然我们还是绘制一个几何体,但是因为现在有2个常量数据结构体,所以我们就要设置2个常量缓冲区,同时也要创建2个CBV。当然了,创建CBV前还是要先创建CBV堆。注意:我们要将堆中的描述符数量设置成2。
//创建CBV堆
D3D12_DESCRIPTOR_HEAP_DESC cbHeapDesc;
cbHeapDesc.Type = D3D12_DESCRIPTOR_HEAP_TYPE_CBV_SRV_UAV;
cbHeapDesc.Flags = D3D12_DESCRIPTOR_HEAP_FLAG_SHADER_VISIBLE;
cbHeapDesc.NumDescriptors = 2; //此处一个堆中包含2个CBV
cbHeapDesc.NodeMask = 0;
ThrowIfFailed(d3dDevice->CreateDescriptorHeap(&cbHeapDesc, IID_PPV_ARGS(&cbvHeap)));
接下来创建第一个CBV,也就是ObjectConstants的CBV。这里的地址计算貌似很复杂,其实是分了两部分。第一部分是计算了“子物体在常量缓冲区中的地址”,即objCB_Address,它是通过子物体的数量和常量数据大小来做地址偏移计算的,当前我们没有子物体,所以这里的地址就是GetGPUVirtualAddress()函数返回的首地址。第二部分是计算了“CBV元素在CBV堆中的地址”,即handle,句柄我们可以暂时理解成指针,它是通过元素索引和cbvSrvUav类型数据大小来计算得到地址偏移的(句柄的地址偏移是通过Offset函数来实现的)。通过上面两个地址,我们就创建出了第一个CBV。从代码中可以很清晰的看到,CBV和常量数据是一一对应的。
objCB = std::make_unique<UploadBufferResource<ObjectConstants>>(d3dDevice.Get(), 1, true);
//获得常量缓冲区首地址
D3D12_GPU_VIRTUAL_ADDRESS objCB_Address;
objCB_Address = objCB->Resource()->GetGPUVirtualAddress();
int objCbElementIndex = 0; //常量缓冲区子物体个数(子缓冲区个数)下标
objCB_Address += objCbElementIndex * objConstSize;//子物体在常量缓冲区中的地址
int heapIndex = 0; //CBV堆中的CBV元素索引
auto handle = CD3DX12_CPU_DESCRIPTOR_HANDLE(cbvHeap->GetCPUDescriptorHandleForHeapStart());//获得CBV堆首地址
handle.Offset(heapIndex, cbv_srv_uavDescriptorSize); //CBV句柄(CBV堆中的CBV元素地址)
//创建CBV描述符
D3D12_CONSTANT_BUFFER_VIEW_DESC cbvDesc0;
cbvDesc0.BufferLocation = objCB_Address;
cbvDesc0.SizeInBytes = objConstSize;
d3dDevice->CreateConstantBufferView(&cbvDesc0, handle);
同理,我们创建第二个CBV,也就是PassConstants的CBV。可以看到我们的heapIndex此时是1,因为是第二个CBV了。
passCB = std::make_unique<UploadBufferResource<PassConstants>>(d3dDevice.Get(), 1, true);
//获得常量缓冲区首地址
D3D12_GPU_VIRTUAL_ADDRESS passCB_Address;
passCB_Address = passCB->Resource()->GetGPUVirtualAddress();
int passCbElementIndex = 0;
passCB_Address += passCbElementIndex * passConstSize;
heapIndex = 1;
handle = CD3DX12_CPU_DESCRIPTOR_HANDLE(cbvHeap->GetCPUDescriptorHandleForHeapStart());
handle.Offset(heapIndex, cbv_srv_uavDescriptorSize);
//创建CBV描述符
D3D12_CONSTANT_BUFFER_VIEW_DESC cbvDesc1;
cbvDesc1.BufferLocation = passCB_Address;
cbvDesc1.SizeInBytes = passConstSize;
d3dDevice->CreateConstantBufferView(&cbvDesc1, handle);
3.复制常量数据至GPU
接下来我们要用CopyData函数将常量缓冲区中的数据传至GPU,因为现在有2个常量缓冲区,所以要传2次。而在构建世界矩阵的时候,我们乘了一个在世界坐标X方向平移了2个单位的矩阵,即w *= XMMatrixTranslation(2.0f, 0.0f, 0.0f),然后将它传入objConstants缓冲区,观察投影矩阵则传入passConstants缓冲区。
void D3D12InitApp::Update(GameTime& gt)
{
ObjectConstants objConstants;
PassConstants passConstants;
//构建观察矩阵
float x = radius * sinf(phi) * cosf(theta);
float y = radius * cosf(phi);
float z = radius * sinf(phi) * sinf(theta);
/*float y = 0;
float x = 0;
float z = 10;*/
XMVECTOR pos = XMVectorSet(x, y, z, 1.0f);
XMVECTOR target = XMVectorZero();
XMVECTOR up = XMVectorSet(0.0f, 1.0f, 0.0f, 0.0f);
XMMATRIX v = XMMatrixLookAtLH(pos, target, up);
//构建世界矩阵
XMMATRIX w = XMLoadFloat4x4(&world);
w *= XMMatrixTranslation(2.0f, 0.0f, 0.0f);
//拿到投影矩阵
XMMATRIX p = XMLoadFloat4x4(&proj);
//矩阵计算
XMMATRIX VP_Matrix = v * p;
XMStoreFloat4x4(&passConstants.viewProj, XMMatrixTranspose(VP_Matrix));
//passConstants.totalTime = gt.TotalTime();
//passConstants.pulseColor = XMFLOAT4(Colors::Gold);
passCB->CopyData(0, passConstants);
//XMMATRIX赋值给XMFLOAT4X4
XMStoreFloat4x4(&objConstants.world, XMMatrixTranspose(w));
//将数据拷贝至GPU缓存
objCB->CopyData(0, objConstants);
}
4.构建根签名
根签名作用是将常量数据绑定至寄存器槽,供着色器程序访问。因为现在我们有2个常量数据结构体,所以要创建2个元素的根参数,即2个CBV表,并绑定2个寄存器槽。
void D3D12InitApp::BuildRootSignature()
{
//根参数可以是描述符表、根描述符、根常量
CD3DX12_ROOT_PARAMETER slotRootParameter[2];
//创建由单个CBV所组成的描述符表
CD3DX12_DESCRIPTOR_RANGE cbvTable0;
cbvTable0.Init(D3D12_DESCRIPTOR_RANGE_TYPE_CBV, //描述符类型
1, //描述符数量
0);//描述符所绑定的寄存器槽号
CD3DX12_DESCRIPTOR_RANGE cbvTable1;
cbvTable1.Init(D3D12_DESCRIPTOR_RANGE_TYPE_CBV, //描述符类型
1, //描述符数量
1);//描述符所绑定的寄存器槽号
slotRootParameter[0].InitAsDescriptorTable(1, &cbvTable0);
slotRootParameter[1].InitAsDescriptorTable(1, &cbvTable1);
//根签名由一组根参数构成
CD3DX12_ROOT_SIGNATURE_DESC rootSig(2, //根参数的数量
slotRootParameter, //根参数指针
0, nullptr, D3D12_ROOT_SIGNATURE_FLAG_ALLOW_INPUT_ASSEMBLER_INPUT_LAYOUT);
//用单个寄存器槽来创建一个根签名,该槽位指向一个仅含有单个常量缓冲区的描述符区域
ComPtr<ID3DBlob> serializedRootSig = nullptr;
ComPtr<ID3DBlob> errorBlob = nullptr;
HRESULT hr = D3D12SerializeRootSignature(&rootSig, D3D_ROOT_SIGNATURE_VERSION_1, &serializedRootSig, &errorBlob);
if (errorBlob != nullptr)
{
OutputDebugStringA((char*)errorBlob->GetBufferPointer());
}
ThrowIfFailed(hr);
ThrowIfFailed(d3dDevice->CreateRootSignature(0,
serializedRootSig->GetBufferPointer(),
serializedRootSig->GetBufferSize(),
IID_PPV_ARGS(&rootSignature)));
}
5.设置根描述符表
在Draw函数中,我们会使用cmdList->SetGraphicsRootDescriptorTable()函数绑定命令至流水线,而现在我们因为有2个描述符表,所以要绑定两次。注意:根参数的起始索引要和根描述符表的地址(即CBV堆中的元素地址)对应。
//设置根描述符表
int objCbvIndex = 0;
auto handle = CD3DX12_GPU_DESCRIPTOR_HANDLE(cbvHeap->GetGPUDescriptorHandleForHeapStart());
handle.Offset(objCbvIndex, cbv_srv_uavDescriptorSize);
cmdList->SetGraphicsRootDescriptorTable(0, //根参数的起始索引
handle);
int passCbvIndex = 1;
handle = CD3DX12_GPU_DESCRIPTOR_HANDLE(cbvHeap->GetGPUDescriptorHandleForHeapStart());
handle.Offset(passCbvIndex, cbv_srv_uavDescriptorSize);
cmdList->SetGraphicsRootDescriptorTable(1, //根参数的起始索引
handle);
6.着色器程序
在shader程序中,我们会传入两个常量结构体,分别来自寄存器b0和b1,然后通过两次矩阵计算得到裁剪空间坐标(因为在cpu阶段我们将矩阵拆开了,所以在着色器中每个顶点要多计算一次矩阵乘法,但是对于GPU的强大并行能力来说,这并不算什么)。
cbuffer cbPerObject : register(b0)
{
float4x4 gWorld;
};
cbuffer cbPass : register(b1)
{
float4x4 gViewProj;
};
struct VertexIn
{
float3 PosL : POSITION;
float4 Color : COLOR;
};
struct VertexOut
{
float4 PosH : SV_POSITION;
float4 Color : COLOR;
};
VertexOut VS(VertexIn vin)
{
VertexOut vout;
float3 PosW = mul(float4(vin.PosL, 1.0f), gWorld).xyz;
vout.PosH = mul(float4(PosW, 1.0f), gViewProj);
vout.Color = vin.Color;
return vout;
}
float4 PS(VertexOut pin) : SV_Target
{
return pin.Color;
}
编译运行,显示正常,并且立方体在世界坐标的X方向移动了2个单位。说明两个常量缓冲区的值都起作用了。
![05e22a8d27595df9c7683775a01730d1.png](https://i-blog.csdnimg.cn/blog_migrate/01e1d04cb7a2b4880555dc861ceb25b9.png)
转动一下摄像机,因为摄像机的目标点是(0,0),所以可知立方体的世界坐标确实改变了。
![62b3690c0f88245de2f7f9d76b1a48c2.gif](https://i-blog.csdnimg.cn/blog_migrate/16ce9c57b1ab290e46ae0b924ae7f4eb.gif)
有了这篇的铺垫,我们接下来将会绘制多个几何体(没有实例化),并学习渲染项的代码管理,以及帧资源的代码优化,最终绘制出多个几何体实例。