Terrain Mesh Generation
In order to obtain an “infinite” landscape, a large majority of the terrain generation is driven through Perlin Noise, and some procedural path generation using some flow fields. A
GameSeed class allows us to recreate a game’s environment, paths, etc by altering the internal random number generator’s seed value when the game first starts.
The world-building class handles rendering chunks of the world using Marching Cubes based on generated Perlin values. Essentially, the generated noise serves as a heightmap across the world. The marching cubes algorithm is performed for each chunk, checking against the height/noise values provided by our Perlin-based height functions, and generating a mesh accordingly.
Terrain Design Features
In order to achieve a certain design aesthetic, the terrain generation requires the ability to be tuned and adhere to a set of rules when generating the world. The below screenshot demonstrates some terrain features we were interested in for the world:
An important note is that since the terrain generation essentially treats the perlin noise values as a heightmap, there is no way to create “layered” terrain. That is, we could not create terrain featuring caves or overhangs. This did not affect the game design, and actually made programming a bit easier as the world could be regarded as a simple 2D plane - see plots ‘n’ paths for an example.
Water, Ponds, and Lakes
The water is a very simple trick - a plane is fixed to move with the camera but remains at a constant “sea level.” This value is later referenced in code to quickly determine if a location is underwater or not. The plane’s shader is responsible for handling the stylized water reflection and general look, so it’s essentially set-and-forget.
Mountains are simply different sets of posterized perlin noise combined together, and then added into the generic ‘terrain height’ noise value.
Depending on the type of mountain, an arbitrary threshold is applied so noise values below a certain threshold are ignored (thus, flat), and then any remaining noise values are scaled to create tall mountains.
In code, the final height for any given terrain is the combination of the above mountain functions, plus the base terrain formula. This allows us to take the “2D world” approach mentioned earlier, as we can easily generate the height for any point in the world through a function call.
After the chunk has been marched, a pass is done across all of its vertices, setting the
y position according to the generated height for that world point. This essentially smooths out the topography of the terrain mesh while utilizing the vertices created by marching cubes.
Note that this would not be possible if our terrain was not driven by Perlin values - smoothing chunk edges becomes a challenge on its own without the affordance of the 2D noise values. In earlier attempts at terrain smoothing, it was found that smoothing terrain cross chunks became quite an issue:
An interesting thing to note is that marching cubes creates a mesh with duplicate vertices, and the mesh must be “welded” (or converted to share vertices) in order for smoothing to look good. Welding the mesh also allows for normal sharing, which gives a “smoother” look to the landscape’s lighting and shader effects. Shown here is the ‘default’ output compared to after sharing vertices.
Handoff to Path/Plot systems
Once the initial terrain is generated, the paths and plots systems take over to handle creating paths through the world and plant trees/grass throughout the world.