Appearance
How to Build: Biome Blending
If you have ever looked at terrain in a game and wondered why it feels alive -- why the grass fades into dirt near the road, why moss creeps across stone at the edges of a cave -- the answer is usually some form of noise-driven texture blending. That is exactly what the biome system does for Endless Idle.
Each depth tier in the game has its own visual identity. The Hollows feel like warm underground stone lit by torchlight. The Verdant Deep is overgrown with bioluminescent flora. The biome system is what makes those identities real: it controls which textures appear, how they are weighted per vertex, and how they blend across the terrain surface.
The good news is that we already built this system once in beta, and it works well. The main job here is porting it over and creating new tier-specific biome definitions.
How the biome system works
There are two halves:
Data side: A BiomeDefinition declares 4 texture layers, each with its own noise configuration, coverage bias, tint color, and optional variant texture. These layers correspond directly to the 4 splat channels in the terrain material.
Runtime side: During chunk generation, the biome's noise config drives per-vertex texture weight calculation. The material shader then samples 4 textures per vertex and blends them using those weights, producing smooth terrain transitions with no hard edges.
BiomeDefinition --> noise pass (per vertex) --> splat weights --> material shader --> blended terrainThe BiomeDefinition shape
This is the core data structure. Understanding it is essential:
typescript
interface BiomeDefinition {
noise: NoiseConfig // terrain height noise
layers: [TextureLayer, TextureLayer, TextureLayer, TextureLayer]
}
interface NoiseConfig {
algorithm: 'perlin' | 'ridged' | 'billow' | 'caldera'
frequency: number
octaves: number
heightScale: number
exponent: number
warpStrength: number
}
interface TextureLayer {
textureId: string // key into TERRAIN_TEXTURES
tint: [number, number, number] // RGB multiplier
noise: NoiseConfig // independent noise for this layer
coverage: number // bias: higher = more of this texture
variant?: { // optional secondary texture
textureId: string
noise: NoiseConfig
}
}Notice how each layer has its own independent noise configuration. This is what gives the terrain its organic feel -- the grass layer might use low-frequency smooth noise while the rock layer uses high-frequency ridged noise, and they overlap naturally because their noise fields are independent.
There are no height thresholds here. Unlike a lot of terrain systems that say "rock above 500m, snow above 800m," our weights are entirely noise-computed per vertex. This gives us much more control over the artistic result.
The activeBiome singleton
The runtime biome state lives in a mutable singleton called activeBiome in terrain/biomes/registry.ts. Systems read from it directly. The pattern:
typescript
// In registry.ts
const BIOMES: Map<string, BiomeDefinition> // all registered presets
let activeBiome: BiomeDefinition // the current one
function loadBiomePreset(id: string): void // deep-clones preset into activeBiomeWhen a level is generated, the level manager calls loadBiomePreset with the tier's biome ID, and the noise pass reads from activeBiome to compute per-vertex weights. Because it is a deep clone, you can safely mutate activeBiome at runtime (for debug tweaking, for example) without corrupting the preset.
Creating tier-specific biomes
We are replacing the 6 beta biomes (grassland, forest, desert, mountains, tundra, volcanic) with tier-specific definitions:
Hollows (Tier 1)
Warm stone textures with muted earth tones and torchlight amber tinting. Start from the beta desert and mountains definitions but push toward warmer hues. Think sandstone corridors, dusty paths, amber-lit cavern walls.
Verdant Deep (Tier 2)
Deep greens over dark earth, with bioluminescent blues and purples in the variant layers. Start from the beta forest definition and shift the tints toward deeper, cooler colors. Think overgrown underground gardens with faintly glowing moss.
Each definition follows the same BiomeDefinition shape with 4 texture layers. The creative work is in choosing the right texture IDs, noise parameters, and tint values to sell the fantasy.
The material shader
The terrain material is built with Three.js Shading Language (TSL) and lives in terrain-material.ts. It reads three per-vertex attributes:
| Attribute | Type | Purpose |
|---|---|---|
w0, w1, w2, w3 | float | Splat weights (sum to ~1.0) |
ti0, ti1, ti2, ti3 | int | Texture indices into the catalogue |
tintR, tintG, tintB | float | Per-vertex color multiplier |
The shader samples 4 textures by index, multiplies each sample by its splat weight, sums them, and applies the tint. This runs identically regardless of whether the vertex data came from authored .bin files or the WFC pipeline.
You should not need to modify the shader at all. Just make sure the texture IDs in your biome definitions are registered in TERRAIN_TEXTURES.
Wiring biome selection to level generation
When the level manager starts a new level, it loads the appropriate biome for that tier:
typescript
const TIER_BIOMES: string[] = ['hollows', 'verdant-deep']
function startLevel(world: World, levelNumber: number): void {
const tierIndex = getTierIndex(levelNumber)
loadBiomePreset(TIER_BIOMES[tierIndex])
// ... then generate terrain, which reads from activeBiome
}The noise pass in the WFC pipeline reads from activeBiome to compute per-vertex weights. Because loadBiomePreset is called before generation begins, the weights automatically reflect the correct tier's visual style.
Files to port from beta
These are all stable and well-tested:
| File | What to do |
|---|---|
terrain/biomes/types.ts | Port directly -- BiomeDefinition, NoiseConfig, TextureLayer |
terrain/biomes/noise.ts | Port directly -- Perlin, fractional Brownian motion (fBm), ridged, billow, caldera algorithms |
terrain/biomes/registry.ts | Port directly -- BIOMES map, activeBiome, loadBiomePreset |
terrain/biomes/definitions/ | Create new files for hollows and verdant-deep |
terrain/materials/terrain-texture-registry.ts | Port directly -- texture catalogue and uniform nodes |
terrain/materials/terrain-material.ts | Port directly -- TSL splat blending shader |
If your tier biomes need textures that are not in the beta catalogue, add them to TERRAIN_TEXTURES and place the image files in the public assets directory.
What comes next
With biome blending in place, the terrain looks rich and varied per tier. But we still need a camera to actually look at it. Next up is the orbit camera system that follows the player along the terrain surface.