123 lines
7.4 KiB
HTML
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>
|