Skip to content

feat(biome): Milestone 1 — Biome Foundation + Terrain Shape Variation#14

Merged
maxfelker merged 3 commits into
mainfrom
feature/biome-m1-foundation
Mar 1, 2026
Merged

feat(biome): Milestone 1 — Biome Foundation + Terrain Shape Variation#14
maxfelker merged 3 commits into
mainfrom
feature/biome-m1-foundation

Conversation

@maxfelker

Copy link
Copy Markdown
Owner

Milestone 1: Biome Configuration Foundation + Terrain Shape Variation

Overview

Adds the core biome system to the terrain generation engine. Each region of the world now has a distinct biome (Grassland, Desert, Mountains, Valley, Swamp, or Forest), each generating visually different terrain shapes.

How Biome Selection Works

Two low-frequency noise maps (temperature + humidity) are sampled at each chunk center with domain warping to create organic, curved biome boundaries (no grid lines). The temperature × humidity pair maps to a biome via a simplified Whittaker biome classification chart, which naturally enforces valid adjacency — desert and swamp are never neighbors.

Biome Terrain Parameters

Biome Octaves Frequency Persistence Height Mult Effect
Grassland 5 0.0008 0.50 1.0× Gentle rolling hills
Desert 3 0.0004 0.40 0.8× Long smooth dunes
Mountains 7 0.002 0.60 3.0× Steep jagged peaks
Valley 4 0.0006 0.45 0.6× Wide, gentle lowlands
Swamp 2 0.001 0.35 0.25× Nearly flat
Forest 5 0.001 0.55 1.2× Moderate hilly terrain

Files Changed

Go WASM (new package wasm/biome/):

  • biome.go — BiomeType, BiomeDefinition structs
  • registry.go — 6 biome configs + ScaleHeightmap helper
  • selector.go — Domain-warped temperature/humidity noise → Whittaker classification
  • world_config.go — WorldConfig (seed, biomeScale)
  • biome_test.go — 9 tests covering determinism, Whittaker rules, registry completeness

Go WASM (modified wasm/main.go):

  • goGenerateChunk: applies biome noise params, scales heightmap and extended heightmap, returns biomeId appended to combined buffer
  • goLoadWorldConfig: new WASM export for loading WorldConfig JSON

TypeScript:

  • src/engine/biome/BiomeTypes.ts — BiomeType const object, WorldConfig type
  • src/engine/biome/BiomeRegistry.ts — client-side biome metadata
  • WasmBridge.ts — updated global declarations
  • terrain.worker.ts — decodes biomeId from combined buffer, handles loadWorldConfig
  • WasmClient.ts — generateChunk returns biomeId; adds loadWorldConfig()
  • ChunkManager.ts — ChunkGPUData stores biomeId

Test Results

  • ✅ All Go tests pass (go test ./...)
  • ✅ All 56 TypeScript tests pass (npm test)
  • ✅ Docker prod build passes and serves 200 OK

Visual Result

Walking in the world now encounters dramatically different terrain shapes: flat swamp regions, towering jagged mountains (3× standard height), smooth desert dunes, and moderate rolling forest terrain.

maxfelker and others added 3 commits March 1, 2026 15:28
Add biome system foundation with 6 biomes (Grassland, Desert, Mountains,
Valley, Swamp, Forest) each with unique noise parameters and height
multipliers, driven by temperature+humidity noise maps with domain warping.

Go WASM changes:
- wasm/biome/: new package with BiomeDefinition, BiomeRegistry (6 biomes),
  domain-warped Whittaker biome selector, WorldConfig, and unit tests
- wasm/main.go: goGenerateChunk now applies biome noise params (octaves,
  frequency, lacunarity, persistence) and height multiplier per chunk;
  appends biomeId at end of combined Float32Array; adds go_loadWorldConfig
  WASM export

TypeScript changes:
- src/engine/biome/BiomeTypes.ts: BiomeType const object + WorldConfig type
- src/engine/biome/BiomeRegistry.ts: client-side biome metadata
- WasmBridge.ts: updated global declarations for go_loadWorldConfig and
  updated go_generateChunk return comment
- terrain.worker.ts: decodes biomeId from combined buffer (last element);
  handles loadWorldConfig CALL method
- WasmClient.ts: generateChunk returns biomeId; adds loadWorldConfig()
- ChunkManager.ts: ChunkGPUData stores biomeId; generateChunk propagates it

Visual: Desert chunks use low-octave smooth dunes, Mountains use 7-octave
jagged peaks at 3x height, Swamp nearly flat at 0.25x height.

All Go tests pass. All 56 TS tests pass. Docker prod build verified (200 OK).

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Root cause: applying a single biome HeightMultiplier to an entire chunk caused
height mismatches at boundaries where adjacent chunks had different biomes.
A Mountains chunk (3.0×) next to a Grassland chunk (1.0×) produced visible
sky-coloured rectangular gaps where the mesh edges didn't align.

Fix: replace chunk-center biome lookup with per-vertex world-position sampling.
Both sides of a shared edge now compute the same world coordinate → same biome
→ same noise config → matching height → no seam.

New wasm/biome/generator.go:
- GenerateHeightmapPerVertex: samples biome at each vertex world position,
  applies that biome's noise params + HeightMultiplier per vertex
- GenerateExtendedHeightmapPerVertex: same approach for the (res+2)² extended
  heightmap used in cross-boundary normal computation

Updated wasm/main.go:
- goGenerateChunk uses GenerateHeightmapPerVertex /
  GenerateExtendedHeightmapPerVertex instead of single-biome approach
- dominant biome (by vertex count) still returned as biomeId

New tests:
- TestGenerateHeightmapPerVertex_SeamContinuity: verifies right edge of
  chunk(0,0) exactly matches left edge of chunk(1,0) (diff < 1e-4)
- TestGenerateExtendedHeightmapPerVertex_Size: size correctness

All Go tests pass. All 56 TS tests pass. Docker prod build verified.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Previous approach: hard per-vertex biome selection meant vertices on opposite
sides of a classification threshold (e.g. temp < 0.28 = Mountains vs Grassland)
used completely different noise configs and HeightMultipliers, creating jagged
wall/spine artifacts visible at every biome boundary.

Fix: replace hard classification with Gaussian-weighted biome blending.

How it works:
1. GetBiomeParams() returns continuous temperature + humidity noise values
   (extracted from GetBiomeAt so both functions share the sampling logic)
2. gaussianBiomeWeights() computes a normalised [6]float64 blend weight for
   each biome using a Gaussian distance from each biome's climate center in
   temp×humidity space (sigma ≈ 0.18-0.24)
3. blendedHeight() samples terrain height for every biome that has weight > 0.5%
   and returns the weighted average
4. GenerateHeightmapPerVertex / GenerateExtendedHeightmapPerVertex use
   blendedHeight() at every vertex

Effect: at a Mountains/Grassland boundary the vertex gets ~57% Mountain height
+ ~43% Grassland height instead of a hard jump. The transition spans ~1500
world units (≈3 chunks) creating smooth, organic biome transitions.
Chunk boundary seams remain gap-free because identical world coords produce
identical blended results on both sides.

Tests added:
- TestGenerateHeightmapPerVertex_Smoothness: verifies no adjacent-vertex diff > 0.80
- TestGaussianBiomeWeights_SumToOne: sanity-checks classification at boundary points

All Go tests pass. All 56 TS tests pass. Docker prod build verified.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
@maxfelker maxfelker merged commit 1f47d3d into main Mar 1, 2026
1 check passed
@maxfelker maxfelker deleted the feature/biome-m1-foundation branch March 1, 2026 23:54
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant