7.7、陆地与波浪演示程序
在本小节中,我们将展示如何构建“陆地与波浪”演示程序:
- 构建一个三角形栅格(grid),并通过将其中的顶点设置到不同的位置来创建地形
- 再构建一个三角形栅格(grid),通过动态的改变其顶点高度来创建波浪
- 此示例还会针对不同的常量缓冲区来切换所使用的根描述符,这可以让我们不需要设置CBV描述符堆
7.7.1、生成栅格顶点
为了生成地形和波浪,我们首先要构建三角形栅格(grid),这里我们不进行详细的数学分析,直接给出结论:
\[
在xz平面上,第i行、第j列栅格顶点的坐标可以表示为:v = [-0.5w + j*dx, 0.0f, 0.5d - i*dz]
\]
其中w为栅格的总宽度(宽),dx为每一个小四边形的宽度,d为栅格的总深度(长),dz为每一个小四边形的深度,
(其实可以把栅格理解成一个由许多小四边形组成的大四边形)
下面是生成栅格顶点的代码:
/* ** Summary:生成栅格顶点 ** Parameters: ** width:栅格的宽度 ** depth:栅格的深度 ** m:行数 ** n:列数 ** Return:栅格的网格数据 */ GeometryGenerator::MeshData GeometryGenerator::CreateGrid(float width, float depth, uint32 m, uint32 n) { //用作返回值的数据 MeshData meshData; //顶点数量 uint32 vertexCount = m*n; //三角形数量 uint32 faceCount = (m-1)*(n-1)*2; // // 创建顶点 // float halfWidth = 0.5f*width; float halfDepth = 0.5f*depth; float dx = width / (n-1); float dz = depth / (m-1); float du = 1.0f / (n-1); float dv = 1.0f / (m-1); meshData.Vertices.resize(vertexCount); for(uint32 i = 0; i < m; ++i) { float z = halfDepth - i*dz; for(uint32 j = 0; j < n; ++j) { float x = -halfWidth + j*dx; meshData.Vertices[i*n+j].Position = XMFLOAT3(x, 0.0f, z); meshData.Vertices[i*n+j].Normal = XMFLOAT3(0.0f, 1.0f, 0.0f); meshData.Vertices[i*n+j].TangentU = XMFLOAT3(1.0f, 0.0f, 0.0f); //在栅格上拉伸纹理 meshData.Vertices[i*n+j].TexC.x = j*du; meshData.Vertices[i*n+j].TexC.y = i*dv; } } return meshData; }
7.7.2、生成栅格索引
完成顶点的计算之后,我们需要指定索引来定义栅格三角形。所以我们需要遍历每一个四边形,并计算索引以此来定义构成每一个四边形的三角形。具体看代码:
/* ** Summary:生成栅格顶点 ** Parameters: ** width:栅格的宽度 ** depth:栅格的深度 ** m:行数 ** n:列数 ** Return:栅格的网格数据 */ GeometryGenerator::MeshData GeometryGenerator::CreateGrid(float width, float depth, uint32 m, uint32 n) { //用作返回值的数据 MeshData meshData; //顶点数量 uint32 vertexCount = m*n; //三角形数量 uint32 faceCount = (m-1)*(n-1)*2; // // 创建索引 // //三角形数量*3 meshData.Indices32.resize(faceCount*3); //遍历每一个四边形计算索引 uint32 k = 0; for(uint32 i = 0; i < m-1; ++i) { for(uint32 j = 0; j < n-1; ++j) { meshData.Indices32[k] = i*n+j; meshData.Indices32[k+1] = i*n+j+1; meshData.Indices32[k+2] = (i+1)*n+j; meshData.Indices32[k+3] = (i+1)*n+j; meshData.Indices32[k+4] = i*n+j+1; meshData.Indices32[k+5] = (i+1)*n+j+1; //下一个四边形 k += 6; } } return meshData; }
7.7.3、应用计算高度的函数
通过前面两节,我们可以创建出一个栅格,栅格创建完毕之后,我们就可以获取栅格的顶点元素,通过设置顶点的高度(y坐标)便可以将平坦的栅格转换成高低起伏的曲面,并为它生成对应的颜色
//顶点结构体 struct Vertex { XMFLOAT3 Pos; XMFLOAT4 Coloe; } void LandAndWavesApp::BuildLandGeometry() { GeometryGenerator geoGen; GeometryGenerator::MeshData grid = geoGen.CreateGrid(160.0f, 160.0f, 50, 50); // //获取顶点元素,并利用高度函数计算每一个顶点的高度值(y轴值) //根据不同的高度设置不同的颜色 // std::vector<Vertex> vertices(grid.Vertices.size()); for(size_t i = 0; i < grid.Vertices.size(); ++i) { auto& p = grid.Vertices[i].Position; vertices[i].Pos = p; vertices[i].Pos.y = GetHillsHeight(p.x, p.z); // 基于顶点高度设置颜色 …… const UINT vbByteSize = (UINT)vertices.size() * sizeof(Vertex); std::vector<std::uint16_t> indices = grid.GetIndices16(); const UINT ibByteSize = (UINT)indices.size() * sizeof(std::uint16_t); auto geo = std::make_unique<MeshGeometry>(); geo->Name = "landGeo"; //创建缓冲区 ThrowIfFailed(D3DCreateBlob(vbByteSize, &geo->VertexBufferCPU)); //将顶点数据存入geo中 CopyMemory(geo->VertexBufferCPU->GetBufferPointer(), vertices.data(), vbByteSize); //创建缓冲区 ThrowIfFailed(D3DCreateBlob(ibByteSize, &geo->IndexBufferCPU)); //将索引数据存入geo中 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; SubmeshGeometry submesh; submesh.IndexCount = (UINT)indices.size(); submesh.StartIndexLocation = 0; submesh.BaseVertexLocation = 0; geo->DrawArgs["grid"] = submesh; mGeometries["landGeo"] = std::move(geo); }
7.7.4、根常量缓冲区视图
在这个示例程序中,我们将使用根描述符,这样我们就不使用描述符表了,为了使用根描述符,我们需要做一些改动:
- 根签名需要变成需要两个根CBV,而不是两个描述符表
- 不用使用CBV堆吗,也不需要向其填充描述符
- 涉及一种用于绑定根描述符的新语法
新的根签名定义如下:
// 创建根签名 CD3DX12_ROOT_PARAMETER slotRootParameter[2]; // 物体的CBV slotRootParameter[0].InitAsConstantBufferView(0); //渲染过程的CBV slotRootParameter[1].InitAsConstantBufferView(1); // 创建根签名描述符 CD3DX12_ROOT_SIGNATURE_DESC rootSigDesc(2, slotRootParameter, 0, nullptr, D3D12_ROOT_SIGNATURE_FLAG_ALLOW_INPUT_ASSEMBLER_INPUT_LAYOUT);
创建完根签名之后,我们需要使用下列方法,以传递参数的形式将CBV和某个根描述符相互绑定
/* ** Summary:将CBv和根描述符相互绑定 ** Parameters: ** RootParameterIndex:CBV将要绑定的根参数索引,即寄存器的槽位号 ** BufferLocation:含有常量缓冲区数据资源的虚拟地址 ** Return:Void */ void SetGraphicsRootConstantBufferView( UINT RootParameterIndex, D3D12_GPU_VIRTUAL_ADDRESS BufferLocation);
经过上述变化之后,我们的绘制代码会发生一些改变:
void LandAndWavesApp::Draw(const GameTimer& gt) { ………… // 绑定渲染过程中所使用的常量缓冲区,在每一个渲染过程中,该代码只需要执行一次 auto passCB = mCurrFrameResource->PassCB->Resource(); mCommandList->SetGraphicsRootConstantBufferView(1, passCB->GetGPUVirtualAddress()); DrawRenderItems(mCommandList.Get(), mRitemLayer[(int)RenderLayer::Opaque]); ………… }
void LandAndWavesApp::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); D3D12_GPU_VIRTUAL_ADDRESS objCBAddress = objectCB->GetGPUVirtualAddress(); objCBAddress += ri->ObjCBIndex*objCBByteSize; cmdList->SetGraphicsRootConstantBufferView(0, objCBAddress); cmdList->DrawIndexedInstanced(ri->IndexCount, 1, ri->StartIndexLocation, ri->BaseVertexLocation, 0); } }
7.7.5、动态顶点缓冲区
到目前为止,我们每次都将顶点数据存在默认的资源缓冲区中,然后借此存储静态几何体。所以我们不能动态的改变此资源中存储的几何体,只能一次性设置好数据,然后用GPU读取其中的数据并进行绘制。
因此,为了改变这个窘境,一种名为动态缓冲区的资源便出现了,它允许用户频繁的改变顶点数据,比如我们在模拟波浪的时候,我们需要随着时间的变化而不断更新顶点的高度,因此我们需要使用动态顶点缓冲区。或者是粒子系统中,我们需要寻找每一个粒子的新位置,而且在每一帧都要使用CPU进行物理模拟计算和碰撞检测,这个时候我们也需要使用动态顶点缓冲区在绘制每一帧的时候更新粒子的位置
在通过上传缓冲区来更新常量缓冲区中的数据的小节中,我们已经接触过如何在每一帧通过CPU来向GPU上传数据的具体流程,现在我们只要将常量缓冲区换成顶点数组就可以实现动态顶点缓冲区了。
std::unique_ptr<UploadBuffer<Vertex>> WavesVB = nullptr; WavesVB = std::make_unique<UploadBuffer<Vertex>>(device, waveVertCount, false);
由于我们每一帧都要从CPU中向动态顶点缓冲区上传新的数据,所以我们需要将动态顶点缓冲区存为一种帧资源,否则,我们可能在GPU没有完成上一帧的处理,就修改了相关的内存了。
在每一帧中,我们都使用下列方式来模拟波浪并更新顶点缓冲区
void LandAndWavesApp::UpdateWaves(const GameTimer& gt) { // 每隔1/4秒生成一个随机波浪 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(); }
使用动态缓冲区时会不可避免的产生一些额外的开销,因为新的数据必须从CPU端内存回传到GPU端显存,所以,如果静态缓冲区可以实现和动态缓冲区同样的工作,我们应该避免使用动态缓冲区。本章示例程序主要展示了如何通过一个动态顶点缓冲区来实现波浪的模拟。(题外话:只要将顶点数组改成索引数组,我们便可以创建一个动态索引缓冲区了,这方面可以自己扩展一下)