Landscape Rendering


You already saw a simplified way to generate a 3D landscape with help of an .X model in the previous chapter. There you created the diffuse texture first and then created a normal map from that and finally you added a simple height map to generate the canyon for the XNA Shooter game.

For this game you are still going to use a very simplified approach to render the landscape because the development time was so short and developing a highly sophisticated landscape rendering engine alone can take months. The landscape engine in Arena Wars, for example, supports very big landscapes, but it was never used in the game, and it was a lot of work to get it optimized on the many available hardware configurations, especially because 24 different ground texture types are supported there.

To keep things simple in the racing game only one very big ground texture is used, but you also have a normal map for it and an additional detail texture to add a little detail in case you look very closely at the ground, which can happen in the game if you stay still and are close to the landscape ground. Originally I created a 4096×4096 texture, but it was very hard to constantly update a texture this big and have to manage a normal map for it too. Uncompressed this texture is about 64 MB just for the diffuse texture, and another 64 MB for the normal map. Now imagine you have 10 or more layers of each of these textures when creating them and a computer with only about 1 GB of RAM. The story does not end nicely. Even compressed as a DXT5 texture the 4096×4096 normal map was still about 12 MB and the loading process alone was too annoying when I was developing the landscape class with the help of the unit test in there. What finally stopped me from using 4096×4096 textures ever again in XNA was the content pipeline, which almost takes forever to convert textures this big into the .xnb content files.

I went down to 2048×2048 textures, which look almost as good as the 4096×4096 ones, but only take up 1/4 of the space and loading time. The normal map was even reduced to 1024×1024 a little later because it did not make much of a visual difference. Another reason to not use that big of a texture size is the Xbox 360, which has a limited amount of video memory (64 MB) and you should not load too much stuff or too big textures, or the performance will go down. Instead of creating a super-sized texture I added the detail texture to make the landscape look better when zooming in. The rendering texture to accomplish this is discussed in the next pages.

Textures

First you need a height map and an idea of how big your landscape will be. I initially wanted each landscape texel (1 pixel of a texture) to be 1 meter (3.3 feet) and by having a 4096×4096 texture the whole landscape would be 4x4 km big (about 2.5×2.5 miles). Even after the texture was reduced to 2048×2048 the landscape size stayed the same; now each texel is 2×2 meters.

So where do you get this many height values for your landscape? Just painting them yourself will probably not look very good and you don’t have a good program for that and certainly not enough time to write a custom landscape height-map editor. A good idea is to search for height maps on the Internet and try to use existing height-map data from one of the many sources that provide it (geological organizations, NASA, which even has height-map data for other planets, and so on).

For the racing game I tried to get some mountains and I mixed them in together. It was good enough for testing, but later I needed mountains around the landscape and I had to modify the landscape height map a bit to create a big mountain in the middle and mountains around the border of the landscape. The final height map result can be seen in Figure 12-5. Please note that the LandscapeGridHeights.png height map that is used for the game in the Landscapes directory has a size of only 257×257 pixels because the grid that is generated in the Landscape class only generates 257×257 vertices (which are already 66,049 vertices) for the 256*256*2 polygons (which are around 130,000 polygons). Having more polygons would slow down rendering too much, but handling 130,000 polygons is not a big problem for today’s graphic cards and the Xbox 360 handles it fine too (still several hundred frames per second).

image from book
Figure 12-5

You may notice flat areas on the height map, which are used for city areas where it is simpler to have all the buildings and objects generated at the same height as well as the palms at the side of the road. The white area in the middle is the big mountain and you can see the gray to white areas around the border of the map, which show the mountains at the border to hide the fact that you have nothing behind them.

This height map is now used together with a little bumpy grain texture to generate the normal map. Additionally you could also mix in the diffuse map as you did in the last chapter, but I changed the diffuse map so often for this landscape that the normal map is not really affected anymore by the diffuse texture. Figure 12-6 shows the used diffuse and normal maps for the landscape in the game.

image from book
Figure 12-6

Please note that I changed these textures a lot until I got them this way and I’m not completely pleased with them, but you have to stop sometime, especially if you don’t have more time to improve the landscape. The normal map, for example, looks really great from a far distance, but more closely it doesn’t have many variations and it could maybe be improved to fit better to the diffuse texture. Anyway, it looks fine in the game and I have not heard any complaints yet.

