Dynamic Vertex Buffers: Waves Demo
We will need to add on to our Hills Demo in three areas to add in our wave mesh:
- Init() – We will be adding a new helper function, BuildWavesGeometryBuffers() to create the geometry for our ocean mesh.
- UpdateScene() – We will add code to animate our ocean mesh, and update the vertex buffer on each frame.
- DrawScene() – We will add the code to render the new ocean mesh. Since we will be rendering the mesh as a wireframe, we will need to set the wireframe render state before drawing the ocean mesh, and revert to the default render states after we are finished; otherwise, we would end up rendering our hills in wireframe, too, after the first frame.
Additionally, we will be adding a helper class to compute the heights for each vertex in the ocean mesh, using a wave equation.
BuildWavesGeometryBuffers()
We have added a new vertex and index buffer for our ocean mesh. Since we are going to be updating the mesh each frame, we specify ResourceUsage.Dynamic and CpuAccessFlags.Write when we create the vertex buffer. We also create the buffer without any initial data. The index buffer can be immutable, as the indexes will not change.
private void BuildWavesGeometryBuffers() { var vbd = new BufferDescription(Vertex.Stride * _waves.VertexCount, ResourceUsage.Dynamic, BindFlags.VertexBuffer, CpuAccessFlags.Write, ResourceOptionFlags.None, 0); _wavesVB = new Buffer(Device, vbd); var indices = new List<int>(); var m = _waves.RowCount; var n = _waves.ColumnCount; for (int i = 0; i < m-1; i++) { for (int j = 0; j < n-1; j++) { indices.Add(i*n+j); indices.Add(i*n+j+1); indices.Add((i+1)*n+j); indices.Add((i + 1) * n + j); indices.Add(i * n + j + 1); indices.Add((i + 1) * n + j + 1); } } var ibd = new BufferDescription(sizeof(int) * indices.Count, ResourceUsage.Immutable, BindFlags.IndexBuffer, CpuAccessFlags.None, ResourceOptionFlags.None, 0); _wavesIB = new Buffer(Device, new DataStream(indices.ToArray(), false, false), ibd); }
UpdateScene()
In our UpdateScene function, we will update our wave equation. We will also add a new ripple to the ocean every quarter of a second. After we have updated our wave simulation, we need to lock the vertex buffer and write the new vertex positions, using ImmediateContext.MapSubresouce(). Because we don’t care about the previous vertex data, we can specify MapMode.WriteDiscard. SlimDX provides a stream-type class, DataBox, to wrap the raw pointer that we would be dealing with in C++, which is much more convenient for this case, where we are uploading an entirely new set of vertex data. Finally we unlock and upload the new data by calling ImmediateContext.UnmapSubresource.
public override void UpdateScene(float dt) { // camera update code omitted... if ((Timer.TotalTime - _tBase) >= 0.25f) { _tBase += 0.25f; var i = 5 + MathF.Rand() % 190; var j = 5 + MathF.Rand() % 190; var r = MathF.Rand(1.0f, 2.0f); _waves.Disturb(i, j, r); } _waves.Update(dt); var mappedData = ImmediateContext.MapSubresource(_wavesVB, 0, MapMode.WriteDiscard, MapFlags.None); for (int i = 0; i < _waves.VertexCount; i++) { mappedData.Data.Write(new Vertex(_waves[i], Color.Blue)); } ImmediateContext.UnmapSubresource(_wavesVB, 0); }
DrawScene()
We clear the redner and depth/stencil buffers and draw the hill geometry as before. After we have drawn the hill geometry, we set the wireframe render state, set our wave vertex and index buffers, and the world matrix for our ocean mesh (I have moved the ocean mesh down a couple of units, as I feel it looks a little better). Next, we draw the ocean mesh, and finally, reset the render state to default, so that we will draw the hill geometry correctly on the next frame.
public override void DrawScene() { base.DrawScene(); ImmediateContext.ClearRenderTargetView(RenderTargetView, Color.LightSteelBlue); ImmediateContext.ClearDepthStencilView(DepthStencilView, DepthStencilClearFlags.Depth | DepthStencilClearFlags.Stencil, 1.0f, 0); ImmediateContext.InputAssembler.InputLayout = _inputLayout; ImmediateContext.InputAssembler.PrimitiveTopology = PrimitiveTopology.TriangleList; for (int i = 0; i < _tech.Description.PassCount; i++) { ImmediateContext.InputAssembler.SetVertexBuffers(0, new VertexBufferBinding(_landVB, Vertex.Stride, 0)); ImmediateContext.InputAssembler.SetIndexBuffer(_landIB, Format.R32_UInt, 0); _fxWVP.SetMatrix(_gridWorld * _view * _proj); var pass = _tech.GetPassByIndex(i); pass.Apply(ImmediateContext); ImmediateContext.DrawIndexed(_gridIndexCount, 0,0); ImmediateContext.Rasterizer.State = _wireframeRS; ImmediateContext.InputAssembler.SetVertexBuffers(0, new VertexBufferBinding(_wavesVB, Vertex.Stride, 0)); ImmediateContext.InputAssembler.SetIndexBuffer(_wavesIB, Format.R32_UInt, 0); _fxWVP.SetMatrix(_wavesWorld * _view * _proj); pass.Apply(ImmediateContext); ImmediateContext.DrawIndexed(3*_waves.TriangleCount, 0, 0); ImmediateContext.Rasterizer.State = null; } SwapChain.Present(0, PresentFlags.None); }
Waves.cs
Waves.cs contains our helper class to compute the wave equation. Luna doesn’t go into much detail on the implementation of this class, but for an explanation of the equation used, see Chapter 15 of Mathematics for 3D Game Programming and Computer Graphics by Eric Lengyel. You can find the implementation and the rest of the source code for this example at my GitHub repository, https://github.com/ericrrichards/dx11.git.
Next Time…
This finishes up the examples for Chapter 6. Next time, we will move onto Chapter 7 and dive into lighting, investigating the lighting equation, different types of lights, and update this example to use per-pixel lighting, rather than fixed colors.