D3D12渲染技术之网格地形

版权声明:本文为博主原创文章,未经博主允许不得转载。 https://blog.csdn.net/jxw167/article/details/82732228

本篇博客我们将展示一个案例, 该案例实现在程序上构造三角形网格,通过偏移顶点高度以创建地形,方法比较简单。 此外,使用另一个三角形网格来表示水,并设置顶点高度的动画以创建波浪。 此案例还切换到使用根描述符作为常量缓冲区,这允许我们放弃对CBV的描述符堆的支持,实现的效果图如下所示:
这里写图片描述
我们用函数y = f(x,z)实现表面, 通过在xz平面构建网格来近似表面,其中每个四边形由两个三角形构成,然后将函数应用于每个网格点; 见下图:
这里写图片描述

网格顶点

我们的主要任务是如何在xz平面中构建网格, m×n顶点的网格表示(m-1)×(n-1)个四边形(或单元),如下图所示, 每个单元格将被两个三角形覆盖,因此总共有2个(m - 1)×(n - 1)个三角形。 如果网格具有宽度w和深度d,则沿x轴的单元间隔是dx = w /(n-1),并且沿z轴的单元间隔是dz = d /(m-1)。 为了生成顶点,我们从左上角开始逐行逐行计算顶点坐标, xz平面中第i个网格顶点的坐标由下式给出:
这里写图片描述

这里写图片描述
以下代码生成网格顶点:

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);
 
      // Stretch texture over grid.
      meshData.Vertices[i*n+j].TexC.x = j*du;
      meshData.Vertices[i*n+j].TexC.y = i*dv;
    }
  }

顶点索引

在计算了顶点之后,我们需要通过指定索引来定义网格三角形, 为此,我们迭代每个四边形,从左上角开始逐行,并计算索引以定义四边形的两个三角形; 如下图所示,对于m×n顶点网格,两个三角形的线性数组索引计算如下:
这里写图片描述

这里写图片描述
对应的代码如下所示:

meshData.Indices32.resize(faceCount*3); // 3 indices per face
 
  // Iterate over each quad and compute indices.
  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; // next quad
    }
  }
 
  return meshData;
}

高度函数

在我们创建网格之后,可以从MeshData网格中提取想要的顶点元素,将平面网格转换为表示山丘的表面,并根据顶点高度(y坐标)为每个顶点生成颜色。

// Not to be confused with GeometryGenerator::Vertex.
struct Vertex
{
  XMFLOAT3 Pos;
  XMFLOAT4 Color;
};
void LandAndWavesApp::BuildLandGeometry()
{
  GeometryGenerator geoGen;
  GeometryGenerator::MeshData grid = geoGen.CreateGrid(160.0f, 160.0f, 50, 50);
 
  //
  // Extract the vertex elements we are interested and apply the height
  // function to each vertex. In addition, color the vertices based on
  // their height so we have sandy looking beaches, grassy low hills, 
  // and snow mountain peaks.
  //
 
  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);
	 // Color the vertex based on its height.
    if(vertices[i].Pos.y < -10.0f)
    {
      // Sandy beach color.
      vertices[i].Color = XMFLOAT4(1.0f, 0.96f, 0.62f, 1.0f);
    }
    else if(vertices[i].Pos.y < 5.0f)
    {
      // Light yellow-green.
      vertices[i].Color = XMFLOAT4(0.48f, 0.77f, 0.46f, 1.0f);
    }
    else if(vertices[i].Pos.y < 12.0f)
    {
      // Dark yellow-green.
      vertices[i].Color = XMFLOAT4(0.1f, 0.48f, 0.19f, 1.0f);
    }
    else if(vertices[i].Pos.y < 20.0f)
    {
      // Dark brown.
      vertices[i].Color = XMFLOAT4(0.45f, 0.39f, 0.34f, 1.0f);
    }
    else
     {
      // White snow.
      vertices[i].Color = XMFLOAT4(1.0f, 1.0f, 1.0f, 1.0f);
    }
  }
  
  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));
  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;
 
  SubmeshGeometry submesh;
 submesh.IndexCount = (UINT)indices.size();
  submesh.StartIndexLocation = 0;
  submesh.BaseVertexLocation = 0;
 
  geo->DrawArgs["grid"] = submesh;
 
  mGeometries["landGeo"] = std::move(geo);
}

我们在这个案例中使用的函数f(x,z)由下式给出:

float LandAndWavesApp::GetHeight(float x, float z)const
{
  return 0.3f*(z*sinf(0.1f*x) + x*cosf(0.1f*z));
}

它的图形看起来有点像山丘和山谷的地形,见上面图显示的地形。

根CBVs

