Concept · Building the World
Procedural Generation
Nobody sculpted FloraForge's mountains, and no artist painted its coastlines. The entire 65-kilometre world — every hill, forest, desert and river — is computed from rules and math, starting from a single number. This page explains how that works: what noise is, how layers of it become terrain, and how the engine turns two numeric fields into a world with biomes and rivers.
Content from rules, not from hands
The traditional way to build a game world is to build it: artists place every rock, sculpt every valley, paint every texture. Procedural generation replaces hand placement with a function. Instead of storing the height of the ground everywhere, you store a rule that can answer the height of the ground anywhere — and the landscape falls out of that rule.
The numbers make the case. FloraForge's world is a 256 × 256 grid of 256-metre chunks, each sampled on a 129 × 129 grid — over a billion height samples if you tried to store them all. What the engine actually stores is one 32-bit integer: the seed (42, by default). Every random-looking choice in generation is derived from it, so the same seed always rebuilds exactly the same world, down to the position of each tree. You never save the mountains; you save the number that grows them.
Noise: randomness you can walk across
Pure randomness makes terrible terrain — independent random heights next to each other look like television static, not hills. The core ingredient of procedural worlds is noise: a function that looks random at a distance but is perfectly smooth up close, so neighbouring points always get similar values. FloraForge uses OpenSimplex noise, a member of the same family as the classic Perlin noise. It has three properties the engine leans on:
- Deterministic. The same seed and coordinates always produce the same value — the foundation of the seed guarantee.
- Smooth. Values vary continuously, so the terrain has slopes instead of cliffs of static.
- Stateless. Any point can be evaluated on its own, without computing its neighbours first. That is what lets a chunk anywhere in the world be generated in complete isolation — the property chunk streaming depends on.
Octaves: one wave is a dune, three are a landscape
A single layer of noise produces blobby, same-sized lumps. Real terrain has structure at every scale: continents, ridgelines, and the small roughness underfoot. The standard fix — often called fractal Brownian motion, or fBm — is to stack several octaves of noise: each layer has a higher frequency (smaller features) and a lower amplitude (less height influence) than the last, and the sum has detail at every scale. FloraForge's height function uses three named octaves:
The same recipe at different scales appears all over the engine — the
sky's clouds are a five-octave fBm stack computed in a
fragment shader.
One refinement is worth pointing out: the ridge octave isn't used raw.
The engine takes 1.0 - noise.abs(), which folds the noise
at its zero crossings into sharp creases — that single absolute value
is the difference between round hills and mountain ridgelines.
The 4D trick: a world without edges
FloraForge's world wraps: fly far enough east and you arrive from the west. Plain 2D noise would put an ugly seam at the wrap line, where two unrelated parts of the noise field meet. The engine's solution is geometric: map each world axis onto a circle, so that walking across the whole world travels exactly once around it. Two circles need four coordinates — together they trace a torus embedded in 4D space — so the engine samples 4D OpenSimplex noise on that torus surface. Coming back to your starting point means arriving at literally the same noise input, so the terrain is exactly periodic and the seam doesn't exist:
fn torus_sample(noise: &OpenSimplex, x: f64, z: f64, frequency: f64) -> f64 {
let l = WORLD_SIZE_METERS;
// Whole noise cycles per world lap — snapping k to an integer is
// what makes the field exactly periodic.
let k = (frequency * l).round().max(1.0);
let radius = k / TAU;
let theta_x = TAU * x / l;
let theta_z = TAU * z / l;
noise.get([
radius * theta_x.cos(),
radius * theta_x.sin(),
radius * theta_z.cos(),
radius * theta_z.sin(),
])
}
pub fn sample_height(&self, x: f32, z: f32) -> f32 {
let c = &self.config;
let (broad, ridge_raw) = self.fields.sample(x, z);
let ridges = 1.0 - ridge_raw.abs(); // fold into sharp creases
let rough = Self::torus_sample(&self.detail, x as f64, z as f64,
c.detail.frequency) as f32;
broad * c.continental.amplitude + ridges * c.ridge.amplitude + rough * c.detail.amplitude
}
Elegance has a price: 4D noise is the single most expensive thing the
generator does. Profiling showed that roughly 98% of the cost
of generating a chunk is noise sampling — each 256-metre chunk
evaluates 129 × 129 = 16,641 points, for both a height field
and a moisture field. The engine claws much of that back with two
tricks visible in the code above: the low-frequency octaves
(self.fields) are precomputed once on a coarse global grid
and merely interpolated per vertex, and the per-axis sine/cosine pairs
are hoisted out of the grid loops — both bit-identical to the naive
version, just cheaper.
From two numbers to a living world
Height alone gives geography but not character. The generator samples a second, independent noise field — moisture — and the pair classifies every point into a biome with rules simple enough to quote in full:
pub fn classify(height: f32, moisture: f32, config: &BiomeConfig) -> Biome {
if height > config.snow_height { // above 165 m
return Biome::Snow;
}
if height > config.rock_height { // above 120 m
return Biome::Rock;
}
if moisture < config.desert_moisture { // drier than 0.3
return Biome::Desert;
}
if moisture > config.forest_moisture { // wetter than 0.62
return Biome::Forest;
}
Biome::Grassland
}
Five biomes from two thresholds each — and because the inputs are smooth fields, the borders between them are natural, wandering transitions rather than straight lines. The biome then drives what grows there: which plant species appear, how densely, and where houses may stand. Those ecological rules have their own page, The Biology of FloraForge.
One feature refuses to be a pure per-point function:
rivers. Water flows downhill across chunk boundaries
and accumulates — a global phenomenon. So once per world load, the
engine solves hydrology over a coarse grid spanning the whole world:
it fills depressions, builds a drainage tree where every cell points
downhill toward an ocean outlet, and accumulates upstream area. Cells
that collect enough drainage become rivers, and a carve depth
and wetness are baked into the grid
(src/world_core/rivers.rs). Per-chunk generation then just
subtracts the carved channel from the noise terrain — cheap, and
globally consistent, because every chunk reads the same baked field.
Determinism pays twice
Because the whole pipeline is a pure function of the seed, the result
is perfectly cacheable. FloraForge exploits that with a precomputed
base world: generating all 65,536 chunks once (mostly
to place the base vegetation) is real work, so the result is saved as
world_base.bin. Releases ship that file, and the
browser build
downloads it instead of regenerating the planet from scratch in your
tab. The snapshot is stamped with a key derived from the generation
parameters, so if the rules ever change, a stale snapshot is rejected
and rebuilt rather than silently mismatching the code.
src/world_core/: heightmap.rs
(the noise octaves), biome.rs, rivers.rs (the
hydrology solve), and chunk_generator.rs, which runs the
layers in order — terrain, biomes, then content — for one chunk at a
time. Unit tests pin the guarantees down: the terrain
must be exactly periodic across the world seam, two generators with the
same seed must place identical plants, and the optimised grid samplers
must match the simple per-point ones bit for bit.