Instantly convert whiteboxes to game-ready terrain. Editor plugin written in Rust for speed and stability.
Lick rocks 176% faster!
Moving from an initial level whitebox to game-ready terrain is a tedious, time-consuming, repetitive process. It doesn't take much active thinking to sculpt, texture, and break apart collision for islands, but the process eats up hours of my time that I could be fixing bugs, implementing new mechanics, or even just doing art for other things.
Make a tool to do it for me! If islands generally follow the same process of sculpting natural details from whitebox, use the same procedural texture, and need the same collision format, why not automate it?
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.
I first started this journey by getting fed up with texture baking. The texel density for an island, even with a 4K texture, was simply too low, and would cost a lot of texture memory when expanded to tons of islands, so I needed to lean into something procedural. Thus, I developed a real-time island shader, using a 4K noise map with pre-computed normal variant in tandem.
Breakdown of initial island shader. When islands were sculpted, a custom import script automatically handled baking vertex colors and UVs for the shader, to reduce render time.
...versus the baked 4K textures I was using before. Seriously, look at that texel density, haha.
Tileable noisemaps I used on the initial pass of the shader. By sampling them at different UV scales, I was able to minimize noise repetition. However, I later on ended up packing 4 different noise maps into a single RGBA texture instead.
This was fine and dandy for a while, but I realized just how many islands I have to make for any given level. Since my game has platformer mechanics and dynamic levels, land needs to be broken into many chunks. Here's an example of an old level whitebox.
Level whitebox. This was only an initial pass, and I ended up having to break the level up even more to create consistent tension and keep movement interesting. Also, that's a lot of islands.
After seeing some videos online, I learned about Signed Distance Fields (SDFs), and began to look into how I could apply them to my own work. I started with a raymarching GLSL shader which used SDFs constructed out of randomized cubes, smoothing surfaces with logarithms and factoring in perlin noise to create a natural, weathered look.
Raymarcher for islands. The only used inputs are some box size parameters, and two noise maps.
Here's some code for it too.
After experimenting, I used godot-rust to start developing the backend island generator code for Godot Engine, using this fast surface nets library to quickly convert SDF-driven voxels into mesh geometry. Afterward, the mesh data is broken into separate collision hulls depending on how close each vertex is to the original whitebox shapes, which are then convex-hulled by Godot Engine for RigidBody physics. My Rust code then bakes vertex color masks and UVs to the mesh, before finally handing it off to the engine.
This process allows me to gather unique data about the island too, like an accurate volume estimate, its surface area, etc, which can be useful for deriving physics and navigation properties.
I used Godot's plugin system to create the front-end interface that you see in editor whenever highlighting an IslandBuilder class.
Since not everyone has the same caliber computer, I found it necessary to modify the island shader for different performance requirements.
One example is to minimize the amount of texture lookups when adding in noise. While I usually re-read the same noise texture on multiple UV scales to avoid repeating patterns in the sand, it became too expensive for lower-end machines to deal with. To alleviate this, I instead opted to bake perlin noise into the alpha channel of island vertex colors, which provides a little extra randomness we can use for masking.
While the tool already gets the job done, here are a few ways I plan on improving it in the future:
The initial pass of shader code is as follows, written in GDShader.
Before my island generator, I had an import script to automatically bake shader data before saving the island to the engine. Here it is, written in GDScript. Mesh baking begins on line 154.