前言
上一章节,我们初步利用渲染管线,显示出一个立方体,这一章节我们将深入。
通过这一章,我们将了解到,如何优化渲染方法,如何绘制多个几何体。
对于本章节,有两个实例程序,第一个程序将讲述如何组织帧资源、渲染项、细化根签名、几何体的生成方式,第二个程序将生成山与水。
程序一
相对于上一章,我们多了如下几个文件:
- GeometryGenerator.h/cpp 用于生成几何体
- FrameResource.h/cpp 帧资源
- ShapesApp.cpp入口文件
本次的初始化包含如下步骤:
bool ShapesApp::Initialize()
{
if(!D3DApp::Initialize())
return false;
ThrowIfFailed(mCommandList->Reset(mDirectCmdListAlloc.Get(), nullptr));
BuildRootSignature();//构建根签名
BuildShadersAndInputLayout();//构建输入布局
BuildShapeGeometry();//构建几何体
BuildRenderItems();//构建渲染项
BuildFrameResources();//构建帧资源
BuildDescriptorHeaps();//构建描述符堆
BuildConstantBufferViews();//构建常量缓冲区
BuildPSOs();//构建管线状态对象
ThrowIfFailed(mCommandList->Close());
ID3D12CommandList* cmdsLists[] = { mCommandList.Get() };
mCommandQueue->ExecuteCommandLists(_countof(cmdsLists), cmdsLists);
FlushCommandQueue();
return true;
}
先看我们的shader:
//基于世界的变换矩阵
cbuffer cbPerObject : register(b0)
{
float4x4 gWorld;
};
//超级项,首次说明于239页,以及超级根签名的理由于271页
cbuffer cbPass : register(b1)
{
float4x4 gView;
float4x4 gInvView;
float4x4 gProj;
float4x4 gInvProj;
float4x4 gViewProj;
float4x4 gInvViewProj;
float3 gEyePosW;
float cbPerObjectPad1;
float2 gRenderTargetSize;
float2 gInvRenderTargetSize;
float gNearZ;
float gFarZ;
float gTotalTime;
float gDeltaTime;
};
struct VertexIn
{
float3 PosL : POSITION;
float4 Color : COLOR;
};
struct VertexOut
{
float4 PosH : SV_POSITION;
float4 Color : COLOR;
};
VertexOut VS(VertexIn vin)
{
VertexOut vout;
float4 posW = mul(float4(vin.PosL, 1.0f), gWorld);
vout.PosH = mul(posW, gViewProj);
vout.Color = vin.Color;
return vout;
}
float4 PS(VertexOut pin) : SV_Target
{
return pin.Color;
}
根签名的创建:
void ShapesApp::BuildRootSignature()
{
//两个常量缓冲对应两个根参数(都是描述符表)
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);
// 根参数
CD3DX12_ROOT_PARAMETER slotRootParameter[2];
slotRootParameter[0].InitAsDescriptorTable(1, &cbvTable0);
slotRootParameter[1].InitAsDescriptorTable(1, &cbvTable1);
// 根签名描述符
CD3DX12_ROOT_SIGNATURE_DESC rootSigDesc(2, slotRootParameter, 0, nullptr,
D3D12_ROOT_SIGNATURE_FLAG_ALLOW_INPUT_ASSEMBLER_INPUT_LAYOUT);
// 序列化根签名
ComPtr<ID3DBlob> serializedRootSig = nullptr;
ComPtr<ID3DBlob> errorBlob = nullptr;
HRESULT hr = D3D12SerializeRootSignature(&rootSigDesc, D3D_ROOT_SIGNATURE_VERSION_1,
serializedRootSig.GetAddressOf(), errorBlob.GetAddressOf());
if(errorBlob != nullptr)
{
::OutputDebugStringA((char*)errorBlob->GetBufferPointer());
}
ThrowIfFailed(hr);
ThrowIfFailed(md3dDevice->CreateRootSignature(
0,
serializedRootSig->GetBufferPointer(),
serializedRootSig->GetBufferSize(),
IID_PPV_ARGS(mRootSignature.GetAddressOf())));
}
编译hlsl,初始化布局
void ShapesApp::BuildShadersAndInputLayout()
{
mShaders["standardVS"] = d3dUtil::CompileShader(L"Shaders\\color.hlsl", nullptr, "VS", "vs_5_1");
mShaders["opaquePS"] = d3dUtil::CompileShader(L"Shaders\\color.hlsl", nullptr, "PS", "ps_5_1");
mInputLayout =
{
{ "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 },
};
}
生成几何体数据,在243页,分别对圆柱、球体、盒子、网格的生成方法、细分逼近方法有介绍。
顶点分别了存储位置、法向量、UV坐标、切线空间T轴,代码在书中或github找,我暂时就不贴了。
void ShapesApp::BuildShapeGeometry()
{
GeometryGenerator geoGen;
GeometryGenerator::MeshData box = geoGen.CreateBox(1.5f, 0.5f, 1.5f, 3);//宽1.5,高0.5,深1.5,细分三次(2^3=8)
GeometryGenerator::MeshData grid = geoGen.CreateGrid(20.0f, 30.0f, 60, 40);//长宽、长细分、宽细分
GeometryGenerator::MeshData sphere = geoGen.CreateSphere(0.5f, 20, 20);//球体半径、经纬细分
GeometryGenerator::MeshData cylinder = geoGen.CreateCylinder(0.5f, 0.3f, 3.0f, 20, 20);//底顶面半径、高、切片数量、堆叠数量
// 各几何体顶点数组的偏移
UINT boxVertexOffset = 0;
UINT gridVertexOffset = (UINT)box.Vertices.size();
UINT sphereVertexOffset = gridVertexOffset + (UINT)grid.Vertices.size();
UINT cylinderVertexOffset = sphereVertexOffset + (UINT)sphere.Vertices.size();
// 索引数组的偏移
UINT boxIndexOffset = 0;
UINT gridIndexOffset = (UINT)box.Indices32.size();
UINT sphereIndexOffset = gridIndexOffset + (UINT)grid.Indices32.size();
UINT cylinderIndexOffset = sphereIndexOffset + (UINT)sphere.Indices32.size();
// 定义子网格对象
SubmeshGeometry boxSubmesh;
boxSubmesh.IndexCount = (UINT)box.Indices32.size();//索引数量
boxSubmesh.StartIndexLocation = boxIndexOffset;//索引起始位置
boxSubmesh.BaseVertexLocation = boxVertexOffset;//顶点起始位置
SubmeshGeometry gridSubmesh;
gridSubmesh.IndexCount = (UINT)grid.Indices32.size();
gridSubmesh.StartIndexLocation = gridIndexOffset;
gridSubmesh.BaseVertexLocation = gridVertexOffset;
SubmeshGeometry sphereSubmesh;
sphereSubmesh.IndexCount = (UINT)sphere.Indices32.size();
sphereSubmesh.StartIndexLocation = sphereIndexOffset;
sphereSubmesh.BaseVertexLocation = sphereVertexOffset;
SubmeshGeometry cylinderSubmesh;
cylinderSubmesh.IndexCount = (UINT)cylinder.Indices32.size();
cylinderSubmesh.StartIndexLocation = cylinderIndexOffset;
cylinderSubmesh.BaseVertexLocation = cylinderVertexOffset;
// 生成顶点数组
auto totalVertexCount =
box.Vertices.size() +
grid.Vertices.size() +
sphere.Vertices.size() +
cylinder.Vertices.size();
std::vector<Vertex> vertices(totalVertexCount);
// 将生成的数据全部填入数组
UINT k = 0;
for(size_t i = 0; i < box.Vertices.size(); ++i, ++k)
{
vertices[k].Pos = box.Vertices[i].Position;
vertices[k].Color = XMFLOAT4(DirectX::Colors::DarkGreen);
}
for(size_t i = 0; i < grid.Vertices.size(); ++i, ++k)
{
vertices[k].Pos = grid.Vertices[i].Position;
vertices[k].Color = XMFLOAT4(DirectX::Colors::ForestGreen);
}
for(size_t i = 0; i < sphere.Vertices.size(); ++i, ++k)
{
vertices[k].Pos = sphere.Vertices[i].Position;
vertices[k].Color = XMFLOAT4(DirectX::Colors::Crimson);
}
for(size_t i = 0; i < cylinder.Vertices.size(); ++i, ++k)
{
vertices[k].Pos = cylinder.Vertices[i].Position;
vertices[k].Color = XMFLOAT4(DirectX::Colors::SteelBlue);
}
//生成索引数组
std::vector<std::uint16_t> indices;
indices.insert(indices.end(), std::begin(box.GetIndices16()), std::end(box.GetIndices16()));
indices.insert(indices.end(), std::begin(grid.GetIndices16()), std::end(grid.GetIndices16()));
indices.insert(indices.end(), std::begin(sphere.GetIndices16()), std::end(sphere.GetIndices16()));
indices.insert(indices.end(), std::begin(cylinder.GetIndices16()), std::end(cylinder.GetIndices16()));
const UINT vbByteSize = (UINT)vertices.size() * sizeof(Vertex);
const UINT ibByteSize = (UINT)indices.size() * sizeof(std::uint16_t);
//集合网格对象生成,初始化顶点、索引数组
auto geo = std::make_unique<MeshGeometry>();
geo->Name = "shapeGeo";
ThrowIfFailed(D3DCreateBlob(vbByteSize, &geo->VertexBufferCPU));
CopyMemory(geo->VertexBufferCPU->GetBufferPointer(), vertices.data(), vbByteSize);
ThrowIfFailed(D3DCreateBlob(ibByteSize, &geo->IndexBufferCPU));
CopyMemory(geo->IndexBufferCPU->GetBufferPointer(), indices.data(), ibByteSize);
geo->VertexBufferGPU = d3dUtil::CreateDefaultBuffer(md3dDevice.Get(),
mCommandList.Get(), vertices.data(), vbByteSize, geo->VertexBufferUploader);
geo->IndexBufferGPU = d3dUtil::CreateDefaultBuffer(md3dDevice.Get(),
mCommandList.Get(), indices.data(), ibByteSize, geo->IndexBufferUploader);
// 全体网格信息
geo->VertexByteStride = sizeof(Vertex);
geo->VertexBufferByteSize = vbByteSize;
geo->IndexFormat = DXGI_FORMAT_R16_UINT;
geo->IndexBufferByteSize = ibByteSize;
// 各个子网格信息
geo->DrawArgs["box"] = boxSubmesh;
geo->DrawArgs["grid"] = gridSubmesh;
geo->DrawArgs["sphere"] = sphereSubmesh;
geo->DrawArgs["cylinder"] = cylinderSubmesh;
mGeometries[geo->Name] = std::move(geo);
}
接下来,就是构建渲染项,对于渲染项的描述,在238页,简单描述:对于同一个几何体,我们常常希望能表现出不同的样式(例如不同位置、旋转、缩放),因此我们采用渲染项,来记录原几何体,以及发生的变化。
渲染项的结构因hlsl和渲染需求而议,对于当前程序的hlsl来说,使相同几何体发生变化的只有ObjectCB而非PassCB,PassCB是用来记录其他常用参数的,因此,这个RenderItem的定义如下:
struct RenderItem
{
RenderItem() = default;
// 描述了物体之于世界空间的旋转、缩放、移动
XMFLOAT4X4 World = MathHelper::Identity4x4();
// 更新标志
int NumFramesDirty = gNumFrameResources;
// 处于常量缓冲区的第几个
UINT ObjCBIndex = -1;
// 几何体
MeshGeometry* Geo = nullptr;
// 图元拓扑方式
D3D12_PRIMITIVE_TOPOLOGY PrimitiveType = D3D_PRIMITIVE_TOPOLOGY_TRIANGLELIST;
// 渲染项信息
UINT IndexCount = 0;//索引数量
UINT StartIndexLocation = 0;//索引处于缓冲区的位置
int BaseVertexLocation = 0;//顶点处于缓冲区的位置
};
对于void ShapesApp::BuildRenderItems()
方法,有大量重复的代码,详见于255页。
帧资源
接下来是BuildFrameResources方法。帧资源是用于解决CPU与GPU同步效率低下问题的。详见于235页。
之前我们写的代码,每次Draw最后,都要跟着FlushCommandQueue方法,这样却有一个问题:当我们设置完所有命令列表内的命令,提交给命令队列都,都要先Flush,这时CPU会等待GPU的运行,过程中CPU的时间完全浪费;而我们执行完GPU程序,我们复用命令列表,继续向列表内添加命令,此时命令队列完全为空,也就表名,这一过程中,GPU的时间完全浪费,也就是说,CPU和GPU完全没有并行!
理想中的状态如下:GPU能完全不空闲,一直执行命令队列中的指令,而CPU除了放置命令列表外,还要执行诸如人工智能等运。
GPU闲下来了,说明提交资源不够频繁,为何不能一直提交资源?因为命令列表所需要的分配器需要等等队列中的命令执行完毕才可以Reset,这样的话,我们用好几个分配器轮流使用就好了,CPU就可以一直给GPU命令。而CPU也可以在等待时间去做其他游戏逻辑了。
对于一个帧资源,我们希望每帧都有自己的命令分配器,除此之外,由于每帧都可能对常量缓冲区进行更新,因此也要给每个帧资源都要配上常量缓冲区:
struct FrameResource
{
public:
FrameResource(ID3D12Device* device, UINT passCount, UINT objectCount);//创建命令分配器,和常量缓冲区上传堆
FrameResource(const FrameResource& rhs) = delete;
FrameResource& operator=(const FrameResource& rhs) = delete;
~FrameResource();
// 分配器
Microsoft::WRL::ComPtr<ID3D12CommandAllocator> CmdListAlloc;
// 常量缓冲区上传堆
std::unique_ptr<UploadBuffer<PassConstants>> PassCB = nullptr;
std::unique_ptr<UploadBuffer<ObjectConstants>> ObjectCB = nullptr;
// 每个帧资源独自持有围栏用于防止CPU运行过快的同步问题。
UINT64 Fence = 0;
};
其中PassConstants和ObjectConstants定义在类上方,可以发现,这个类处于公共文件下,今后也会反复用到。
创建帧资源代码如下:
void ShapesApp::BuildFrameResources()
{
for(int i = 0; i < gNumFrameResources; ++i)
{
mFrameResources.push_back(std::make_unique<FrameResource>(md3dDevice.Get(),
1, (UINT)mAllRitems.size()));
}
}
接下来是常量缓冲区描述符堆的构建(见258页):
void ShapesApp::BuildDescriptorHeaps()
{
UINT objCount = (UINT)mOpaqueRitems.size();//非透明物体的个数
// 对于每个帧资源,都有n个渲染项持有objectCB,自己独自持有一个passCB,当前例子中有三个帧资源,因此有3*(n+1)个常量缓冲区
UINT numDescriptors = (objCount+1) * gNumFrameResources;
// passCB的偏移(起始)位置
mPassCbvOffset = objCount * gNumFrameResources;
D3D12_DESCRIPTOR_HEAP_DESC cbvHeapDesc;
cbvHeapDesc.NumDescriptors = numDescriptors;
cbvHeapDesc.Type = D3D12_DESCRIPTOR_HEAP_TYPE_CBV_SRV_UAV;
cbvHeapDesc.Flags = D3D12_DESCRIPTOR_HEAP_FLAG_SHADER_VISIBLE;
cbvHeapDesc.NodeMask = 0;
ThrowIfFailed(md3dDevice->CreateDescriptorHeap(&cbvHeapDesc,
IID_PPV_ARGS(&mCbvHeap)));
}
构建常量缓冲区视图:
void ShapesApp::BuildConstantBufferViews()
{
UINT objCBByteSize = d3dUtil::CalcConstantBufferByteSize(sizeof(ObjectConstants));//每个缓冲区为256Byte倍数
UINT objCount = (UINT)mOpaqueRitems.size();//渲染项个数
// 对于每个帧资源的渲染项缓冲区,创建常量缓冲区视图
for(int frameIndex = 0; frameIndex < gNumFrameResources; ++frameIndex)
{
auto objectCB = mFrameResources[frameIndex]->ObjectCB->Resource();
for(UINT i = 0; i < objCount; ++i)
{
D3D12_GPU_VIRTUAL_ADDRESS cbAddress = objectCB->GetGPUVirtualAddress();//得到到虚拟首地址
// 到第I个渲染项所处的位置
cbAddress += i*objCBByteSize;
// 对应堆所处的索引位置(第framIndex帧的第i个资源)
int heapIndex = frameIndex*objCount + i;
auto handle = CD3DX12_CPU_DESCRIPTOR_HANDLE(mCbvHeap->GetCPUDescriptorHandleForHeapStart());
handle.Offset(heapIndex, mCbvSrvUavDescriptorSize);
// 构建常量缓冲区视图
D3D12_CONSTANT_BUFFER_VIEW_DESC cbvDesc;
cbvDesc.BufferLocation = cbAddress;
cbvDesc.SizeInBytes = objCBByteSize;
md3dDevice->CreateConstantBufferView(&cbvDesc, handle);
}
}
UINT passCBByteSize = d3dUtil::CalcConstantBufferByteSize(sizeof(PassConstants));
for(int frameIndex = 0; frameIndex < gNumFrameResources; ++frameIndex)
{
auto passCB = mFrameResources[frameIndex]->PassCB->Resource();
D3D12_GPU_VIRTUAL_ADDRESS cbAddress = passCB->GetGPUVirtualAddress();
int heapIndex = mPassCbvOffset + frameIndex;
auto handle = CD3DX12_CPU_DESCRIPTOR_HANDLE(mCbvHeap->GetCPUDescriptorHandleForHeapStart());
handle.Offset(heapIndex, mCbvSrvUavDescriptorSize);
D3D12_CONSTANT_BUFFER_VIEW_DESC cbvDesc;
cbvDesc.BufferLocation = cbAddress;
cbvDesc.SizeInBytes = passCBByteSize;
md3dDevice->CreateConstantBufferView(&cbvDesc, handle);
}
}
初始化最后的步骤,初始化管线状态对象:
void ShapesApp::BuildPSOs()
{
D3D12_GRAPHICS_PIPELINE_STATE_DESC opaquePsoDesc;//对象描述符
//不透明对象流水线
ZeroMemory(&opaquePsoDesc, sizeof(D3D12_GRAPHICS_PIPELINE_STATE_DESC));
opaquePsoDesc.InputLayout = { mInputLayout.data(), (UINT)mInputLayout.size() };//布局
opaquePsoDesc.pRootSignature = mRootSignature.Get();//根签名
opaquePsoDesc.VS = //顶点着色器
{
reinterpret_cast<BYTE*>(mShaders["standardVS"]->GetBufferPointer()),
mShaders["standardVS"]->GetBufferSize()
};
opaquePsoDesc.PS = //像素着色器
{
reinterpret_cast<BYTE*>(mShaders["opaquePS"]->GetBufferPointer()),
mShaders["opaquePS"]->GetBufferSize()
};
opaquePsoDesc.RasterizerState = CD3DX12_RASTERIZER_DESC(D3D12_DEFAULT);//光栅化状态
opaquePsoDesc.RasterizerState.FillMode = D3D12_FILL_MODE_WIREFRAME;
opaquePsoDesc.BlendState = CD3DX12_BLEND_DESC(D3D12_DEFAULT);//混合状态
opaquePsoDesc.DepthStencilState = CD3DX12_DEPTH_STENCIL_DESC(D3D12_DEFAULT);//深度、模版测试状态
opaquePsoDesc.SampleMask = UINT_MAX;//通常为此
opaquePsoDesc.PrimitiveTopologyType = D3D12_PRIMITIVE_TOPOLOGY_TYPE_TRIANGLE;//图元拓扑方法
opaquePsoDesc.NumRenderTargets = 1;//渲染目标数
opaquePsoDesc.RTVFormats[0] = mBackBufferFormat;//渲染目标格式
opaquePsoDesc.SampleDesc.Count = m4xMsaaState ? 4 : 1;//采样数、质量
opaquePsoDesc.SampleDesc.Quality = m4xMsaaState ? (m4xMsaaQuality - 1) : 0;
opaquePsoDesc.DSVFormat = mDepthStencilFormat;//S
ThrowIfFailed(md3dDevice->CreateGraphicsPipelineState(&opaquePsoDesc, IID_PPV_ARGS(&mPSOs["opaque"])));
//不透明对象线框流水线
D3D12_GRAPHICS_PIPELINE_STATE_DESC opaqueWireframePsoDesc = opaquePsoDesc;
opaqueWireframePsoDesc.RasterizerState.FillMode = D3D12_FILL_MODE_WIREFRAME;
ThrowIfFailed(md3dDevice->CreateGraphicsPipelineState(&opaqueWireframePsoDesc, IID_PPV_ARGS(&mPSOs["opaque_wireframe"])));
}
再看Update方法:
void ShapesApp::Update(const GameTimer& gt)
{
OnKeyboardInput(gt);//按1切换是否线框显示
UpdateCamera(gt);//更新view矩阵
// 切换为当前帧资源
mCurrFrameResourceIndex = (mCurrFrameResourceIndex + 1) % gNumFrameResources;
mCurrFrameResource = mFrameResources[mCurrFrameResourceIndex].get();
// 如果当前帧资源还未渲染完毕,则等待
if(mCurrFrameResource->Fence != 0 && mFence->GetCompletedValue() < mCurrFrameResource->Fence)
{
HANDLE eventHandle = CreateEventEx(nullptr, false, false, EVENT_ALL_ACCESS);
ThrowIfFailed(mFence->SetEventOnCompletion(mCurrFrameResource->Fence, eventHandle));
WaitForSingleObject(eventHandle, INFINITE);
CloseHandle(eventHandle);
}
//更新常量缓冲区
UpdateObjectCBs(gt);
UpdateMainPassCB(gt);
}
关于常量缓冲区的更新:
void ShapesApp::UpdateObjectCBs(const GameTimer& gt)
{
auto currObjectCB = mCurrFrameResource->ObjectCB.get();//得到当前帧资源
for(auto& e : mAllRitems)//对所有渲染项进行遍历
{
// 只要标志大于0,就把当前渲染项的world拿出来,更新到当前的帧资源的常量缓冲区里
if(e->NumFramesDirty > 0)
{
XMMATRIX world = XMLoadFloat4x4(&e->World);
ObjectConstants objConstants;
XMStoreFloat4x4(&objConstants.World, XMMatrixTranspose(world));
currObjectCB->CopyData(e->ObjCBIndex, objConstants);
// 只要改变,这里就设置为帧资源的个数,所以会连续n帧都更新帧资源,这样就会把所有帧资源都更新到了
e->NumFramesDirty--;
}
}
}
渲染
现在看看Draw方法:
void ShapesApp::Draw(const GameTimer& gt)
{
auto cmdListAlloc = mCurrFrameResource->CmdListAlloc;
// Update方法中已经保证了分配器是同步后的,因此可以直接reset
ThrowIfFailed(cmdListAlloc->Reset());
// 充值命令列表,并且渲染管线会根据成员变量判断是否要用线框流水线
if(mIsWireframe)
{
ThrowIfFailed(mCommandList->Reset(cmdListAlloc.Get(), mPSOs["opaque_wireframe"].Get()));
}
else
{
ThrowIfFailed(mCommandList->Reset(cmdListAlloc.Get(), mPSOs["opaque"].Get()));
}
//************************重复部分************************
mCommandList->RSSetViewports(1, &mScreenViewport);
mCommandList->RSSetScissorRects(1, &mScissorRect);
mCommandList->ResourceBarrier(1, &CD3DX12_RESOURCE_BARRIER::Transition(CurrentBackBuffer(),
D3D12_RESOURCE_STATE_PRESENT, D3D12_RESOURCE_STATE_RENDER_TARGET));
mCommandList->ClearRenderTargetView(CurrentBackBufferView(), Colors::LightSteelBlue, 0, nullptr);
mCommandList->ClearDepthStencilView(DepthStencilView(), D3D12_CLEAR_FLAG_DEPTH | D3D12_CLEAR_FLAG_STENCIL, 1.0f, 0, 0, nullptr);
// Specify the buffers we are going to render to.
mCommandList->OMSetRenderTargets(1, &CurrentBackBufferView(), true, &DepthStencilView());
ID3D12DescriptorHeap* descriptorHeaps[] = { mCbvHeap.Get() };
mCommandList->SetDescriptorHeaps(_countof(descriptorHeaps), descriptorHeaps);
mCommandList->SetGraphicsRootSignature(mRootSignature.Get());
//************************不重复部分************************
//找到passCbv在堆中的位置,获得首位置Handle并偏移到正确位置
int passCbvIndex = mPassCbvOffset + mCurrFrameResourceIndex;
auto passCbvHandle = CD3DX12_GPU_DESCRIPTOR_HANDLE(mCbvHeap->GetGPUDescriptorHandleForHeapStart());
passCbvHandle.Offset(passCbvIndex, mCbvSrvUavDescriptorSize);
mCommandList->SetGraphicsRootDescriptorTable(1, passCbvHandle);//添加根实参,P270
DrawRenderItems(mCommandList.Get(), mOpaqueRitems);//绘制所有渲染项
//************************重复部分************************
mCommandList->ResourceBarrier(1, &CD3DX12_RESOURCE_BARRIER::Transition(CurrentBackBuffer(),
D3D12_RESOURCE_STATE_RENDER_TARGET, D3D12_RESOURCE_STATE_PRESENT));
ThrowIfFailed(mCommandList->Close());
ID3D12CommandList* cmdsLists[] = { mCommandList.Get() };
mCommandQueue->ExecuteCommandLists(_countof(cmdsLists), cmdsLists);
ThrowIfFailed(mSwapChain->Present(0, 0));
mCurrBackBuffer = (mCurrBackBuffer + 1) % SwapChainBufferCount;
// 新的同步机制
mCurrFrameResource->Fence = ++mCurrentFence;
mCommandQueue->Signal(mFence.Get(), mCurrentFence);
}
其中,绘制场景方法如下:
void ShapesApp::DrawRenderItems(ID3D12GraphicsCommandList* cmdList, const std::vector<RenderItem*>& ritems)
{
UINT objCBByteSize = d3dUtil::CalcConstantBufferByteSize(sizeof(ObjectConstants));
auto objectCB = mCurrFrameResource->ObjectCB->Resource();
// 对每个渲染项遍历
for(size_t i = 0; i < ritems.size(); ++i)
{
auto ri = ritems[i];
cmdList->IASetVertexBuffers(0, 1, &ri->Geo->VertexBufferView());
cmdList->IASetIndexBuffer(&ri->Geo->IndexBufferView());
cmdList->IASetPrimitiveTopology(ri->PrimitiveType);
// 找到常量缓冲区的堆
UINT cbvIndex = mCurrFrameResourceIndex*(UINT)mOpaqueRitems.size() + ri->ObjCBIndex;
auto cbvHandle = CD3DX12_GPU_DESCRIPTOR_HANDLE(mCbvHeap->GetGPUDescriptorHandleForHeapStart());
cbvHandle.Offset(cbvIndex, mCbvSrvUavDescriptorSize);
cmdList->SetGraphicsRootDescriptorTable(0, cbvHandle);//设置根实参
//Draw Call
cmdList->DrawIndexedInstanced(ri->IndexCount, 1, ri->StartIndexLocation, ri->BaseVertexLocation, 0);
}
}
于是我们得到以下输出显示:
程序二
程序二,非常好看,生成了一个岛屿和海洋结构,但黑箱子多了一些,我尽量对源程序进行解读。
首先是生成网格(栅格)几何体的方法,在273页。
山体结构的生成比较容易,它是用了二元方程求出的方格,详情可见GetHillsHeight方法,它的函数为: 山体的颜色由高度决定,见276页,如果问为什么颜色不是平整的,而是有渐变的?那是因为边界处的顶点高度不一致,边界两侧的顶点向联时,会对线段上的片段(像素)进行颜色插值,所以颜色演起来有渐变(可以观察网格上的点的颜色,便于理解)。
根签名这次试用了根描述符,而不是描述符表,因此不用得到堆,直接获取虚拟地址传入命令列表的SetGraphics方法即可(见277页)。
动态顶点缓冲区
这是程序的大头,之前顶点缓冲区都是固定的,我们让几何体移动、旋转、缩放都是通过改变常量缓冲区进行的。这次我们希望直接改变顶点缓冲区。
回想之前静态缓冲区是如何初始化到渲染一个几何体的。
1.用创建几何体网格,得到顶点和索引缓冲
2.拿到一个MeshGeometry对象,分别交给CPU资源(Blob)类型,和GPU资源(Resource)类型。其中CPU资源属于直接交付即可,GPU资源需要调用书上给我们封装好的CreateDefaultBuffer方法,这个方法会帮我创建一个默认缓冲区,并用上传缓冲区作为中介,将资源上传给默认缓冲区的GPU资源。
3.根据资源,构建渲染项。
4.帧资源持有常量缓冲区。
5.每帧更新:对于根据渲染项,填充帧资源
6.DrawCall
现在我们不同了,我们看看每一步有什么区别
1.几何体网格创建,但顶点缓冲不会在现在生成
2.没有顶点,所以不会直接传递给顶点缓冲区,毕竟每帧都要改变,放在这里也无补于事,因此BuildWavesGeometryBuffers方法里有了这样的区别:
//首先得到顶点缓冲,根据网格的索引获取方法
//.....
UINT vbByteSize = mWaves->VertexCount()*sizeof(Vertex);//顶点缓冲大小
UINT ibByteSize = (UINT)indices.size()*sizeof(std::uint16_t);//索引缓冲大小
auto geo = std::make_unique<MeshGeometry>();
geo->Name = "waterGeo";
// 关于顶点缓冲的CPU和GPU资源全部不填入
geo->VertexBufferCPU = nullptr;
geo->VertexBufferGPU = nullptr;
//但填入索引缓冲的
ThrowIfFailed(D3DCreateBlob(ibByteSize, &geo->IndexBufferCPU));
CopyMemory(geo->IndexBufferCPU->GetBufferPointer(), indices.data(), ibByteSize);
geo->IndexBufferGPU = d3dUtil::CreateDefaultBuffer(md3dDevice.Get(),
mCommandList.Get(), indices.data(), ibByteSize, geo->IndexBufferUploader);
//剩下的数据照常填写
//....
3.构建渲染项,和原来没有什么不同
4.帧资源和原来也有一定变化,由于现在顶点缓冲每帧都要更新,和常量缓冲的更新速率一致,因此需要给每个帧资源都加上顶点缓冲:
std::unique_ptr<UploadBuffer<Vertex>> WavesVB = nullptr;
//初始化:
//设备,顶点数,不是常量缓冲区
WavesVB = std::make_unique<UploadBuffer<Vertex>>(device, waveVertCount, false);
5.每帧更新:原有的更新帧资源还要,但又出现一个更新方法,在279页:
void LandAndWavesApp::UpdateWaves(const GameTimer& gt)
{
// 没过0.25秒,随机打个水抛
static float t_base = 0.0f;
if((mTimer.TotalTime() - t_base) >= 0.25f)
{
t_base += 0.25f;
int i = MathHelper::Rand(4, mWaves->RowCount() - 5);
int j = MathHelper::Rand(4, mWaves->ColumnCount() - 5);
float r = MathHelper::RandF(0.2f, 0.5f);
mWaves->Disturb(i, j, r);
}
// 根据时间更新水平面
mWaves->Update(gt.DeltaTime());
/********************************重点!!!!!********************************/
// 找到帧资源的上传缓冲区,将所有顶点数组都放在里面
auto currWavesVB = mCurrFrameResource->WavesVB.get();
for(int i = 0; i < mWaves->VertexCount(); ++i)
{
Vertex v;
v.Pos = mWaves->Position(i);
v.Color = XMFLOAT4(DirectX::Colors::Blue);
currWavesVB->CopyData(i, v);
}
// 将动态顶点缓冲去设置到当前帧的顶点缓冲区
mWavesRitem->Geo->VertexBufferGPU = currWavesVB->Resource();
}
关于黑箱子,是波浪生成Wave的实现不清楚,但对于DirectX12的使用大致已经了解了。
下一章光照,希望学习速度能提高一些。
网友评论