WorldForge Studio

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:

  1. Walks world.objects[].
  2. Resolves each assetId via the asset pipeline.
  3. Loads the GLB mesh + texture.
  4. Places it at (x, elevation, y) with the configured rotation / scale.
  5. 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[] with tags: ["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:

FeatureDescription
Tree clustersGenerated from forest density + style + age + RNG seed
RocksPlaced on slopes > 20° with biome-appropriate variants
Grass tuftsInstanced grass at fine density; supports wind animation
MushroomsGenerated in cave / undergrowth biomes
VinesGenerated on trees in jungle / ancient biomes
RuinsRe-use ruin meshes from the active pack; place on flat ground
RiversAnimated water shader running along precomputed splines
WaterfallsGenerated 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:

ElevationInterpretationVisual
-100 .. 0underwaterocean mesh
0sea levelwater plane
1 .. 10grassland / beachgrass shader
11 .. 99hillsrolling terrain, sparse rocks
100 .. 499mountainsheightfield mesh, boulders, pines
500+snow mountainssnow caps, glaciers
Volcano biomesmolten crater, ashspecial 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:

SeasonEffect
Springflowers, bright greens
Summerdense foliage, vivid colours
Autumnorange / red leaves
Wintersnow 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[].collision and terrain[].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.