Skip to content
Voxelwave Studio
← Devlogs · Arventus · Devlog · Sep 2026 · 9 min

Generating a world instead of placing one.

The terrain technology behind Arventus: the slope construction, the vegetation pipeline, the measured draw budgets, and the 49-million-cell world it streams.

A generated island in Meadow Terrain: grass tops, soil rims, exposed rock walls and a carved river, rendered in Godot 4.7
One seed of the Meadow Islands demo. Grass tops, soil rims, rock walls, coastline cliffs and a carved river — all generated from the same shared-corner geometry, in the Godot 4.7 Compatibility renderer.

Arventus needs a world rather than a level: somewhere a player can walk for an hour, find a river, follow it to its source, and decide that the flat shelf beside it is where their workshop goes. At that scale, hand-authoring terrain is the single largest cost in the project.

So the first engineering milestone is not gameplay. It is Meadow Terrain: an in-house Godot 4.7 add-on that generates the land, and a pipeline that exports the generated result into a production scene that can be hand-finished and optimised.

What Meadow Terrain actually is

Meadow Terrain is a reusable procedural terrain foundation for Godot 4.7 (tested against 4.7.2, Compatibility renderer). It is a plugin, not a game. Meadow Islands — the screenshot above — is its demo scene, not the product.

The plugin builds grassy slopes, exposed cliffs and matching collision directly from an irregular quad graph. Drop a MeadowTerrain node into a scene, give it a seed, and it generates on ready. It deliberately does not bring a camera, lighting, UI, controls, ocean, trees or autoloads — those belong to the host game.

The constraint the add-on is built to: it must copy into a clean Godot project and pass its own test suite with no game code present.

Slopes that agree with themselves

The problem in this kind of terrain is not generating hills. It is making two adjacent cells agree on exactly where their shared edge sits, so no seam appears — while still allowing a cliff, where the point is that the two sides disagree about height at the same position.

The construction that got us there:

  • The quad graph supplies shared corners, four neighbours per cell, and discrete cell elevations.
  • Every land connection is classified smooth or cliff. Equal levels always connect; a difference up to Slope Max Step can also connect.
  • With Directional Slopes, only connections whose uphill direction agrees with the configured uphill vector are eligible. That produces the signature look — slopes on one side of the hills, cliffs on the other. Sea coasts always stay cliffs.
  • Around each shared corner, cells are grouped through eligible connections, and each group takes the average of its original heights. Two groups separated by a cliff can hold different heights at the same corner — which is exactly what a cliff is.
  • Curved edge samples interpolate those corner heights, so both sides of a smooth connection use matching numbers by construction rather than by tolerance.
  • Whatever height difference remains becomes soil rim and rock wall. Walls taper to zero at cliff ends, and if the upper/lower order flips along an edge, the renderer splits the edge at the crossing instead of emitting a folded face.

This is a shared-corner construction rather than a novel algorithm. What matters for the game is that collision, the rendered mesh and the sample_world() surface query all read the same topology, so when a placement system asks how high the ground is at a point, the answer matches what the player walks on.

One limit worth stating: the slope setting is a height-step eligibility rule, not a maximum walking-angle guarantee. Narrow cells can still produce steep ground, so navigation and slope limits remain game-specific.

Vegetation

Ground cover is what makes a location read as a place rather than a mesh. It is also where a frame budget goes, so it is measured rather than estimated.

Shore slopes

The old scatter rejected every cell classified as a water bank — including the dry, grass-covered upper part of that bank — and separately rejected any top triangle whose upward normal component fell below 0.65. Result: bald slopes everywhere the land met water. On seed 7341 the probe found zero grass roots in bank cells, despite 2,086 dry, gentle bank candidates.

Placement now reads the same shore data the terrain shader uses: identical corner weights define the grass/sand transition, so per-root rejection uses the real blend instead of a cell-level decision. Minimum up-dot dropped to 0.25 for the short-field preset. Cliff walls are still never sampled.

Seed 7341 now carries 63,221 grass roots — 9,064 of them in shore cells, and 2,780 on top triangles the old 0.65 threshold would have thrown away. All 44 trees retained.

A precision defect in the instance data

