Project Type: Godot, Standalone Application
Project Timeframe: 2 Months, 2025
At the core of the architecture is the TerrainMeshGenerator, which has the responsibility of dispatching mesh generation threads. It is a Node, so it is part of the scene tree. It will then instantiate a equal-sided grid of TerrainChunk nodes.
The actual shape of the terrain is defined by the primitives list of the mesh generator. This is an abstract class that can affect any point on the height-map, pulling it up or down depending on internal rules.
The actual representation of the terrain is the aforementioned terrain chunks. These inherit Godot's MeshInstance3D. Which means they have a mesh. On top of the base class' data, they keep a separate list of LOD meshes.
The mesh generation procedure runs in two phases. The first is constructing a grid of points, each at a height evaluated from the primitive list.
It will generate an evenly spaced vertex grid and assign UV coordinates, evaluate each primitive in order, and set the height.
TerrainMeshGenerator::generate_grid
The second is connecting the created points along the shortest edge. Which ensures smooth-looking edges and cliffs. As well as avoiding the jarring jagged edges that are created when generating with a fixed edge direction.
TerrainMeshGenerator::face_from
Execution on the TerrainMeshGenerator is split across two threads.
As it's a node it gets a main-thread NOTIFICATION_PROCESS every frame, along with all the regular notifications its configured for.
Meanwhile on the second thread, created on NOTIFICATION_ENTER_TREE, it will work through the queue of
TerrainMeshTask
objects. Generating a surface description for each, and afterwards pushing the completed task onto an output queue.
When no work is on the list, it will stop and wait for the work_signal Semaphore. Ensuring that the thread never idly spins.
TerrainMeshGenerator::background_generation_thread
On the main thread, the output queue of surface descriptions is processed into usable meshes, which are then committed to their respective MeshChunks. Here the queue also checks if the mesh has been re-queued, in which case the output queue is invalidated to avoid doing double work.
Because this is a three-thread synchronisation point, locking the mesh generation thread, main thread, and the render thread, this step can be very expensive. So it's best to avoid doing wherever possible. Especially when there's a chance of running the operation multiple times per frame.
TerrainMeshGenerator::process
In order to avoid clogging the mesh generation queue, LOD-levels are lazy-loaded.
When a change to the terrain data is broadcast, each chunk marks its existing LoD meshes as DIRTY.
Then on the next NOTIFICATION_PROCESS, they will push their currently needed LoD mesh onto the queue.
TerrainChunk::process_lod
In order to ensure a responsive feeling, the highest LoD meshes are always processed first. Since these are the least detailed, they can also be processed faster. From the user's perspective, the terrain shows the submitted updates almost immediately. And only the most detailed resolution feels slower.
TerrainMeshGenerator::push_task