The QuadTerrain LOD (Quadtree Terrain Level of Detail Algorithm) is finished! Actually it's been finished for a while, but the official announcement is this thread. The original thread is over here.
Those who have been following the original thread know what I've been up to, but I'll reiterate and explain this project here in tutorial format.
For those not interested, the summary is this: you can use the downloadable files at the bottom of the page to create a massive terrain complete with Quadtree LOD and easily import it as the ground for your game! Multitexturing, normal mapping and detail mapping are all included in the shader.
The Basics
A terrain is an object or mesh which represents the 'ground' in an outdoor environment. Because it's normally orientated along a flat plane, most terrains can be represented by a heightmap or heightfield, and it is fairly usual to generate the terrain mesh from the heightmap itself.
This is exactly what is done in the Generated Geometry Sample, and in Riemers Series 1 and Series 4.
But a terrain is a very large and complex object. Generating one from a 1024x1024 heightmap results in over a million vertices, which is far too many for an efficient real-time game element.
This is where Level Of Detail (LOD) algorithms come in. An LOD algorithm renders distant objects with less detail, and thus fewer vertices, than close objects. After all, there's no point rendering every rock on a distant cliff face!
There are many different styles of terrain LOD: ROAM, Quadtree, Geomipmapping... even Brute Force (where you just render everything) is a valid method for a good graphics card and a low detail terrain.
This project makes use of a Quadtree method.
The QuadTree
A quadtree is a hierarchical data structure where every object in the structure has 4 children. In 2D space, this translates to a square cut into 4 squares, each of which is cut into 4 more squares, and so on.
In the context of a terrain, suppose you were to accompany each square (quad-nodes) with a mesh, which covers the same area that the square covers. Larger quadnodes would cover large areas, but would have low detail, while small nodes would only cover the immediate area but would render with lots of detail. You could then start at the top 'root' node, and sink down through the tree, sinking further where more detail is needed.
By only rendering the meshes associated with the nodes we sink to (known as 'leaf' nodes), we can cut the detail level of nodes very quickly, especially since we don't need to check any nodes below a leaf node.
The Stitching
Of course, nothing is perfect. Two same sized quadnode meshes next to each other will fit seamlessly, but imagine a high detail node next to a low detail node. The result will be artifacts known as gaps.
This can be fixed by 'stitching', where you either add or remove edge vertices from the nodes so that they match each other. In this case, I removed vertices from the higher detail node, creating a triangle pattern which connects to the lower detail node. In this screenshot, you can see it applied to every edge of every quadnode. In this one, it is applied correctly.
Of course, this isn't as simple as it sounds: each quadnode is now accompanied by 9 meshes, 1 for no stitching, 4 for a single edge stitched, and another 4 for two stitched edges. Since these are generated at runtime, however, they aren't a problem.
The Bounding Frustrums
It isn't only distant objects need to be LODd. Nearby nodes which are not in view because you are looking in the opposite direction need not be rendered at all.
By doing a collision test between the camera's "Bounding Frustrum" (a 3d shape representing the cameras viewport) and a quadnodes bounding box, we can determine which nodes are outside the view and quickly cut a whole heap of geometry from the render. By combining this with the distance testing, we can stop that from sinking further into off-screen nodes as well!
The Multiple Vertex Buffers
One of the things I spent a lot of time on was converting the terrain to run with more than one vertex buffer.
The vertex buffer contains all the vertices read off of the heightmap. In general, it's best to use a single buffer, because each buffer must be rendered with a separate Draw call. Unfortunately, there is a maximum limit to the number of vertices that can be rendered at once on the graphics card, and going above this limit will force the call to be rendered on the CPU (resulting in death by frame rate).
This limit varies, but on my home computer over a million vertices (a 1024x1024 terrain) is just a bit too much.
So, I implemented a nasty and complex algorithm to separate the terrain into a number of vertex buffers, based on the idea that each quadnode could be entirely contained within a parent vertex buffer if we split it correctly. This (eventually) came out well: it is now possible to split your terrain based on a size value. A 1024x1024 terrain split by 512 vertex buffers will come out as 4, 512x512 vertex buffers, with a fifth for all quadnodes which cover an area greater than 512 (only the root node, in this case).
It's worth noting that I may have managed this whilst drunk, tired or otherwise incapacitated, as I cannot remember actually coding it, and have no really idea how or why it works. But it does, and seems to be bug free.
The Global Normal Map
This shader was my first attempt at HLSL (High Level Shader Language), and came out brilliantly in my opinion.
Dynamically changing the geometry can have a nasty effect: although the shape of a low detail node may be very similar to that of a high detail node, the shading can result in a fairly large difference in appearance if done per vertex.
To solve this, I took advantage of the power of HLSL, and told the terrain shader to use a global normal texture rather than per vertex normals. The advantage was twofold: My vertex buffers halved in size, and more importantly shading no longer changes between high and low detail quadnodes.
The Detail Normal Map
Initially, I didn't understand the mathematics behind normal mapping, and thought that by simply adding a detail normal map value to my global value in the shader I could create detail normal mapping. The result looked OK, but it wasn't accurate: the detail normal 'pulled' the global normal upwards. When I tested with a high strength value for the detail normal, this was instantly apparent: the entire terrain was shaded as if it was a lot flatter than it truly was.
It wasn't until after I'd finished the multitexturing that I worked out how to fix this, but when I did the difference was apparent (see below).
The Multitexuring
Using a single texture for the entire terrain has two problems: the terrain is too big to be covered with a single texture with a high enough resolution, and tiling the same ground texture over the entire terrain is very bland. Therefore, I went for a multitexturing approach that made use of a blend texture.
In short, mutitexturing is using more than one texture on the same object, and a blend texture uses it's red, green and blue channels to define the amount of each texture to show. In the sample, Blue represents sand, Green - grass, Red - Foliage and Black displays as Rock.
Quickly back on the subject of Detail Normal Mapping: you can see the difference between my original method, and my final method.
The Versatility
I've done my best to make the QuadTerrain class as versatile as possible, and as a result the different features and the reason for them may take a bit of explaining.
The constructor looks like this:
| QuadTerrain q = new QuadTerrain(GraphicsDevice, Effect, HeightMap, QuadNodeSize, VertexBufferSize, XZScale, YScale); |
- GraphicsDevice - speaks for itself. Plug the Graphics device into this field to provide the Quadterrain a link to it.
- Effect - You will need to load the TerrainShader with Content.Load, and provide the resulting effect as this parameter to establish a link between the two.
- Heightmap - Load your Heightmap as a Texture2D, and provide it as this parameter.
- IMPORTANT: To create the quadtree structure, your heightmap's dimensions must be square, and of the format 2n+1.
- Examples of good dimensions are 257x257, 513x513 and 1025x1025.
- QuadNodeSize - The most important versatility element of the class, QuadNodeSize sets the number of vertices along each edge of each Quadnode.
- Increasing the size of this value will decrease the aggressiveness of the LOD, resulting in less CPU work (because the quadtree is not as deep) and a shorter loading time, at the expense of more GPU work (because there will be more polygons to render). A good idea if you are CPU bound.
- Decreasing the size will increase the aggressiveness of the LOD, and will decrease the amount of work being performed on the GPU. The trade-off, as you may have guessed, will be CPU work and a longer loading time. A good idea if you are GPU bound.
- The absolute minimum value for this field is 5: any less will result in an error. The absolute maximum was never tested, but it kind of ruins the whole point of LOD to use a value close to the size of the terrain.
- IMPORTANT: Like the heightmaps dimensions, this value must be of the format 2n+1.
- Examples of good values include 17, 33 and 64.
- VertexBufferSize - If this value is less than the size of the heightmap, the quad terrain will be split into several vertex buffers, each of which will be rendered with a different draw call.
- In general, a value the same size as the heightmap is recommended, but for a large heightmap it may be necessary to split the Vertex buffer.
- You can get the number of vertices supported on your graphics card with:
- device.GraphicsDeviceCapabilitie.MaxVertexIndex
- IMPORTANT: Like the last one, this value must be of the format 2n+1.
- Examples of good values include the size of your heightmap. :D
- XZScale - This is simply the number of distance units between the horisontal positions of consecutive vertices.
- Terrain scaling is a great way to increase the size of the terrain without adding extra load. Consider using a smaller heightmap and increasing this value to compensate if you're having trouble milking good performance out of your game.
- YScale - This is the terrain height scaling factor.
- Making this larger will increase the height of your hills and mountains, but be wary: because the heightmap colour can only be an integer between 0 and 255, making this value too high might make smooth changes between hills and flat area's impossible.
The Update command also has a few important fields:
| q.UpdateTerrain(CameraPosition, BoundingFrustrum, LODLevel); |
- CameraPosition - You must plug in a Vector3 representing the XYZ co-ordinates of your camera to allow the terrain to update. This is because the LOD has to know which quadnodes are distant and which ones are near to status them ready for rendering.
- BoundingFrustrum - For the frustrum tests, the terrain needs to know the BoundingFrustrum for the camera. Plug it into this field.
- LODLevel - A scaling factor which is applied to the distance checks which determine the distance a node must be away from the camera to be considered 'not detailed enough'.
- This value is contained in the update command because, unlike the values in the constructor, the terrain is capable of changing it at runtime. Thus, if your game is running slowly (or if you don't have any performance issues at all), you can dynamically change it to gain performance or improve the visual quality of the terrain.
The Final
Yippy Skippy, the Evil!
The sample is below. To use it in your own game, copy QuadTerrain.cs and TerrainShader.fx into your project, Load the shader, some textures of your own, and add the constructor and update methods! I highly recommend you don't try to build up off of the sample, because it's not great code.
If you want to go into the QuadTerrain.cs code and mess about, go for it! This is my first 'proper' XNA project, so there's no reason to assume I got it all right. Any feedback you can give will be considered and appreciated.
If you use this class (or a modification of it) in your game, give a post in this thread and let me know! I'd love to see what people are doing with it! Mentioning me in your game credits would be nice too, given the time and effort it took me to build this, but you don't have to. :)
And for my final point: The QuadTerrain class is not an excuse to avoid learning how to use Vertex and Index Buffers! Visit Riemers XNA tutorials, Ziggyware, Shawns Blog and all the other sites I can't remember off the top of my head, go though the samples, read and participate in the forums and learn everything you can, and the games you create will be so much better for it!
Cheers!
Quasar.