Island Builder

Interactive whiteboxes and game-ready terrain generation using Signed Distance Fields. Open-source Godot Engine plugin written in Rust and GDScript.

See? Math IS useful!


Results

Demonstration of resulting IslandBuilder plugin, written in Rust for Godot Engine. The tool converts whitebox geometry into SDFs, which are then used to generate mesh and collision geometry.

This tool is part of my Stag Toolkit plugin for Godot. Tool may be subject to further improvements. I make use of Fast Surface Nets for converting Signed Distance Field data to a triangle mesh, and godot-rust to hook Rust code into Godot Engine.

Mesh Breakdown

The IslandBuilder starts by reading a whitebox, constructed of CSG primitives, that the level designer made. Each CSG shape is treated like a Signed Distance Field (SDF), which basically means the surface of the shape is algebraically described.

A simple island whitebox

An example island whitebox for Abyss, before mesh generation.

The IslandBuilder's associated plugin, StagToolkit, also comes with a handy "CSG Linter" for easily picking apart CSG primitives while editing.

A simple island whitebox, with linted primitives

The same whitebox, with 'linted' primitives displayed over the real-time preview mesh.

This data is then resampled to a voxel grid that is scaled and diced according to the Axis Aligned Bounding Box of the island whitebox. Because the island surface is algebraically driven, we can easily throw in CSG functions and perlin noise when sampling the SDF.

A CSG island with varying levels of noise

A island chiseled and holed out using CSG, with, from left to right: no noise, some noise, and excessive noise.

Finally, to round out the appearance of the island, the tool performs a 3D box-blur of the voxel grid. Margins of the voxel grid are zeroed out to prevent excessive edge bleeding. A Surface Nets algorithm is then used to then convert the voxel data into a 3D mesh.

An island with varying levels of smoothing

From left to right: zero, three, and ten repetitions of box blur.

Because our IslandBuilder mesh is generated with a pre-determined algorithm, we can easily tweak and refine the output deterministically, unlike generative AI. Data can also be brought out of the engine using Godot's GLTF exporter, if necessary.

The IslandBuilder makes heavy use of chunking and worker threads for better performance, allowing faster iteration in-editor.

Shader Breakdown

The real-time preview generates the bare minimum data to render in-editor so it can be used interactively. When finalizing a mesh, extra data is baked into the mesh, such as vertex colors, UV projections, and LODs, for faster shading during gameplay.

Comparison between real-time preview and finalized shaders, and mesh data.

This means we actually work with two shaders: an expensive "preview" shader that calculates the vertex colors and UVs during the mesh process, and an optimized "high-quality" shader that utilizes the baked mesh data instead.

Noise maps and gradients used in the Island shaders

Shader textures. Noise maps are tileable for all channels and stored in 4K raw. Top Left: Noise map RGB and (bottom left) alpha. Top right: Noisy normal map. Bottom right: gradients used for color selection.

The noise texture features perlin noise at four different frequencies to add variation. However, tiling will still occur unless we sample noise again at a different UV scale, since our texture's channels all tile at the same rate. We also can't store more than one layer of noise in a normal map, and height maps are inefficient for runtime usage.

For lower-end machines, there is a "low-quality" shader which utilizes noise baked into the vertex Alpha in lieu of the anti-tiling texture lookups, to cut down on rendering time. This is toggled in-game via the HIGH_QUALITY flag in our optimized shader.

Comparison between high- and low-quality island shaders

Comparison between High (left) and Medium/Low (right) quality island shader. Differences in dirt and sand color arise from different noise sampling methods.

Without a baked noise mask, low-quality shader reveals tiling due to fewer texture lookups

Same comparison, but with a hand-made island which lacks a vertex Alpha mask, which the Low-quality shader relies on for variation.

Collision Breakdown

In Abyss, nearly all floating islands are Rigid Bodies, which means they can be moved anywhere via the physics engine. Godot Engine requires rigid bodies to use convex collision hulls, so we need to break our concave island into convex hulls. These must fit the island's mesh accurately so the player knows where to walk and land, and also be smooth enough for undisturbed parkour slides.

Godot Engine's convex hull generation isn't the greatest, but has very little data to work with. The IslandBuilder has the advantage of a user-provided whitebox. Since a whitebox determines the ideal level geometry, we can just assign triangles from our generated island mesh to the nearest whitebox primitive again before creating convex hulls.

Comparision between Godot-generated and IslandBuilder-generated collision hulls

Left: generated island shape. Middle: nine Godot Engine-generated convex collision hulls. Right: twenty-one IslandBuilder-generated convex collision hulls.

The extra hulls fit the island more closely, allowing for a smooth platforming experience, while still being planar-decimated for performant collisions with little compromise on accuracy.

Step-by-Step

This plugin works interactively within the Godot editor. Previewing a mesh follows these steps, in order:

  1. (User Input) Add an IslandBuilder node and link it to an IslandBody (GDScript class that manages island physics).
  2. (User Input) Begin adding CSG primitives beneath the IslandBuilder node.
  3. (Tooling) Islandbuilder listens for node tree changes and transform updates while the user works, in a thread.
  4. (Tooling) CSG primitives are serialized as their Signed Distance Field (SDF) counterparts. Basically, each box or sphere is described by a point's distance to it's surface.
  5. (Tooling) All primitives are sampled in an axis-aligned bounding box and stored in a voxel grid.
  6. (Tooling) Perlin noise is added to the grid, and multiple box-blur iterations occur on top to smooth out detail.
  7. (Tooling) A Surface Nets algorithm is used, converting the voxel data into a triangle mesh.
  8. (Tooling) Aforementioned mesh is handed back to Godot for the user to see, in real-time.

Once you're done whiteboxing your island, you can "Finalize" it to optimize the mesh and set up collision to be game-ready.



Future Work

While the tool is already production-ready, here are ways I plan to improve it in the future.