Terrain Editor

Info

Project Type: Godot, Standalone Application

Project Timeframe: 2 Months, 2025

Sara/terrain-editor

Product Overview

Architecture

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.

Mesh Generation

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

Multi-threading

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

Lazy Loading Levels of Detail

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