剖析山峰河谷演示程序
2023-06-15
山峰河谷演示程序:
从演示中可以看出,本程序是在 山峰演示程序 的基础上增加了一层波纹。那么这种动态的波纹效果是怎样实现的呢?
要实现这种波纹效果,可以使用动态缓冲区(dynamic buffer)。动态缓冲区中的数据可以在每一帧中进行修改,因此使用它可以实现一些动画效果。它的思路跟前面山峰的创建类似:
- 创建 平面的网格,通过某种算法计算每一时刻 平面上每个点的高度( 坐标)。
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 来完成的。
* 未经同意不得转载。