// World synthesis: prompt -> region plan -> semantic layout -> composite height field. // // This is an independent implementation of the terrain foundation described in // "WorldClaw: Agentic 3D Open-world Generation at Scale" (arXiv 2608.05248). // No Tencent code or model weights are used — that repository publishes neither. // The structure it describes and that we follow here: // H(x) = sum_r m_r(x) * [ h_r + sum_k w_rk N_rk(x) + sum_j a_rj G_rj(x) ] // i.e. per-region base elevation, per-region noise bands, and per-region geomorphic // operators, combined through smoothed region masks so borders blend rather than step. import { erode, fillDepressions, drainage, hydrology, moisture } from './hydro.js'; export const GRID = 256; // height field resolution (65k terrain samples) export const WORLD = 1000; // one-kilometre world extent in metres const VERTICAL_SCALE = 2.2; // keep kilometre-scale relief visually meaningful // ---------------------------------------------------------------- rng + noise -- export function makeRng(seed) { let s = seed >>> 0 || 1; return function rng() { s ^= s << 13; s >>>= 0; s ^= s >> 17; s ^= s << 5; s >>>= 0; return s / 4294967296; }; } function makeValueNoise(rng) { const size = 256; const perm = new Uint8Array(size * 2); const base = new Uint8Array(size); for (let i = 0; i < size; i++) base[i] = i; for (let i = size - 1; i > 0; i--) { const j = Math.floor(rng() * (i + 1)); [base[i], base[j]] = [base[j], base[i]]; } for (let i = 0; i < size * 2; i++) perm[i] = base[i & 255]; const grad = new Float32Array(size * 2); for (let i = 0; i < size * 2; i++) grad[i] = rng() * 2 - 1; const fade = t => t * t * t * (t * (t * 6 - 15) + 10); const lerp = (a, b, t) => a + (b - a) * t; return function noise2(x, y) { const xi = Math.floor(x) & 255, yi = Math.floor(y) & 255; const xf = x - Math.floor(x), yf = y - Math.floor(y); const u = fade(xf), v = fade(yf); const aa = grad[perm[perm[xi] + yi]]; const ab = grad[perm[perm[xi] + yi + 1]]; const ba = grad[perm[perm[xi + 1] + yi]]; const bb = grad[perm[perm[xi + 1] + yi + 1]]; return lerp(lerp(aa, ba, u), lerp(ab, bb, u), v); }; } function fbm(noise, x, y, octaves, lacunarity = 2.0, gain = 0.5) { let sum = 0, amp = 1, freq = 1, norm = 0; for (let o = 0; o < octaves; o++) { sum += amp * noise(x * freq, y * freq); norm += amp; amp *= gain; freq *= lacunarity; } return sum / norm; } // ------------------------------------------------------------------- biomes ---- // Each biome is one row of the height formula: a base elevation, a noise band, and // a geomorphic operator. Colours are the semantic layout map's encoding as well as // the terrain tint, so the map and the mesh cannot drift apart. export const BIOMES = { ocean: { label: 'Ocean', color: '#1d4e6b', base: -13, amp: 1.2, freq: 1.4, op: 'flat', rough: 0.1, props: [] }, lake: { label: 'Lake', color: '#2a6b8a', base: -6, amp: 0.8, freq: 1.6, op: 'flat', rough: 0.1, props: [] }, beach: { label: 'Beach', color: '#d9c89a', base: 0.6, amp: 1.0, freq: 2.2, op: 'flat', rough: 0.2, props: ['palm', 'rock'] }, plains: { label: 'Plains', color: '#7d9455', base: 4, amp: 3.0, freq: 1.8, op: 'none', rough: 0.4, props: ['tree', 'shrub', 'rock'] }, meadow: { label: 'Meadow', color: '#94a95c', base: 5, amp: 4.0, freq: 2.4, op: 'none', rough: 0.4, props: ['shrub', 'tree'] }, savanna: { label: 'Savanna', color: '#a89a5a', base: 5, amp: 3.5, freq: 1.6, op: 'none', rough: 0.4, props: ['acacia', 'rock'] }, forest: { label: 'Forest', color: '#3f6b3a', base: 7, amp: 6.0, freq: 2.2, op: 'none', rough: 0.6, props: ['pine', 'tree', 'rock'] }, jungle: { label: 'Jungle', color: '#2f6b39', base: 6, amp: 7.0, freq: 2.8, op: 'none', rough: 0.7, props: ['palm', 'tree', 'shrub'] }, swamp: { label: 'Swamp', color: '#4a5c3a', base: 1.2, amp: 1.6, freq: 2.6, op: 'flat', rough: 0.3, props: ['tree', 'shrub'] }, desert: { label: 'Desert', color: '#c9a86a', base: 4, amp: 5.0, freq: 1.5, op: 'dune', rough: 0.3, props: ['cactus', 'rock'] }, badlands: { label: 'Badlands', color: '#a8734a', base: 9, amp: 12.0, freq: 1.9, op: 'terrace', rough: 0.8, props: ['rock', 'cactus'] }, canyon: { label: 'Canyon', color: '#9c5f42', base: 12, amp: 16.0, freq: 1.2, op: 'erosion', rough: 1.0, props: ['rock'] }, mesa: { label: 'Mesa', color: '#b07048', base: 14, amp: 10.0, freq: 1.0, op: 'terrace', rough: 0.7, props: ['rock', 'cactus'] }, hills: { label: 'Hills', color: '#6f8a4e', base: 10, amp: 9.0, freq: 1.6, op: 'none', rough: 0.5, props: ['tree', 'shrub', 'rock'] }, mountain: { label: 'Mountains', color: '#7c7a72', base: 20, amp: 26.0, freq: 1.1, op: 'peak', rough: 1.0, props: ['pine', 'rock'] }, alpine: { label: 'Alpine', color: '#8c8d8a', base: 26, amp: 24.0, freq: 1.3, op: 'peak', rough: 1.0, props: ['pine', 'rock'] }, snow: { label: 'Snowfield', color: '#dfe6ea', base: 30, amp: 16.0, freq: 1.2, op: 'peak', rough: 0.8, props: ['rock'] }, tundra: { label: 'Tundra', color: '#9aa89c', base: 6, amp: 4.0, freq: 1.8, op: 'none', rough: 0.4, props: ['rock', 'shrub'] }, volcano: { label: 'Volcano', color: '#4a3f3d', base: 22, amp: 30.0, freq: 1.0, op: 'cone', rough: 1.0, props: ['rock'] }, crater: { label: 'Crater', color: '#6b665f', base: 8, amp: 14.0, freq: 1.4, op: 'crater', rough: 0.9, props: ['rock'] }, }; // Keyword -> biome. Longer phrases are matched first so "snow mountain" does not // collapse to "snow" alone. const KEYWORDS = [ [['ocean', 'sea', 'coastline', '바다', '해안'], ['ocean', 'beach']], [['island', '섬'], ['ocean', 'beach', 'jungle', 'mountain']], [['lake', 'pond', '호수'], ['lake', 'meadow']], [['river', 'valley', '계곡', '강'], ['canyon', 'hills', 'forest']], [['beach', 'shore', '해변'], ['beach', 'ocean']], [['canyon', 'gorge', '협곡'], ['canyon', 'mesa', 'desert']], [['mesa', 'butte'], ['mesa', 'desert']], [['badland', 'wasteland', '황무지'], ['badlands', 'desert']], [['desert', 'dune', 'sand', '사막'], ['desert', 'badlands']], [['oasis', '오아시스'], ['desert', 'lake']], [['volcano', 'lava', '화산'], ['volcano', 'badlands']], [['crater', 'moon', 'lunar', '분화구'], ['crater', 'tundra']], [['jungle', 'rainforest', '정글'], ['jungle', 'hills']], [['forest', 'wood', 'pine', 'taiga', '숲'], ['forest', 'hills']], [['swamp', 'marsh', 'bog', '늪'], ['swamp', 'forest']], [['savanna', 'safari'], ['savanna', 'plains']], [['tundra', 'arctic', 'frozen', '툰드라'], ['tundra', 'snow']], [['glacier', 'snow', 'ice', '설원', '빙하'], ['snow', 'alpine']], [['alpine', 'alps'], ['alpine', 'snow', 'forest']], [['mountain', 'peak', 'ridge', '산'], ['mountain', 'hills']], [['hill', 'highland', '언덕'], ['hills', 'meadow']], [['meadow', 'grass', 'field', 'prairie', '초원'], ['meadow', 'plains']], [['plain', 'steppe', '평원'], ['plains', 'hills']], ]; export function planRegions(prompt, rng) { const text = (prompt || '').toLowerCase(); // Rank by where the word appears in the prompt, not by dictionary order: in // "a snowy alpine range above a pine forest and a lake" the subject is the range, // and coverage below is derived from this ranking. const hits = []; for (const [words, biomes] of KEYWORDS) { let at = Infinity; for (const w of words) { const i = text.indexOf(w); if (i >= 0) at = Math.min(at, i); } if (at < Infinity) hits.push({ at, biomes }); } hits.sort((a, b) => a.at - b.at); const picked = []; for (const h of hits) { for (const b of h.biomes) if (BIOMES[b] && !picked.includes(b)) picked.push(b); } if (picked.length === 0) picked.push('meadow', 'forest', 'hills', 'mountain'); if (picked.length === 1) { const companions = { ocean: 'beach', desert: 'mesa', snow: 'alpine', jungle: 'hills' }; picked.push(companions[picked[0]] || 'plains'); } const regions = picked.slice(0, 5); // Coverage: earlier keywords weigh more, so the leading noun dominates the map. const weights = regions.map((_, i) => 1 / (1 + i * 0.55)); const total = weights.reduce((a, b) => a + b, 0); return regions.map((key, i) => ({ key, ...BIOMES[key], coverage: weights[i] / total, seeds: Math.max(1, Math.round(weights[i] / total * 9)), jitter: rng(), })); } // -------------------------------------------------------- semantic layout map -- // Region ownership by warped Voronoi: the domain warp is what keeps borders from // looking like a polygon diagram. function buildLayout(regions, rng, noise) { const seeds = []; regions.forEach((r, ri) => { for (let s = 0; s < r.seeds; s++) { seeds.push({ ri, x: rng() * GRID, y: rng() * GRID }); } }); const owner = new Int16Array(GRID * GRID); for (let y = 0; y < GRID; y++) { for (let x = 0; x < GRID; x++) { const wx = x + fbm(noise, x * 0.018, y * 0.018, 3) * 26; const wy = y + fbm(noise, x * 0.018 + 40, y * 0.018 + 40, 3) * 26; let best = 0, bestD = Infinity; for (const s of seeds) { const d = (s.x - wx) ** 2 + (s.y - wy) ** 2; if (d < bestD) { bestD = d; best = s.ri; } } owner[y * GRID + x] = best; } } return owner; } // Soft masks: one blurred 0..1 field per region, renormalised so they sum to 1. function buildMasks(owner, count, passes = 3) { const masks = []; for (let r = 0; r < count; r++) { const m = new Float32Array(GRID * GRID); for (let i = 0; i < m.length; i++) m[i] = owner[i] === r ? 1 : 0; masks.push(m); } const tmp = new Float32Array(GRID * GRID); for (const m of masks) { for (let p = 0; p < passes; p++) { for (let y = 0; y < GRID; y++) { for (let x = 0; x < GRID; x++) { let sum = 0, n = 0; for (let dy = -2; dy <= 2; dy++) { const yy = y + dy; if (yy < 0 || yy >= GRID) continue; for (let dx = -2; dx <= 2; dx++) { const xx = x + dx; if (xx < 0 || xx >= GRID) continue; sum += m[yy * GRID + xx]; n++; } } tmp[y * GRID + x] = sum / n; } } m.set(tmp); } } for (let i = 0; i < GRID * GRID; i++) { let sum = 0; for (const m of masks) sum += m[i]; if (sum > 1e-6) for (const m of masks) m[i] /= sum; else masks[0][i] = 1; } return masks; } // -------------------------------------------------------- geomorphic operators -- function operator(kind, x, y, cx, cy, noise, phase) { const nx = (x - cx) / GRID * 2, ny = (y - cy) / GRID * 2; const d = Math.sqrt(nx * nx + ny * ny); switch (kind) { case 'peak': // ridged mass falling off outward return Math.max(0, 1 - d * 1.15) ** 1.6 * (0.55 + 0.45 * Math.abs(fbm(noise, x * 0.02, y * 0.02, 4))); case 'cone': // volcano: cone with a summit vent return Math.max(0, 1 - d * 1.3) ** 1.2 - Math.max(0, 1 - d * 7) ** 2 * 0.55; case 'crater': // rim up, floor down return Math.max(0, 1 - Math.abs(d * 3.2 - 1) * 2.2) * 0.9 - Math.max(0, 1 - d * 3.2) ** 2 * 0.7; case 'dune': // travelling ridges return Math.sin((x * 0.16 + y * 0.07) + phase * 6.28 + fbm(noise, x * 0.02, y * 0.02, 2) * 2.2) * 0.5; case 'terrace': // stepped plateaus return Math.round(fbm(noise, x * 0.012, y * 0.012, 3) * 3.2) / 3.2; case 'erosion': { // incised channels const v = Math.abs(fbm(noise, x * 0.014 + phase * 10, y * 0.014, 4)); return -((1 - v) ** 2) * 1.3; // parens required: `-x ** 2` is a syntax error } case 'flat': return 0; default: return 0; } } // ------------------------------------------------------------------ synthesis -- export function synthesize(prompt, seed) { const rng = makeRng(seed); const noise = makeValueNoise(rng); const regions = planRegions(prompt, rng); const owner = buildLayout(regions, rng, noise); const masks = buildMasks(owner, regions.length); // Region centroids feed the radial operators (peak, cone, crater). const cent = regions.map(() => ({ x: 0, y: 0, n: 0 })); for (let y = 0; y < GRID; y++) { for (let x = 0; x < GRID; x++) { const c = cent[owner[y * GRID + x]]; c.x += x; c.y += y; c.n++; } } cent.forEach(c => { if (c.n) { c.x /= c.n; c.y /= c.n; } }); const height = new Float32Array(GRID * GRID); for (let y = 0; y < GRID; y++) { for (let x = 0; x < GRID; x++) { const i = y * GRID + x; let h = 0; for (let r = 0; r < regions.length; r++) { const m = masks[r][i]; if (m < 0.002) continue; const reg = regions[r]; const bands = fbm(noise, x * 0.012 * reg.freq, y * 0.012 * reg.freq, 5) * reg.amp + fbm(noise, x * 0.05 * reg.freq, y * 0.05 * reg.freq, 3) * reg.amp * 0.22 * reg.rough; const geo = operator(reg.op, x, y, cent[r].x, cent[r].y, noise, reg.jitter) * reg.amp; h += m * (reg.base + bands + geo); } height[i] = h * VERTICAL_SCALE; } } // One smoothing pass: masks blend the fields, but operator seams still benefit. const sm = new Float32Array(height.length); for (let y = 0; y < GRID; y++) { for (let x = 0; x < GRID; x++) { let sum = 0, n = 0; for (let dy = -1; dy <= 1; dy++) { for (let dx = -1; dx <= 1; dx++) { const yy = y + dy, xx = x + dx; if (yy < 0 || yy >= GRID || xx < 0 || xx >= GRID) continue; sum += height[yy * GRID + xx]; n++; } } sm[y * GRID + x] = sum / n; } } const hasWater = regions.some(r => r.key === 'ocean' || r.key === 'lake' || r.key === 'swamp'); const seaLevel = hasWater ? 0 : -999; // The world model proper: erosion cuts the valleys, the valleys carry the // rivers, the rivers set the moisture. Without this the terrain is scenery; // with it, every later decision (colour, vegetation, wildlife) has a cause. erode(sm, rng); // carve the valleys const filled = fillDepressions(sm); // make every cell drain, and find the lakes const { acc: flow } = drainage(filled); // drainage area on a surface that connects const water = hydrology(sm, flow, seaLevel, filled); const moist = moisture(water.depth); return { regions, owner, masks, height: sm, seaLevel, flow, water, moisture: moist, }; }