Finally, a detail map is also added for when you look closely at the landscape. You will not notice the detail map after a while, but Figure 12-7 shows the difference quite convincingly between using a detail map and not using it. You will never go without a detail texture ever again if you have such a big landscape and allow zooming in like you do for the racing game.

image from book
Figure 12-7

Rendering

If you open up the Racing Game project you can see a lot of the classes from the previous chapters, but also two new namespaces, which are discussed in this chapter: Landscapes and Tracks. There is just one class in the Landscapes namespace called Landscape (see Figure 12-8), which is responsible for rendering the landscape, all objects on it, all the tracks and track objects, and basically everything in the game except your own car. In the Mission class you are just calling the Render method of this class to perform all the rendering. For shadow mapping several helper methods are available. More details about the shadow mapping are discussed in Chapter 14.

image from book
Figure 12-8

All landscape objects are generated at the creation time of the Landscape class, especially in the constructors of the Track classes, which are used inside the Landscape class. You will take a quick look at some of the used 3D models in a second.

The first thing you have to do to be able to render your landscape is to generate it first in the constructor. Before you take a look at that you should check out the TestRenderLandscape unit test of the Landscape class in the same way the unit test was written before the class was implemented. You may also notice the other unit test, GenerateLandscapeHeightFile, which generates a level file for the landscape height map the same way you had to generate a special content file for Rocket Commander XNA levels because loading bitmap data is not possible on the Xbox 360.

  /// <summary> /// Test render landscape /// </summary> public static void TestRenderLandscape() {   TestGame.Start("TestRenderLandscape",     delegate     {       RacingGame.LoadLevel(RacingGame.Level.Beginner);       RacingGame.Landscape.SetCarToStartPosition();     },     delegate     {       if (BaseGame.AllowShadowMapping)       {         // Generate shadows         ShaderEffect.shadowMapping.GenerateShadows(           delegate           {             RacingGame.Landscape.GenerateShadow();             RacingGame.CarModel.GenerateShadow(               RacingGame.Player.CarRenderMatrix);           });         // Render shadows         ShaderEffect.shadowMapping.RenderShadows(           delegate           {             RacingGame.Landscape.UseShadow();             RacingGame.CarModel.UseShadow(               RacingGame.Player.CarRenderMatrix);           });       } // if (BaseGame.AllowShadowMapping)       BaseGame.UI.PostScreenGlowShader.Start();       BaseGame.UI.RenderGameBackground();       RacingGame.Landscape.Render();       RacingGame.CarModel.RenderCar(0, Color.Goldenrod,         RacingGame.Player.CarRenderMatrix);       // And flush render manager to draw all objects       BaseGame.MeshRenderManager.Render();       if (BaseGame.AllowShadowMapping)             ShaderEffect.shadowMapping.ShowShadows();       BaseGame.UI.PostScreenGlowShader.Show();       TestGame.UI.WriteText(2, 50, "Number of objects: "+         RacingGame.Landscape.landscapeObjects.Count);     }); } // TestRenderLandscape() 

The unit test does a lot of things; it even shows the car and all landscape objects. Even the track and all the shadow mapping and post-screen shaders are tested here to make sure they work fine together with the landscape. If you just want to test the landscape itself, just call Render of the Landscape class; that’s already enough to test the landscape rendering.

The LoadLevel method of the RacingGame class, which is actually the main class for this game, loads one of the levels. All levels currently use the same landscape, which means you don’t have to reload it. But you should check out the code for generating the landscape vertices anyway. The landscape constructor does the following things:

  • Loads the map height data from the level file and builds tangent vertices with it

  • Generates and smoothes normals for the whole landscape, and also regenerates the tangents with the new normals.

  • Sets the vertex buffer with these vertices

  • Calculates the index buffer (very similar to the landscape triangles you saw in the last chapter)

  • Sets the index buffer for the landscape rendering

  • Loads and generates all track data for the current level, including all landscape objects

  • And finally, adds additional objects like the city ground planes to give the city objects a better looking ground texture.

