最爱午后红茶

剖析山峰河谷演示程序

日期图标
2023-06-15

山峰河谷演示程序:

从演示中可以看出,本程序是在 山峰演示程序 的基础上增加了一层波纹。那么这种动态的波纹效果是怎样实现的呢?

要实现这种波纹效果,可以使用动态缓冲区dynamic buffer)。动态缓冲区中的数据可以在每一帧中进行修改,因此使用它可以实现一些动画效果。它的思路跟前面山峰的创建类似:

  • 创建 xzx-z 平面的网格,通过某种算法计算每一时刻 xzx-z 平面上每个点的高度(yy 坐标)。
bool WavesDemo::Init() {
  if (!D3DApp::Init())
    return false;

  mWaves.Init(200, 200, 0.8f, 0.03f, 3.25f, 0.4f);

  BuildLandGeometryBuffers();
  // 创建水波顶点缓冲区和索引缓冲区
  BuildWavesGeometryBuffers();
  BuildFX();
  BuildVertexLayout();

  D3D11_RASTERIZER_DESC wireframeDesc;
  ZeroMemory(&wireframeDesc, sizeof(D3D11_RASTERIZER_DESC));
  wireframeDesc.FillMode = D3D11_FILL_WIREFRAME;
  wireframeDesc.CullMode = D3D11_CULL_BACK;
  wireframeDesc.FrontCounterClockwise = false;
  wireframeDesc.DepthClipEnable = true;

  HR(md3dDevice->CreateRasterizerState(&wireframeDesc, &mWireframeRS));

  return true;
}

从上面的代码可以看到,我们把渲染状态设置成了线框模式,在后面渲染水波时我们会使用它。因为在没有光照(当前还没有讲到)的情况下,使用实体模式去渲染水波的话不容易看出水波的运动效果。

动态缓冲区的创建在参数的填写上跟静态缓冲区有所区别:

void WavesDemo::BuildWavesGeometryBuffers() {
  // Create the vertex buffer.  Note that we allocate space only, as
  // we will be updating the data every time step of the simulation.

  D3D11_BUFFER_DESC vbd;
  // 标志为动态缓冲区
  vbd.Usage = D3D11_USAGE_DYNAMIC;
  vbd.ByteWidth = sizeof(Vertex) * mWaves.VertexCount();
  vbd.BindFlags = D3D11_BIND_VERTEX_BUFFER;
  // 需要向缓冲区写入数据
  vbd.CPUAccessFlags = D3D11_CPU_ACCESS_WRITE;
  vbd.MiscFlags = 0;
  HR(md3dDevice->CreateBuffer(&vbd, 0, &mWavesVB));

  // Create the index buffer.  The index buffer is fixed, so we only
  // need to create and set once.

  std::vector<UINT> indices(3 * mWaves.TriangleCount()); // 3 indices per face

  // Iterate over each quad.
  UINT m = mWaves.RowCount();
  UINT n = mWaves.ColumnCount();
  int k = 0;
  for (UINT i = 0; i < m - 1; ++i) {
    for (DWORD j = 0; j < n - 1; ++j) {
      indices[k] = i * n + j;
      indices[k + 1] = i * n + j + 1;
      indices[k + 2] = (i + 1) * n + j;

      indices[k + 3] = (i + 1) * n + j;
      indices[k + 4] = i * n + j + 1;
      indices[k + 5] = (i + 1) * n + j + 1;

      k += 6; // next quad
    }
  }

  D3D11_BUFFER_DESC ibd;
  ibd.Usage = D3D11_USAGE_IMMUTABLE;
  ibd.ByteWidth = sizeof(UINT) * indices.size();
  ibd.BindFlags = D3D11_BIND_INDEX_BUFFER;
  ibd.CPUAccessFlags = 0;
  ibd.MiscFlags = 0;
  D3D11_SUBRESOURCE_DATA iinitData;
  iinitData.pSysMem = &indices[0];
  HR(md3dDevice->CreateBuffer(&ibd, &iinitData, &mWavesIB));
}

其中变量 mWaves 是类 Waves 的实例,通过它可以创建水波的顶点和索引,还可以根据当前时间更新顶点(这里不会对这个类进行详细剖析,我们只是用它来说明动态缓冲区的用法)。

水波程序类图
图一:水波程序类图

我们在 UpdateScene() 函数中对水波的顶点进行更新:

