Terraforma is a procedural terrain atelier: a seeded landscape generator you steer with six instruments — relief, scale, detail, warp, ridges, sea level — across five biome palettes, then print as a signed survey poster. Everything on the page is computed live in the browser. No textures, no models, no assets. This guide documents the full machinery.
The framing is a fictional cartographic survey studio. The UI borrows from scientific instruments: an "Instruments" panel with hairline sliders, a monospaced meta strip with a world number and invented coordinates, and a serif wordmark set in wide tracking. The terrain itself is the only saturated element on screen — the chrome stays ink-dark and quiet so the biome palettes carry the color story.
Two decisions define the feel: the terrain is an island diorama (a radial falloff sinks every landmass into an endless fogged sea, so every seed composes like an object on a plinth), and every parameter change morphs — the mesh never snaps, it flows to its new shape. That is what makes the controls feel god-like rather than form-like.
OrbitControls, lights, fog.PlaneGeometry, 200×200 segments (140 on mobile), vertex-colored, flatShading:true so no normal recomputation is ever needed (flat normals come free from GPU derivatives).Determinism matters: World Nº 48213 must always be the same world. So the 2D simplex noise runs on a permutation table shuffled by a seeded Mulberry32 PRNG — reseeding is just re-shuffling 256 bytes:
// tiny seeded PRNG → shuffled permutation table → deterministic noise function mulberry32(a){ return function(){ a|=0; a=a+0x6D2B79F5|0; let t=Math.imul(a^a>>>15, 1|a); t=t+Math.imul(t^t>>>7, 61|t)^t; return ((t^t>>>14)>>>0)/4294967296; } } function seedNoise(seed){ const rnd=mulberry32(seed), p=new Uint8Array(256); for(let i=0;i<256;i++) p[i]=i; for(let i=255;i>0;i--){ const j=Math.floor(rnd()*(i+1)); [p[i],p[j]]=[p[j],p[i]]; } for(let i=0;i<512;i++) perm[i]=p[i&255]; }
The permutation shuffle — the whole "seed" is these 256 swaps
On top of raw simplex sit two classic fractal constructions. fBm stacks octaves with halving amplitude for rolling hills; ridged noise folds the signal with 1−|n|, turning zero-crossings into sharp crests — that is where mountain ranges come from:
function fbm(x,y,oct){ let a=.5, f=1, s=0, n=0; for(let i=0;i<oct;i++){ s+=a*snoise(x*f,y*f); n+=a; a*=.5; f*=2.02; } return s/n; } function ridged(x,y,oct){ let a=.5, f=1, s=0, n=0; for(let i=0;i<oct;i++){ s+=a*(1-Math.abs(snoise(x*f,y*f))); n+=a; a*=.5; f*=2.1; } return (s/n)*2-1; // re-center to [-1,1] }
Lacunarity is deliberately ≠ 2.0 (2.02 / 2.1) to break octave alignment artifacts
Every vertex height is a small pipeline. First the sample position is domain-warped — offset by two low-octave fBm reads — which bends straight noise contours into deltas, fjords and swirls (the Warp slider scales this). Then fBm and ridged noise blend under the Ridges slider, and a radial island falloff pushes the rim below sea level:
// per vertex (x,z) — SEG×SEG grid, recomputed on parameter change let wx=x*f, wz=z*f; if(warp>0){ // domain warp: LOW-frequency noise displacing noise, // offset kept well under one base feature size const qx=fbm(wx*.4+13.7, wz*.4+7.3, 3), qz=fbm(wx*.4-4.2, wz*.4+9.1, 3); wx+=qx*warp*.7; wz+=qz*warp*.7; } const base =fbm(wx,wz,oct); // hills const ridge=ridged(wx*.9+31.4, wz*.9-17.2, rOct); let h=base*(1-ridgeAmt)+ridge*ridgeAmt; let e=(h+1)*.5; // power curve: coastal plains, e=Math.pow(e,1.7); h=e*2-.52; // steep peaks const r=Math.sqrt(x*x+z*z)/(SIZE*.52); // island falloff → diorama const fall=1-smoothstep(.6, 1.02, r); h=h*fall-(1-fall)*.55; // rim sinks below the sea y=h*13*relief;
The height function — six instruments feed five constants
Two hard-won lessons live in this snippet. First, warp amplitude: an early version offset the sample position by several base-feature sizes — the terrain collapsed into white-noise spikes, because a warp that jumps farther than a feature width makes adjacent vertices sample unrelated noise. Keep the offset below ~1 feature size and sample the warp fields at ~0.4× the base frequency. Second, aliasing: octave count is clamped so the highest octave's wavelength never drops below two grid cells (allowed = log₂(0.8/cellSize)/log₂(2.02)+1) — beyond Nyquist, "detail" is just per-vertex static.
The site's Pipeline section makes this function inspectable: six canvas plates re-run the same seed with successive terms enabled — raw fBm, then warp, then ridge blend, then falloff, then the biome tint, then hillshade — each ~22,500 samples, rendered progressively when the section scrolls into view. A "re-survey" button re-charts whatever world is currently on stage, so the plates always explain your terrain, not a canned example.
The offsets (13.7, 7.3, −4.2…) are arbitrary decorrelation shifts so the warp fields and the ridge field don't echo the base field. The water is a separate 900×900 plane with a procedurally generated normal map (a 256px canvas of fBm converted to a tangent-space normal encoding) whose UV offset drifts each frame — cheap, believable shimmer with zero vertex work.
Each biome is a plain object: a 6-stop elevation ramp, plus sky pair, fog, sun, water, rock and mist colors. Painting is pure lookup — normalize height above sea level to t∈[0,1], sample the ramp, then push toward the rock color where the local slope is steep (cliffs stay bare while plateaus keep vegetation):
const t = (y - seaY) / (maxH - seaY) + jitter; // jitter = low-amp noise, breaks banding const slope = (|hL-hR| + |hU-hD|) / (cell*9); // central differences const rockAmt = smoothstep(.52,.98,slope) * .72 * smoothstep(.03,.2,t); // beaches stay sandy color = mix(sampleRamp(stops,t), rockColor, rockAmt); // cheap ambient occlusion: a vertex below its 4 neighbours is a hollow const rel = (hL+hR+hU+hD)/4 - y; if(rel>0) color *= 1 - Math.min(1, rel/2.4)*.32;
Elevation ramp × slope mask × hollow-darkening — the entire "material system"
Switching biomes cross-fades everything at once: vertex colors are repainted each frame sampling both ramps and mixing by an eased blend factor, while fog, sun, hemisphere, water and mote colors lerp in the render loop and the CSS sky gradient transitions in parallel. One click, five coordinated fades, no snap.
The mesh keeps two height buffers — heightsA (from) and heightsB (to). Any change computes a new target and rewinds a clock; the loop eases between them with easeOutExpo. Because a change mid-morph snapshots the current interpolated heights as the new "from", dragging a slider quickly never stutters — the terrain chases you fluidly:
function rebuild(dur){ const e=easeOutExpo(morphT); for(let i=0;i<VERTS;i++) // freeze current pose as the new origin heightsA[i]=heightsA[i]+(heightsB[i]-heightsA[i])*e; heightField(heightsB); // compute the new destination morphT=0; morphDur=dur; }
Interruptible morphing — the core of the "god-mode" feel
prefers-reduced-motion kills auto-rotate, collapses all morphs to instant, and shortens every CSS transition to 1ms."Export Poster" turns the current view into a 1500×2100 signed print. The trick is reusing the live renderer: resize it offscreen to ~1.35× the poster's image window, render one frame, grab toDataURL() synchronously (no preserveDrawingBuffer needed if you read immediately after rendering), then restore the viewport. A 2D canvas composites paper, sky gradient, the render, a hairline frame, letterspaced Fraunces title, survey metadata and the biome's swatch strip:
renderer.setPixelRatio(1); renderer.setSize(imgW*1.35, imgH*1.35, false); camera.aspect=imgW/imgH; camera.updateProjectionMatrix(); renderer.render(scene,camera); const shot=renderer.domElement.toDataURL('image/png'); // read NOW, same task // …restore size, then composite on a 2D canvas: ctx.fillStyle='#EFE8D8'; ctx.fillRect(0,0,W,H); // paper ctx.fillStyle=skyGradient; ctx.fillRect(M,M,imgW,imgH); // sky behind alpha render ctx.drawImage(shot, M,M,imgW,imgH); // letterspaced title: draw glyph by glyph, advancing by measureText + tracking for(const ch of 'TERRAFORMA'){ ctx.fillText(ch,tx,ty); tx+=ctx.measureText(ch).width+26; }
Await document.fonts.load() first, or the canvas falls back to system serif
Because the renderer clears to transparent, the poster compositor can paint the same sky gradient underneath the render — the printed sky always matches the biome exactly.
three@0.170.0; alpha renderer over a CSS sky gradient; ACES tone mapping.PlaneGeometry(100,100,200,200), rotate flat, write heights into position.y, vertex colors from an elevation ramp × slope mask, flatShading:true.toDataURL, composite with type on a 2D canvas, download via a temporary anchor.