SEED Rust Landscapes
Porting Klang's Houdini landscape pipeline to a deterministic Rust generator: a JSON recipe streams byte-identical terrain to server, client and mobile at runtime.
Overview
As SEED went live and we scaled toward hundreds of square kilometres, baking each 10 km² map in Houdini started to slow us down: the process is slow and data hungry, and with over 5000 unique assets per landscape the CDN becomes the bottleneck. To solve that, I used Claude to deep dive into our existing Houdini workflows and reimplement them in Rust and .NET, plus a Unity package, so the landscapes are streamed and built on the fly from a minimal json recipe instead of shipping baked assets. We lost a little fidelity on the mountain meshes; beyond that, the results are visually identical.
My role: I am driving the Rust implementation and company adoption together with our CEO and Technical Director.
Gallery
Deep Dive
Two problems sit at the centre of this project: how do you replace gigabytes of baked terrain with a recipe you can generate anywhere, and how do you make that generation reproduce the look of a hand-tuned Houdini pipeline. The two sections below take each in turn.
A world from a recipe
The old pipeline shipped terrain as data: baked heightmaps, splatmaps and thousands of scattered assets, all are pulled from the CDN. The Rust generator ships an algorithm instead. Everything about a landscape lives in a small JSON recipe (a few hundred numbers: the island seed and bounds, the mountain and beach profiles, biome and scatter rules), and the generator turns that recipe into terrain at runtime.
{
"header": {
"generator_id": 1,
"seed": 74241,
"world_bounds": { "min_x": 0, "min_z": 0, "max_x": 6000, "max_z": 6000 }
},
"island_shape": { "edge_offset_fraction": 0.18, "island_height_meters": 24 },
"mountain_profile": { "spacing_factor": 1.2, "thickness_max_meters": 80 },
"beach_profile": { "max_length_meters": 200, "height_meters": 4 },
"water_level_meters": 10
// a few hundred more lines of numbers in total.
}
The catch with generating at runtime is agreement. The server runs authoritative simulation on the .NET build; every player renders the same world in Unity; mobile runs it too. If any of them computed even slightly different terrain, a player could fall through a slope the server believes is solid. So the generator is built to be deterministic to the bit: a signature derived from the recipe and the generator’s version uniquely names a world, so matching signatures are guaranteed to produce matching terrain (and the signature doubles as the cache key once a world is built). A suite of golden tests fails the build if any algorithm change moves an output without a matching version bump.
Determinism at that level is a discipline rather than a side effect. Floating point has to land on the same bits on every platform, so the maths goes through libm (its sinf/cosf are identical everywhere, unlike f32::sin), everything stays in f32 rather than widening to f64 for “more precision”, and every random-looking decision is a seeded hash rather than a call into rand.
// Every "random" choice is a pure function of the world seed and position, so
// it replays identically on the server, every client and mobile. No `rand`,
// no global state, nothing that could drift between machines.
fn hash01(seed: u64, x: i32, y: i32) -> f32 {
let h = xxhash64(seed, x, y);
(h & 0xffff) as f32 / 65535.0
}
// Scatter an object inside a disc from that hash. The maths stays in f32 and
// goes through libm, whose sinf/cosf/sqrtf are bit-identical across platforms
// (f32::sin is not). Widening to f64 for "more precision" would desync the
// client from the server, so the whole pipeline holds the f32 line.
fn scatter_offset(seed: u64, x: i32, y: i32, radius: f32) -> (f32, f32) {
let angle = hash01(seed, x, y) * std::f32::consts::TAU;
let dist = libm::sqrtf(hash01(seed ^ 0x9E37, x, y)) * radius; // even area
(libm::cosf(angle) * dist, libm::sinf(angle) * dist)
}
From Houdini heightfields to Rust
In Houdini the mountains were placed during a per-tile cook (that pipeline is its own writeup: SEED Houdini Landscape PDG). The Rust port turns that cook into a pure function of the recipe. The whole mountain belt is planned once, before any tile exists: a mask marks where mountains belong, the mask is skeletonised down to a one-pixel centerline, and scaled mountain “stamps” are placed along that skeleton, spaced by the recipe’s profile. Beaches, biomes and buildable plots are computed in the same pass and cached alongside it.
Because the plan is just data, a tile is cheap. Its heightfield is the low-frequency island field sampled bilinearly, plus the contribution of every stamp whose scaled footprint reaches into the tile. There is no cook and no baked mesh: the tile is written straight into a buffer the .NET and Unity sides read zero-copy, so streaming a tile is a function call rather than a CDN fetch.
// A placed mountain is a "stamp": a radial height profile dropped onto the
// belt with its own world position, rotation and scale. Its contribution to a
// world point is that profile sampled in the stamp's own local frame.
fn contribution(stamp: &Stamp, x: f32, z: f32) -> f32 {
// World point -> stamp-local: translate to the centre, un-rotate, un-scale.
// libm keeps sin/cos bit-identical across server, client and mobile.
let (dx, dz) = (x - stamp.world_x, z - stamp.world_z);
let (sin, cos) = (libm::sinf(-stamp.rotation), libm::cosf(-stamp.rotation));
let lx = (dx * cos - dz * sin) / stamp.scale;
let lz = (dx * sin + dz * cos) / stamp.scale;
// Normalised distance from the peak; outside the footprint adds nothing.
let r = libm::sqrtf(lx * lx + lz * lz) / stamp.footprint_radius;
if r >= 1.0 {
return 0.0;
}
// Smoothstep shoulder -> peak, so stamps blend instead of leaving a rim.
let t = 1.0 - r;
stamp.peak_height_meters * t * t * (3.0 - 2.0 * t)
}
// A tile's heightfield is the low-frequency island field plus every stamp whose
// scaled footprint reaches into the tile. Deterministic, no cook, no baked mesh:
// the result is written straight into a buffer Unity and .NET read zero-copy.
fn tile_height(plan: &WorldPlan, x: f32, z: f32) -> f32 {
let mut h = bilinear(&plan.low_freq_height, x, z);
for stamp in plan.stamps.near(x, z) {
h += contribution(stamp, x, z);
}
h
}
The production source is proprietary, so the code and diagrams above are illustrative: they show the techniques and the shape of the pipeline, not Klang’s implementation. All screenshots, and the real generator itself, belong to Klang Games GmbH.