This time around, we are going to revisit our old friend, the Waves Demo, and add textures to the land and water meshes. We will also be taking advantage of the gTexTransform matrix of our Basic.fx shader to tile our land texture multiple times across our mesh, to achieve more detail, and use tiling and translations on on our water mesh texture to create a simple but very visually appealing animation for our waves. This demo corresponds to the TexturedHillsAndWaves demo from Chapter 8 of Frank Luna’s Introduction to 3D Game Programming with Direct3D 11.0. You can download the full source for this example from my GitHub repository at https://github.com/ericrrichards/dx11.git.
Here is a still of our finished project this time:
I’m also going to try to upload a video of this demo in action, as the static screenshot doesn’t quite do justice:
There is actually not a great deal of change from our last iteration of this example, the Lit Terrain Demo. We will add the two new textures, as well as some member variables to control our water animation. We have already done the work to calculate the per-vertex texture coordinates for our meshes previously, although we did not highlight it at the time. Beyond that, we have merely to change our UpdateScene function to scroll the water texture, and be sure to set the correct texture effect variables in our DrawScene function.
New Members
As I mentioned, we will be adding the two new textures as ShaderResourceViews. We will also need a matrix for each to transform the base texture coordinates of each mesh. Lastly, we will create a member to maintain the offset that we will scroll the water texture from frame to frame.
private ShaderResourceView _grassMapSRV; private ShaderResourceView _wavesMapSRV; private Matrix _grassTexTransform; private Matrix _waterTexTransform; private Vector2 _waterTexOffset;
We will create our _grassTexTransform and the _waterTexOffset in our constructor. We will tile our small grass texture 5x5 over our land mesh. This will give us a much nicer appearance than if we tried to stretch the texture over the entire mesh without tiling, as the texture would seem very blurry and without detail when we zoomed in.
_waterTexOffset = new Vector2();
_grassTexTransform = Matrix.Scaling(5.0f, 5.0f, 0.0f);
We will create our textures in our Init() function in the same way that we created the crate texture for our previous texturing example, the CrateDemo.
public override bool Init() { if (! base.Init()) return false; _waves = new Waves(); _waves.Init(160, 160, 1.0f, 0.03f, 3.25f, 0.4f); Effects.InitAll(Device); InputLayouts.InitAll(Device); _grassMapSRV = ShaderResourceView.FromFile(Device, "Textures/grass.dds"); _wavesMapSRV = ShaderResourceView.FromFile(Device, "Textures/water2.dds"); BuildLandGeometryBuffers(); BuildWaveGeometryBuffers(); return true; }
Creating the Land Mesh with Texture Coordinates
Creating the mesh for this example with texturing is almost exactly the same as before. Our GeometryGenerator.CreateGrid() function already creates texture coordinates for each mesh suitable for stretching a texture once across the mesh, so we have only to change our vertex structure from VertexPN to Basic32 and supply the texture coordinate calculated by GeometryGenerator. Note that we also need to use the correct vertex stride when we create our BufferDescription for the vertex buffer; otherwise, you will receive errors about writing past the end of the buffer, as the Basic32 structure is larger than our VertexPN structure.
private void BuildLandGeometryBuffers() { var grid = GeometryGenerator.CreateGrid(160.0f, 160.0f, 50, 50); _landIndexCount = grid.Indices.Count; var vertices = new List<Basic32>(); foreach (var v in grid.Vertices) { var p = new Vector3(v.Position.X, GetHillHeight(v.Position.X, v.Position.Z), v.Position.Z); var n = GetHillNormal(p.X, p.Z); vertices.Add(new Basic32(p, n, v.TexC)); } var vbd = new BufferDescription(Basic32.Stride * vertices.Count, ResourceUsage.Immutable, BindFlags.VertexBuffer, CpuAccessFlags.None, ResourceOptionFlags.None, 0); _landVB = new Buffer(Device, new DataStream(vertices.ToArray(), false, false), vbd); var ibd = new BufferDescription(sizeof(int) * grid.Indices.Count, ResourceUsage.Immutable, BindFlags.IndexBuffer, CpuAccessFlags.None, ResourceOptionFlags.None, 0); _landIB = new Buffer(Device, new DataStream(grid.Indices.ToArray(), false, false), ibd); }
Animating the Water Mesh
We will update our wave function through our Waves class in the same way that we did previously. We will need to calculate the texture coordinate for each vertex when we update the vertex buffer. Note that we offset by 0.5 texel units, since our wave mesh has its coordinates specified in the ranges [-width/2, width/2] and [-depth/2, depth/2]. We will use the texture transform to scale the water texture 5x5 over the mesh, the same as with the land mesh and texture. Lastly we will scroll the texture diagonally across the mesh as a function of the time using a translation matrix. Since we are using AddressMode.Wrap for our texture sampler (See Basic.fx), this will be a continuous animation, and we don’t need to worry about “running out” of texture.
public override void UpdateScene(float dt) { // camera update code omitted... // update waves if ((Timer.TotalTime - _tBase) >= 0.25f) { _tBase += 0.25f; var i = 5 + MathF.Rand() % (_waves.RowCount - 10); var j = 5 + MathF.Rand() % (_waves.ColumnCount - 10); var r = MathF.Rand(1.0f, 2.0f); _waves.Disturb(i, j, r); } _waves.Update(dt); // update waves vertex data var mappedData = ImmediateContext.MapSubresource(_waveVB, 0, MapMode.WriteDiscard, MapFlags.None); for (int i = 0; i < _waves.VertexCount; i++) { mappedData.Data.Write(new Basic32( _waves[i], _waves.Normal(i), new Vector2(0.5f+_waves[i].X/_waves.Width, 0.5f - _waves[i].Z / _waves.Depth))); } ImmediateContext.UnmapSubresource(_waveVB, 0); // tile water texture var wavesScale = Matrix.Scaling(5.0f, 5.0f, 0.0f); // translate texture over time _waterTexOffset.Y += 0.05f * dt; _waterTexOffset.X += 0.1f * dt; var wavesOffset = Matrix.Translation(_waterTexOffset.X, _waterTexOffset.Y, 0); _waterTexTransform = wavesScale * wavesOffset; }
Lastly, note that we create the combined transform by multiplying the scale by the translation, and not vice-versa. This is important. Because the scaling matrix operates with respect to the origin, if we translate the object first, we will get a scaled object, but we will also scale the magnitude of the offsets from the origin as well, whereas if we scale the object first, then translate, we are essentially setting the size of the object, then positioning it. A picture makes this 1000 times clearer…
Drawing
The last hitch is to remember to set the correct InputLayout for our vertex structure, Basic32, and to set the texture and texture transform before we render in DrawScene()
Effects.BasicFX.SetTexTransform(_grassTexTransform); Effects.BasicFX.SetDiffuseMap(_grassMapSRV); var pass = activeTech.GetPassByIndex(p); pass.Apply(ImmediateContext); ImmediateContext.DrawIndexed(_landIndexCount, 0, 0);
Next Time
Up next, we’ll have a more original example, as we are finished with the book demos for Chapter 8. There is, however, an interesting exercise at the end of the chapter, which asks us to render our cube texture from the CrateDemo with a series of textures representing a flame animation. The exercise asks us to load each of the frames of this animation as a separate texture, and swap them each frame. This is horribly inefficient, however, as you would be constantly moving textures from the GPU to memory, so it is not a very realistic example. Instead, we will build a class to create and manage a texture atlas, so that we can store all the frames of the animation in a single texture, and select the individual frames using our texture coordinate transform. It is perhaps overkill for what we are doing, given the way that we set the texture each frame anyway, but where this technique would shine is if we composed the textures for all of our objects into a single texture, as we could then render all objects in a rendering pass without switching textures. This is, in fact a technique that many commercial games have used, notably Id Software’s Quake II.
No comments :
Post a Comment