1. 程式人生 > >D3D12渲染技術之網格地形

D3D12渲染技術之網格地形

本篇部落格我們將展示一個案例, 該案例實現在程式上構造三角形網格,通過偏移頂點高度以建立地形,方法比較簡單。 此外,使用另一個三角形網格來表示水,並設定頂點高度的動畫以建立波浪。 此案例還切換到使用根描述符作為常量緩衝區,這允許我們放棄對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