我们所做的一项更改是使用根描述符,以便可以直接绑定CBV而无需使用描述符堆,以下是需要进行的更改:
1、需要更改根签名以获取两个根CBV而不是两个描述符表。
2、不需要CBV堆也不需要使用描述符填充。
3、绑定根描述符有新的语法。
新的根签名定义如下:

// Root parameter can be a table, root descriptor or root constants.
CD3DX12_ROOT_PARAMETER slotRootParameter[2];
 
// Create root CBV.
slotRootParameter[0].InitAsConstantBufferView(0); // per-object CBV
slotRootParameter[1].InitAsConstantBufferView(1); // per-pass CBV
 
// A root signature is an array of root parameters.
CD3DX12_ROOT_SIGNATURE_DESC rootSigDesc(2, slotRootParameter, 0, 
  nullptr, D3D12_ROOT_SIGNATURE_FLAG_ALLOW_INPUT_ASSEMBLER_INPUT_LAYOUT);

注意我们使用了InitAsConstantBufferView帮助方法创建根CBV;该参数指定此绑定的着色器寄存器(在上面的代码中,着色器常量缓冲寄存器“b0”和“b1”)。
现在,我们使用以下方法将CBV作为参数绑定到根描述符:

void 
ID3D12GraphicsCommandList::SetGraphicsRootConstantBufferView(
  UINT RootParameterIndex,
  D3D12_GPU_VIRTUAL_ADDRESS BufferLocation);

通过此更改,我们的绘图代码现在看起来像这样:

void LandAndWavesApp::Draw(const GameTimer& gt)
{
  […]
 
  // Bind per-pass constant buffer. We only need to do this once per-
  // pass.
  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 each render item…
  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);
  }
}

动态顶点缓冲

到目前为止,我们已将顶点存储在默认缓冲区资源中,当想要存储静态几何时,使用这种资源,也就是说,我们不改变的几何 - 设置数据,GPU读取和绘制数据。动态顶点缓冲区是我们频繁更改顶点数据的地方,比如每帧,例如,假设正在进行波浪模拟,求解解函数f(x,z,t)的波动方程,该函数表示在时间t的xz平面中每个点处的波高,如果我们使用这个函数来绘制波浪,我们将像使用峰和谷一样使用三角形网格网格,并将f(x,z,t)应用于每个网格点,以获得波高。因为此函数还取决于时间t(即,波面随时间变化),我们需要在短时间后(例如每1/30秒)将此函数重新应用于网格点以获得平滑动画。因此,我们需要一个动态顶点缓冲区,以便随着时间的推移更新三角形网格网格顶点的高度,导致动态顶点缓冲的另一种情况是具有复杂物理和碰撞检测的粒子系统。我们将在每个帧上对CPU进行物理和碰撞检测,以找到粒子的新位置,因为粒子位置正在改变每一帧,我们需要一个动态顶点缓冲区来更新粒子位置以绘制每一帧。
我们已经看到了一个案例,当使用上传缓冲区来更新我们的常量缓冲区数据时,将每帧的数据从CPU上传到GPU。 我们可以应用相同的技术并使用UploadBuffer类,但不是存储常量缓冲区数组,而是存储顶点数组:

 std::unique_ptr<UploadBuffer<Vertex>> WavesVB = nullptr;
 
  WavesVB = std::make_unique<UploadBuffer<Vertex>>(
    device, waveVertCount, false);

因为我们需要每帧将新内容从CPU上传到波峰的动态顶点缓冲区,所以动态顶点缓冲区需要是帧资源。 否则,我们可以在GPU处理完最后一帧之前覆盖内存。
每一帧,我们运行波浪模拟并更新顶点缓冲区,如下所示:

void LandAndWavesApp::UpdateWaves(const GameTimer& gt)
{
  // Every quarter second, generate a random wave.
  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);
  }
 
  // Update the wave simulation.
  mWaves->Update(gt.DeltaTime());
   // Update the wave vertex buffer with the new solution.
  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);
  }
 
  // Set the dynamic VB of the wave renderitem to the current frame VB.
  mWavesRitem->Geo->VertexBufferGPU = currWavesVB->Resource();
}

我们保存对波渲染项(mWavesRitem)的引用,以便我们可以动态设置其顶点缓冲区, 我们需要这样做,因为它的顶点缓冲区是一个动态缓冲区并且每帧都会改变。