The most important part of the constructor is the tangent vertex generation from the height-map data, which goes through all 257×257 points of the height map and generates vertices for you:

  // Build our tangent vertices for (int x = 0; x < GridWidth; x++)   for (int y = 0; y < GridHeight; y++)   {     // Step 1: Calculate position     int index = x + y * GridWidth;     Vector3 pos = CalcLandscapePos(x, y, heights);     mapHeights[x, y] = pos.Z;     vertices[index].pos = pos;     // Step 2: Calculate all edge vectors (for normals and tangents)     // This involves quite complicated optimizations and mathematics,     // hard to explain with just a comment. Read my book :D     Vector3 edge1 = pos - CalcLandscapePos(x, y + 1, heights);     Vector3 edge2 = pos - CalcLandscapePos(x + 1, y, heights);     Vector3 edge3 = pos - CalcLandscapePos(x - 1, y + 1, heights);     Vector3 edge4 = pos - CalcLandscapePos(x + 1, y + 1, heights);     Vector3 edge5 = pos - CalcLandscapePos(x - 1, y - 1, heights);     // Step 3: Calculate normal based on the edges (interpolate     // from 3 cross products we build from our edges).     vertices[index].normal = Vector3.Normalize(       Vector3.Cross(edge2, edge1) +       Vector3.Cross(edge4, edge3) +       Vector3.Cross(edge3, edge5));     // Step 4: Set tangent data, just use edge1     vertices[index].tangent = Vector3.Normalize(edge1);     // Step 5: Set texture coordinates, use full 0.0f to 1.0f range!     vertices[index].uv = new Vector2(       y / (float)(GridHeight - 1),       x / (float)(GridWidth - 1));   } // for for (int) 