void WavesDemo::UpdateScene(float dt) {
  // Convert Spherical to Cartesian coordinates.
  float x = mRadius * sinf(mPhi) * cosf(mTheta);
  float z = mRadius * sinf(mPhi) * sinf(mTheta);
  float y = mRadius * cosf(mPhi);

  // Build the view matrix.
  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);
  XMStoreFloat4x4(&mView, V);

  //
  // Every quarter second, generate a random wave.
  //
  static float t_base = 0.0f;
  if ((mTimer.TotalTime() - t_base) >= 0.25f) {
    t_base += 0.25f;

    DWORD i = 5 + rand() % 190;
    DWORD j = 5 + rand() % 190;

    float r = MathHelper::RandF(1.0f, 2.0f);

    mWaves.Disturb(i, j, r);
  }

  mWaves.Update(dt);

  //
  // Update the wave vertex buffer with the new solution.
  //

  D3D11_MAPPED_SUBRESOURCE mappedData;
  HR(md3dImmediateContext->Map(mWavesVB, 0, D3D11_MAP_WRITE_DISCARD, 0,
                               &mappedData));

  Vertex *v = reinterpret_cast<Vertex *>(mappedData.pData);
  for (UINT i = 0; i < mWaves.VertexCount(); ++i) {
    v[i].Pos = mWaves[i];
    v[i].Color = XMFLOAT4(0.0f, 0.0f, 0.0f, 1.0f);
  }

  md3dImmediateContext->Unmap(mWavesVB, 0);
}

函数 ID3D11DeviceContext::Map() 用于将一个缓冲区(如常量缓冲区、顶点缓冲区、纹理等)映射到 CPU 可访问的内存空间中(即 mappedData),这样我们就可以对顶点缓冲区进行修改了。

最后在绘制时,我们依次绘制山峰和水波即可:

void WavesDemo::DrawScene() {
  md3dImmediateContext->ClearRenderTargetView(
      mRenderTargetView,
      reinterpret_cast<const float *>(&Colors::LightSteelBlue));
  md3dImmediateContext->ClearDepthStencilView(
      mDepthStencilView, D3D11_CLEAR_DEPTH | D3D11_CLEAR_STENCIL, 1.0f, 0);

  md3dImmediateContext->IASetInputLayout(mInputLayout);
  md3dImmediateContext->IASetPrimitiveTopology(
      D3D11_PRIMITIVE_TOPOLOGY_TRIANGLELIST);

  UINT stride = sizeof(Vertex);
  UINT offset = 0;

  XMMATRIX view = XMLoadFloat4x4(&mView);
  XMMATRIX proj = XMLoadFloat4x4(&mProj);

  D3DX11_TECHNIQUE_DESC techDesc;
  mTech->GetDesc(&techDesc);
  for (UINT p = 0; p < techDesc.Passes; ++p) {
    //
    // Draw the land.
    //
    md3dImmediateContext->IASetVertexBuffers(0, 1, &mLandVB, &stride, &offset);
    md3dImmediateContext->IASetIndexBuffer(mLandIB, DXGI_FORMAT_R32_UINT, 0);

    XMMATRIX world = XMLoadFloat4x4(&mGridWorld);
    XMMATRIX worldViewProj = world * view * proj;
    mfxWorldViewProj->SetMatrix(reinterpret_cast<float *>(&worldViewProj));
    mTech->GetPassByIndex(p)->Apply(0, md3dImmediateContext);
    md3dImmediateContext->DrawIndexed(mGridIndexCount, 0, 0);

    //
    // Draw the waves.
    //

    // 绘制水波时设置线框模式
    md3dImmediateContext->RSSetState(mWireframeRS);

    md3dImmediateContext->IASetVertexBuffers(0, 1, &mWavesVB, &stride, &offset);
    md3dImmediateContext->IASetIndexBuffer(mWavesIB, DXGI_FORMAT_R32_UINT, 0);

    world = XMLoadFloat4x4(&mWavesWorld);
    worldViewProj = world * view * proj;
    mfxWorldViewProj->SetMatrix(reinterpret_cast<float *>(&worldViewProj));
    mTech->GetPassByIndex(p)->Apply(0, md3dImmediateContext);
    md3dImmediateContext->DrawIndexed(3 * mWaves.TriangleCount(), 0, 0);

    // Restore default.
    // 还原成默认渲染状态,否则下次绘制山峰是会是线框效果
    md3dImmediateContext->RSSetState(0);
  }

  HR(mSwapChain->Present(0, 0));
}

使用动态缓冲区会带来额外的开销(需要从 CPU 内存向 GPU 内存传输数据),在实际开发中应该尽可能少地使用它。另外最新版本的 Direct3D 引入了一些新特征对动态缓冲区进行了优化:

  • 可以在顶点着色器中实现简单动画
  • 通过渲染到纹理render to texture)和顶点纹理推送vertex texture fetch)功能,可以实现完全运行在 GPU 上的水波模拟动画。
  • 几何着色器为 GPU 提供了创建和销毁图元的能力,在以前没有几何着色器时,这些工作都是由 CPU 来完成的。
* 未经同意不得转载。