使用动态缓冲区时会有一些开销,因为必须将新数据从CPU内存传输回GPU内存, 因此,静态缓冲区应该优先于动态缓冲区,前提是静态缓冲区可以工作, 最新版本的Direct3D引入了新功能,以减少对动态缓冲区的需求。 例如:
1、可以在顶点着色器中完成简单动画。
2、通过渲染到纹理或计算着色器和顶点纹理获取功能,可以实现像上面描述的完全在GPU上运行的波模拟。
3、几何着色器为GPU提供了创建或销毁基元的能力,这是一项通常需要在没有几何着色器的情况下在CPU上完成的任务。
4、曲面细分阶段可以在GPU上添加细分几何体,这通常需要在没有硬件细分的情况下在CPU上完成。
索引缓冲区也可以是动态的。 然而,在案例演示中,三角形拓扑保持不变,只有顶点高度发生变化; 因此,只有顶点缓冲区需要是动态的。

本章的案例演示使用动态顶点缓冲区来实现一个简单的波浪模拟,就像本篇开头所描述的那样,我们不关心波模拟的实际算法细节,但更多的是使用该过程来说明动态缓冲区:更新CPU上的模拟,然后使用更新顶点数据 上传缓冲区。

总结

1、等待GPU每帧执行队列中的所有命令都是低效的,因为它会导致CPU和GPU在某个时刻空闲。 更有效的技术是创建帧资源 - CPU需要修改每个帧的资源的循环数组。 这样,CPU在进入下一帧之前不需要等待GPU完成; CPU将仅与下一个可用(即,未被GPU使用)帧资源一起工作。 如果CPU总是以比GPU更快的速度处理帧,那么最终CPU将不得不在某个时刻等待GPU赶上,但这是理想的情况,因为GPU正在被充分利用; 额外的CPU周期总是可以用于游戏的其他部分,如AI,物理和游戏逻辑。

2、我们可以使用ID3D12DescriptorHeap :: GetCPUDescriptorHandleForHeapStart方法获取堆中第一个描述符的句柄, 我们可以使用ID3D12Device :: GetDescriptorHandleIncrementSize(DescriptorHeapType类型)方法获取描述符大小(取决于硬件和描述符类型)。 一旦我们知道描述符增量大小,我们就可以使用两个CD3DX12_CPU_DESCRIPTOR_HANDLE :: Offset方法之一来通过n个描述符来偏移句柄:

// Specify the number of descriptors to offset times the descriptor
// increment size:
D3D12_CPU_DESCRIPTOR_HANDLE handle = mCbvHeap->
   GetCPUDescriptorHandleForHeapStart();
handle.Offset(n * mCbvSrvDescriptorSize);
 
// Or equivalently, specify the number of descriptors to offset,
// followed by the descriptor increment size:
D3D12_CPU_DESCRIPTOR_HANDLE handle = mCbvHeap->GetCPUDescriptorHandleForHeapStart();
handle.Offset(n, mCbvSrvDescriptorSize);

3、根签名定义在发出绘制调用之前需要将哪些资源绑定到管道以及这些资源如何映射到着色器输入寄存器,需要绑定哪些资源取决于绑定着色器程序所期望的资源,创建PSO时,将验证根签名和着色器程序组合。根签名被指定为根参数数组,根参数可以是描述符表,根描述符或根常量,描述符表指定堆中的连续描述符范围。根描述符用于直接在根签名中绑定描述符(它不需要在堆中),根常量用于直接在根签名中绑定常量值,为了提高性能,可以将六十四个DWORD限制在根签名中,每个描述符表需要占用一个DWORD,每个根描述符占用两个DWORD,根常量为每个32位常量占用一个DWORD。硬件会自动保存每个绘制调用的根参数的快照,因此,我们可以安全地更改每个绘制调用的根参数,但是,我们还应该尝试保持根签名较小,以便复制更少的内存。
4、当需要在运行时频繁更新顶点缓冲区的内容时(例如,每帧或每1/30秒),使用动态顶点缓冲区, 我们可以使用UploadBuffer来实现动态顶点缓冲区,但是我们存储了一个顶点数组,而不是存储常量缓冲区数组, 因为我们需要每帧将新内容从CPU上传到地形网格的动态顶点缓冲区,所以动态顶点缓冲区需要是帧资源。 使用动态顶点缓冲区时会有一些开销,因为新数据必须从CPU内存传输回GPU内存, 因此,静态顶点缓冲区应该优先于动态顶点缓冲区,前提是静态顶点缓冲区可以工作, 最新版本的Direct3D引入了新功能,以减少对动态缓冲区的需求。
代码地址:链接:https://pan.baidu.com/s/1X0Vikf6qGYGPKU-Nwf-wYA 密码:h79q

阅读更多

扫码向博主提问

海洋_

博客专家

非学,无以致疑;非问,无以广识
  • 擅长领域:
  • 3D引擎架构
  • 服务器架构
  • GPU渲染
  • 客户端架构
  • 引擎优化
去开通我的Chat快问
想对作者说点什么?

博主推荐

换一批

没有更多推荐了,返回首页