04 — 2D → 3D World Engine
The 2D map is the source of truth. The 3D world is a generated visualisation layer computed from
World.json+ the active theme pack. Same document, different projection. Switching from 2D to 3D is instant; editing in either updates the other.
1. Stack
Next.js (apps/web)
│
├── React Three Fiber ───────────── declarative scene graph
│ │
│ └── Three.js ─────────────── WebGL / WebGPU renderer
│
├── @react-three/drei ───────────── helpers (OrbitControls, Environment, ...)
│
├── @react-three/postprocessing ─── bloom, depth-of-field, motion blur
│
└── @world-map-editor/world-engine ── canonical world document
│
└── @world-map-editor/asset-pipeline ── SVG → mesh / GLB resolve
WebGPU is preferred when available (Chrome 113+, Safari 17+). Falls back to WebGL2 transparently. The renderer is feature-gated, not branched — every material shader compiles for both backends.
1.1 Babylon.js (alternative)
Tracked but not used. Revisit if R3F proves limiting for instanced forests and terrain shaders.
2. Asset ↔ mesh resolution
Every asset ships 2D and 3D representations:
{
"id": "ancient_tree",
"kind": "object",
"2d": { "primary": "objects/ancient_tree.svg" },
"3d": {
"mesh": "3d/palm_tree.glb",
"lod": ["3d/palm_tree.glb", "3d/LOD/palm_tree_far.glb"],
"height": 15, // metres
"castsShadow": true
}
}
When the 3D engine mounts a World, it:
- Walks
world.objects[]. - Resolves each
assetIdvia the asset pipeline. - Loads the GLB mesh + texture.
- Places it at
(x, elevation, y)with the configured rotation / scale. - Attaches LOD switching based on camera distance.
3. Terrain → 3D generation
Each terrain entry derives a 3D representation from elevation + biome + terrain.
3.1 Height mesh
The world is a single mesh whose vertices are sampled at each tile centre. Elevation
in terrain[].elevation drives the y coordinate.
elevation: number // metres
seaLevel: 0 // anything below is rendered as water
Vertex shader interpolates between neighbour tiles for smooth slopes. Steep slopes auto-apply a rock texture; flat tiles apply biome-specific grass / sand / ice.
3.2 Mountains
A mountains tile at elevation ≥ 500 m:
- Generates a heightfield-driven cliff mesh.
- Patches boulders, pine trees, snow caps at higher elevations.
- The renderer detects "ridge" topology (max elevation gradient > 30°) and snaps a cliff transition texture.
3.3 Forest tiles
A forest / dense_forest tile:
- The base tile renders as grass / mud.
- The scatter entries (
world.objects[]withtags: ["forest"]) are instantiated as GLB tree meshes at their(x, y)coordinates. - Density + style + age sliders (theme-controlled) modulate scale and rotation.
3.4 Cities
city / town regions instantiate building meshes at every building object. Roads
become instanced planes with road texture. Walls become ribbon meshes.
4. Procedural 3D generation
In addition to object scatter from the terrain-gen layer, the 3D engine supports runtime procedural features that exist only in 3D:
| Feature | Description |
|---|---|
| Tree clusters | Generated from forest density + style + age + RNG seed |
| Rocks | Placed on slopes > 20° with biome-appropriate variants |
| Grass tufts | Instanced grass at fine density; supports wind animation |
| Mushrooms | Generated in cave / undergrowth biomes |
| Vines | Generated on trees in jungle / ancient biomes |
| Ruins | Re-use ruin meshes from the active pack; place on flat ground |
| Rivers | Animated water shader running along precomputed splines |
| Waterfalls | Generated at elevation discontinuities along rivers |
Procedural features are derived — they never appear in world.objects[]. They
are computed deterministically from the world state + the active pack, so a
"regenerate forest" button produces an identical forest given the same seed.
5. Height system
Every tile has elevation in metres. The 3D engine uses this consistently:
| Elevation | Interpretation | Visual |
|---|---|---|
| -100 .. 0 | underwater | ocean mesh |
| 0 | sea level | water plane |
| 1 .. 10 | grassland / beach | grass shader |
| 11 .. 99 | hills | rolling terrain, sparse rocks |
| 100 .. 499 | mountains | heightfield mesh, boulders, pines |
| 500+ | snow mountains | snow caps, glaciers |
| Volcano biomes | molten crater, ash | special shader |
Per-tile elevation overrides the default biome elevation curve.
6. Water system
Animated water shader supporting:
- Ocean — large flat plane with foam at coastlines, animated wave normals.
- Rivers — spline-driven ribbon mesh with directional flow.
- Lakes — closed polygon water plane.
- Waterfalls — particle + sound trigger at elevation discontinuities.
- Reflections — environment-mapped reflection probe; cheap variant for mobile.
Water is derived from terrain: a river is a path of river biome tiles or a
kind: "river" region polygon. The shader reads the world geometry and never stores
its own state.
7. Weather engine
Global weather profile drives particle systems and shader uniforms.
{
"id": "jungle_storm",
"kind": "weather",
"particles": [
{ "kind": "rain", "density": 0.8, "wind": { "x": 0.2, "y": 0, "z": 0 } },
{ "kind": "fog", "density": 0.4 }
],
"shaderOverrides": {
"wetness": 0.7,
"darkness": 0.5,
"lightning": true
},
"sound": "gas://sound/storm@1.0.0"
}
Built-in weather profiles:
clear,cloudy,rain,storm,snow,fog,sandstorm,ash,magic
Per-region overrides (weather.overrides[]) allow different weather in different
parts of the world.
Effects on materials:
- Wet surfaces — higher specular, darker albedo.
- Snow accumulation — vertex displacement on upward-facing surfaces.
- Footprints — decals in snow when an NPC walks.
- Lightning — strobe light + thunder sound.
8. Day / night cycle
World clock advances in real time (or simulated time in cinematic mode).
06:00 sunrise
12:00 day
18:00 sunset
24:00 night
Drives:
- Sun position (azimuth + altitude).
- Skybox / sky shader.
- Shadow softness and direction.
- Ambient light intensity and colour.
- Fog density.
- NPC behaviour (guards patrol at night, merchants open at day).
Configurable per-world in world.lighting.
9. Seasons
Optional. Adds four palette overlays + a few asset swaps:
| Season | Effect |
|---|---|
| Spring | flowers, bright greens |
| Summer | dense foliage, vivid colours |
| Autumn | orange / red leaves |
| Winter | snow accumulation, leafless trees |
Seasons are a Theme Pack concern — different packs ship different seasonal
overlays.
10. Lighting model
- Sun — directional light from the day/night cycle.
- Ambient — hemisphere light tinted by skybox colour.
- Point lights — torches, lanterns, magical glows. Maximum 16 simultaneous with shadow.
- Spot lights — gate torches, searchlights. Maximum 8 with shadow.
- Environment map — derived from the skybox for PBR reflections.
The lighting state lives in world.lighting; the 3D engine never stores per-scene
lighting overrides.
11. Performance strategy
11.1 Chunking
Large worlds are split into chunks (default 16×16 tiles). Only nearby chunks load:
World
├── Chunk 0,0
├── Chunk 0,1
├── Chunk 1,0
├── Chunk 1,1
└── ...
Loaded chunks live in memory; distant chunks are unloaded. Chunks stream from the asset pipeline as the camera moves.
11.2 LOD (Level of Detail)
Every GLB mesh ships three LODs:
- LOD0 — full quality, ≤ 30 m camera distance.
- LOD1 — simplified, ≤ 80 m.
- LOD2 — billboard / impostor, > 80 m.
Distance thresholds are theme-configurable.
11.3 Instancing
Identical meshes (1000 trees of the same variant) draw as a single instanced call.
R3F's <Instances> component handles this transparently.
11.4 Culling
- Frustum culling — off-screen meshes skip draw.
- Occlusion culling — meshes behind mountains skip draw (Phase 4).
- Tile culling — distant tiles use lower-resolution textures.
11.5 Frame budget
Target: 60 fps on a 2020 mid-range laptop at 1080p. Test scene: 64×48 hex world with 5 000 objects and full weather.
12. Cinematic mode integration
The Cinematic Director (see 06-cinematic-director.md)
plays camera paths defined in world.cameraPaths[]. The 3D engine provides:
- The active scene graph.
- The world clock (for day/night / weather sync).
- The simulation state (NPC positions, weather overrides).
- A controllable Three.js camera with FOV, roll, focusDistance, aperture.
The Director is a thin layer above the 3D engine — it never owns scene state.
13. First-person simulation
The same scene graph supports a first-person player controller:
- Walk / run / jump / crouch — input layer.
- Head bob — subtle camera shake.
- Interaction — pick up objects, talk to NPCs, open doors.
- Collision — derived from
objects[].collisionandterrain[].movement. - Visibility / fog of war — restricts what the player can see.
See 05-simulation-and-game-modes.md for the
full simulation layer.
14. Mobile considerations
- Lower LOD defaults.
- Particle density capped.
- Instanced grass disabled.
- Shadows disabled (or blob shadows only).
- Day/night cycle still runs but at lower frame rate.
15. Determinism
The 3D engine must be deterministic given:
- The same
World.json. - The same theme pack.
- The same camera path.
- The same seed.
This is required for replay, cinematic recording, and reproducible bug reports. Procedural features use a deterministic Mulberry32 RNG seeded by tile coordinate.