Some blades rendered with the wrong colour. The renderer stored a global root index in one MultiMesh custom-data channel, and Godot’s Compatibility backend stores that channel at 16-bit precision. A graphics probe read index 53,101 back as 53,088, so those blades sampled a neighbouring root’s colour and normal.

Sparse swatch tests had missed it, because small indices are exactly representable. The index is now split into two exact integers below 2048 and reconstructed in the shader. The revised GPU tests use non-representable large indices with differing neighbouring data: all 63,221 batch indices and transforms pass real-renderer readback, at maximum channel difference 0.0 on the ground-root swatches.

Carried into the rest of the project: a rendering test that only uses conveniently representable values is not a test.

Measured draw budgets

Grass is split into independent MultiMesh chunks — 6 terrain-local units each — so batches outside the camera frustum are rejected outright, and density falls as blades get small on screen. Same seed, 1280 × 900 viewport, actual Godot Compatibility render. The full field is 63,221 instances across 72 batches:

Camera fixtureInstances submittedBatches
Slope close-up12,28612
Meadow close-up23,44917
Gameplay46,52847
Distant35,26961
Wide overview4,41272

These are controller submission counts, not a frame-rate benchmark — the GPU clips more on top. The number that matters is the last row: a wide overview of the whole island submits 4,412 instances, because at that distance the blades are sub-pixel and the CPU stops sending them. The transition is handled by shader-side shrinking before the instance count drops, so it is opaque geometry easing out, not alpha popping.

From one island to 7,000 × 7,000

The finite island node is the well-tested foundation. The world Arventus needs is far larger, so there is a second, separate backend: a streamed noise-based world of 7,000 × 7,000 logical cells — 49 million — with detailed geometry streamed around the camera.

The recipe, in order:

  • Continental noise (OpenSimplex2S with domain warping) → land/sea shape and ordinary height, capped at 10 levels × 3 units = 30 units. At the default cliff fraction of 0.55 each step is 1.65 units, and the rest of the variation stays on the top surface.
  • Mountain-region mask + ridged noise → an independent relief component above that 30-unit cap. Each 90-unit mountain interval reserves 82% of its rise for cliff, up to three steps. The sampled summit on the default seed sits around 305 world units.
  • Moisture and temperature → climate data.
  • Cover, grove and clump fields → ground colour patches and plant groups, independent of elevation.
  • Drainage graph every 32 cells → strictly downhill paths to sea. Routes that end in sinks, need more than an 18-unit cut, or overlap an accepted corridor are rejected. Both verified seeds accept 32 routes, each with a checked source pool or a narrow spring fallback.

Cells crossing a river bank or an elevation contour get a 2 × 2 refinement with canonical cubic edges derived from neighbouring shared corners; everything else keeps its cheap diagonal triangles. That is what rounds the banks without multiplying 49 million cells by the same tessellation factor. Chunk boundaries do not restart the curves, so streaming seams do not appear.

Why any of this matters for the game

The deliverable is a pipeline, not a terrain demo: generate the world procedurally, then export it into a production scene for hand-finishing and optimisation. Procedural where it removes years of work, authored where it makes a location memorable. A generated valley is a starting condition; the version that ships is the one an artist went back into.

This is the substrate structure generation sits on. Before a settlement can be placed procedurally, something has to answer whether the ground is flat enough, above water, near a river, inside a mountain region, or already claimed. Meadow Terrain answers those queries from the same geometry it renders, which is why it was built first.

Current limits

  • No worker-thread generation. Generation runs on the main thread today.
  • No navigation bake, no terrain painting UI, no editor undo integration, no automatic building placement.
  • The finite node rebuilds its whole world on an edit. Local editing now preserves unaffected render and collision chunks, but global work remains.
  • No terrain-occlusion system. Density culling reduces rendering work; it does not hide geometry behind a hill.
  • The renderer needs a valid finite quad graph. It is not a general polygon or voxel engine.
  • Very wide blades can still intersect sharply curved ground.

It can be integrated into a Godot 3D game today. It is an early foundation rather than a universal production terrain system, and it is described that way deliberately.

Next

Structure-site screening on top of the world fields: score candidate cells for flatness, drainage, water access and region, then let a settlement generator work against those scores. After that, the export path — baking a generated region into a scene an artist can open and finish by hand.

That is the milestone that turns generated terrain into a world the game can be built in.