1
0
Fork 0
objectionable.solutions/sara.objectionable.solutions/projects/terrain-editor.html
2026-04-08 19:20:04 +02:00

123 lines
7.4 KiB
HTML

<!DOCTYPE html>
<html>
<head>
<title>Terrain Editor - Sara Gerretsen</title>
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<script type="text/javascript" src="../shared/jquery.min.js"></script>
<link rel="stylesheet" href="../shared/style.css">
<div id="site-header">
<script>$(function(){$("#site-header").load("../shared/header.html");});</script>
</div>
</head>
<body>
<h1>Terrain Editor</h1>
<section class="project">
<h2>Info</h2>
<div indented>
<p>Project Type: Godot, Standalone Application</p>
<p>Project Timeframe: 2 Months, 2025</p>
</div>
<video height="500" style="max-width:100%" style="max-width:100%" muted autoplay controls>
<source src="../assets/terrain-editor.mp4">
</video>
<a class="git-block" href="https://git.objectionable.solutions/Sara/terrain-editor" target="_blank">
<div class="git-logo"></div><h2>Sara/terrain-editor</h2>
</a>
<h2>Product Overview</h2>
<ul>
<li>Vector terrain editor</li>
<li>CSG-inspired</li>
<li>Nondestructive</li>
<li>Standalone application</li>
</ul>
<h2>Architecture</h2>
<img src="../assets/terrain-editor-uml.svg" style="width:100%"/>
<p>At the core of the architecture is the
<a href="https://git.objectionable.solutions/Sara/terrain-editor/src/branch/development/modules/terrain_editor/terrain_mesh_generator.h">TerrainMeshGenerator</a>,
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
<a href="https://git.objectionable.solutions/Sara/terrain-editor/src/branch/development/modules/terrain_editor/terrain_chunk.h">TerrainChunk</a> nodes.</p>
<p>The actual shape of the terrain is defined by the
<a>primitives</a>
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.</p>
<p>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.</p>
<h2>Mesh Generation</h2>
<section class="split">
<div>
<p>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.</p>
<p>It will generate an evenly spaced vertex grid and assign UV coordinates, evaluate each primitive in order, and set the height.</p>
</div>
<a href="https://git.objectionable.solutions/Sara/terrain-editor/src/commit/32c6c7529e9380965d59c9a6bf2161a67f2bd1e7/modules/terrain_editor/terrain_mesh_generator.cpp#L211" style="flex: 0 0 50%">
<img src="../assets/code/terrain-editor/TerrainMeshGenerator_generate_grid.png" style="width:100%">
TerrainMeshGenerator::generate_grid
</a>
</section>
<section class="split">
<div>
<p>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.</p>
</div>
<a href="https://git.objectionable.solutions/Sara/terrain-editor/src/commit/32c6c7529e9380965d59c9a6bf2161a67f2bd1e7/modules/terrain_editor/terrain_mesh_generator.cpp#L188" style="flex:0 0 50%">
<img src="../assets/code/terrain-editor/TerrainMeshGenerator_face_from.png" style="width:100%">
TerrainMeshGenerator::face_from
</a>
</section>
<h2>Multi-threading</h2>
<section class="split">
<div>
<p>Execution on the TerrainMeshGenerator is split across two threads.
As it's a node it gets a main-thread <code>NOTIFICATION_PROCESS</code> every frame, along with all the regular notifications its configured for.
Meanwhile on the second thread, created on <code>NOTIFICATION_ENTER_TREE</code>, it will work through the queue of
<a href=https://git.objectionable.solutions/Sara/terrain-editor/src/commit/32c6c7529e9380965d59c9a6bf2161a67f2bd1e7/modules/terrain_editor/terrain_mesh_generator.h#L17>TerrainMeshTask</a>
objects. Generating a surface description for each, and afterwards pushing the completed task onto an output queue.
<p>When no work is on the list, it will stop and wait for the
<a href="https://git.objectionable.solutions/Sara/terrain-editor/src/commit/32c6c7529e9380965d59c9a6bf2161a67f2bd1e7/modules/terrain_editor/terrain_mesh_generator.h#L74">work_signal</a>
Semaphore. Ensuring that the thread never idly spins.</p>
</div>
<a href="https://git.objectionable.solutions/Sara/terrain-editor/src/commit/32c6c7529e9380965d59c9a6bf2161a67f2bd1e7/modules/terrain_editor/terrain_mesh_generator.cpp#L134" style="flex: 0 0 50%">
<img src="../assets/code/terrain-editor/TerrainMeshGenerator_background_generation_thread.png" style="width:100%;">
TerrainMeshGenerator::background_generation_thread
</a>
</section>
<section class="split">
<div>
<p>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.</p>
<p>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.</p>
</div>
<a href="https://git.objectionable.solutions/Sara/terrain-editor/src/commit/32c6c7529e9380965d59c9a6bf2161a67f2bd1e7/modules/terrain_editor/terrain_mesh_generator.cpp#L40" style="flex:0 0 50%">
<img src="../assets/code/terrain-editor/TerrainMeshGenerator_process.png" style="width:100%">
TerrainMeshGenerator::process
</a>
</section>
<h2>Lazy Loading Levels of Detail</h2>
<section class="split">
<div>
<p>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 <code>NOTIFICATION_PROCESS</code>, they will push their currently needed LoD mesh onto the queue.</p>
</div>
<a href="https://git.objectionable.solutions/Sara/terrain-editor/src/commit/32c6c7529e9380965d59c9a6bf2161a67f2bd1e7/modules/terrain_editor/terrain_chunk.cpp#L57" style="flex:0 0 50%">
<img src="../assets/code/terrain-editor/TerrainChunk_process_lod.png" style="width:100%">
TerrainChunk::process_lod
</a>
</section>
<section class="split">
<div>
<p>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.</p>
</div>
<a href="https://git.objectionable.solutions/Sara/terrain-editor/src/commit/32c6c7529e9380965d59c9a6bf2161a67f2bd1e7/modules/terrain_editor/terrain_mesh_generator.cpp#L254" style="flex: 0 0 50%">
<img src="../assets/code/terrain-editor/TerrainMeshGenerator_push_task.png" style="width:100%">
TerrainMeshGenerator::push_task
</a>
</section>
</section>
</body>
</html>