You can see that this code generates the vertices in five steps. First, the position vector is calculated. Then all edge vectors are calculated for constructing the normal from three cross products and assigning the tangent. Finally, the texture coordinates are assigned, but you flip x and y to make xy rendering easier later, but still having the correct texture map alignment the same way it looks as a bitmap. The vertices list is already generated when it is defined because you only support 257x257 height-map grids. The CalcLandscapePos helper method that is used here is quite simple and just extracts a height vector from the height-map data:

  private Vector3 CalcLandscapePos(int x, int y, byte[] heights) {   // Make sure we stay on the valid map data   int mapX = x < 0 ? 0 : x >= GridWidth ? GridWidth - 1 : x;   int mapY = y < 0 ? 0 : y >= GridHeight ? GridHeight - 1 : y;   float heightPercent = heights[mapX+mapY*GridWidth] / 255.0f;   return new Vector3(     x * MapWidthFactor,     y * MapHeightFactor,     heightPercent * MapZScale); } // CalcLandscapePos(x, y, texData) 

With all the vertices and indices generated now you can finally render the landscape with the help of the LandscapeNormalMapping.fx shader. You will not believe how easy that is now. The following lines of code will render 130,000 polygons with the LandscapeNormalMapping shader using the landscape diffuse map, the normal map, and the additional detail map:

  // Render landscape (pretty easy with all the data we got here) ShaderEffect.landscapeNormalMapping.Render(   mat, "DiffuseWithDetail20",   delegate   {     BaseGame.Device.VertexDeclaration =       TangentVertex.VertexDeclaration;     BaseGame.Device.Vertices[0].SetSource(vertexBuffer, 0,       TangentVertex.SizeInBytes);     BaseGame.Device.Indices = indexBuffer;     BaseGame.Device.DrawIndexedPrimitives(       PrimitiveType.TriangleList,       0, 0, GridWidth * GridHeight,       0, (GridWidth - 1) * (GridHeight - 1) * 2); }); 

The ShaderEffect class from Chapter 7 allows you to render the landscape material with the specified technique using the RenderDelegate code for rendering.

The Render method of the Landscape class also renders the track and all landscape objects. The track rendering is discussed for the rest of this chapter. We will not talk about all the landscape models used in the game because there are so many. Check out the TestRenderModels unit test of the Model class to view them all; a quick overview is shown in Figure 12-9.

image from book
Figure 12-9

Optimizing Tips

Handling landscape engines is not easy. Even if you have developed a great looking landscape engine that supports many different shaders and texture sets, you might find yourself worrying about performance for a while. On the other hand, if you already have a great performing landscape engine like the one from the racing game or the shoot-’em-up game from the last chapter, you might want to improve the visual quality of it without affecting the performance too much. It is a tricky challenge getting a landscape engine just right for your game. As I told you earlier, in the past I tried to create a more complex landscape and graphics engine than actually required for a game like Arena Wars. It was possible to create huge landscapes 100 times as big as the ones that were finally used, but it did not make sense after a while optimizing these cases when the game never utilizes these features.

Instead you should focus on what the game is doing. Take the racing game as an example. It is much easier to have one fixed landscape, which is always the same. Testing is easier and you can reuse existing parts of the landscape and the locations of many objects for other levels too without having to redesign everything. The landscape is also big enough (4096×4096 meters) for many different tracks because the first level only shows you a small part of it (maybe 20%–30%).

There are three disadvantages to a more-or-less fixed landscape rendering technique like used here and in the last chapter:

  • You can’t just change the size of the landscape that easily. It will involve a lot of work just to make it smaller or bigger. If you are already using 4096×4096 textures you can probably not go up anymore to improve the texture quality, and if your game is very close to the ground it may still look very blurry even with extra detail textures.

  • Changing the diffuse texture map is really hard. You not only have to mix all different texture types together yourself, but it is also hard to see the result and it involves a lot of testing. To make things worse the bigger the mega-texture gets, the slower the development time gets.

  • The landscape engine is just not powerful enough to handle advanced effects like adding craters or extra textures (tracks, roads, leaves, riffs, and so on) to the ground and you can’t even change the visual appearance dynamically. This means the landscape will always look the same. If you wanted to build a level editor you would need a more dynamic solution that would allow you to change the texture types and a way to mix them together automatically.

The best way to get around these issues is to use a landscape rendering technique called splatting, which basically takes a bunch of textures and renders them based on a texture map that uses the same resolution as the height map onto the landscape. Because it would not look very good to have different ground textures sitting side by side, you need a smooth interpolation between them. You could either fade from tile to tile, but that would still look too harsh, or you could save percentage values for each texture type.

The landscape is rendered separately for all ground texture types and you should make the lowest one completely opaque to make sure you can never look through the landscape. Then the other ground types are alpha blended on top of the previous ones until you are done with all of them (see Figure 12-10).

image from book
Figure 12-10

You can also use an alpha texture for blending or just blend softly with color values (see the example in Figure 12-10). Because you will use shaders in XNA you might also want to combine four (pixel shader 1.1) or even eight (pixel shader 2.0) textures together into one shader pass to optimize performance. If your landscape engine needs to handle more textures, however, you will need multiple passes until every thing is rendered. Sometimes it can even be faster not to render all textures on top of each other, but just the ones that are visible. It becomes harder when you have to generate the vertex and index buffers, but if you really have a lot of textures, like 20 or 30 different ground types, the performance will greatly increase because each ground texture type is used only 10% or less and it does not make sense to render it all the time.

Whichever technique you choose, I strongly suggest you start with something simple like just rendering a big landscape with one texture type tiled over it and then improve it later when your game is more than just a landscape engine. You might also want to make sure your landscape can always be rendered at several hundred frames to make sure the game still runs fast enough after you add all the landscape models, 3D units, and effects plus the game logic into it.

There are many more tips and tricks about creating good landscapes. You could implement precalculated lighting and huge landscape shadows; there are also many opportunities for great looking shaders, especially if you have water involved. Grass could be done with fur shaders. The 3D appearance of rocks and cliffs could be enhanced with parallax or even offset mapping. If you are interested in these topics please pick up a good shader book like the Game Programming Gems or ShaderX, or search the Internet for many tips and tricks as well as tutorials.

Figure 12-11 shows the final appearance of the landscape in the racing game after also applying the post-screen shaders, the in-game HUD, and the sky cube mapping (see Chapters 5 and 6 for more information about these kinds of shaders if you have forgotten about them).

image from book
Figure 12-11




Professional XNA Game Programming
Professional XNA Programming: Building Games for Xbox 360 and Windows with XNA Game Studio 2.0
ISBN: 0470261285
EAN: 2147483647
Year: 2007
Pages: 138

flylib.com © 2008-2017.
If you may any questions please contact us: flylib@qtcs.net