Land and Sky

THE KITCHEN SINK — one world that grows with the world-sim layer. Terrain (with live dials), a sea, weather over both, and the encoded sky behind. As provinces, weather systems and the simulation land, they land HERE first: this page is where the pieces meet, so it is also where their disagreements show first — which is the point.

Demo

import { b3d, b3dSun, b3dSkybox, b3dMoon, b3dWeatherCell, b3dLightning, b3dAmbient, b3dLightShafts, b3dTerrain, b3dCloudDeck, b3dDecorator, b3dWater, b3dLight, b3dFog, label3d, slider3d, toggle3d, select3d, button3d, row3d, volcano, craterField, composeLandforms, mergeProvinces, ROCK_RULES } from 'tosijs-3d'
import { tosi } from 'tosijs'

const { demo } = tosi({
  demo: {
    seed: 111,
    grossScale: 0.01,
    detailScale: 0.09,
    horizScale: 3.31,
    grossAmplitude: 230,
    detailAmplitude: 45,
    // Sea level as a FRACTION of v-size, so the ocean scales with the
    // mountains — the same world at any amplitude keeps the same share of
    // land and sea.
    seaLevel: 0.64,
    volcano: true,
    // CLIMATE, above water. Moisture is the one that matters most: the biome
    // chart's default 0.45 sits between the dry row (dune) and the medium
    // row (steppe), which is why a warm world with a coral reef below the
    // waterline read as barren above it. Land sits on the chart's rows at
    // three quarters of its moisture (the top row is the sea's), so forest
    // wants nearly 1: 0.95, which height and the regions then dry in places.
    // TEMPERATURE: 0 is 0 °C and a unit is 50 °C, so this is 22.5 °C.
    temperature: 0.45,
    moisture: 0.95,
    // Plates sized to THIS volcano (420 m). The plugin's 0.09 was tuned on a
    // 55 m cone, where it gives ~11 m plates; here that is gravel.
    volcanicScale: 0.02,
    // The spring equinox (no temperature offset, so the default look is
    // unchanged), with a temperate swing ready for the 'time of year' slider.
    season: 0,
    seasonality: 0.2,
    // How cratered the ground is (0 = none; a preset sets it: Mars, the Moon).
    craters: 0,
    // A field of volcanoes (Io): how many, 0 = none.
    volcanoes: 0,
    // The ground's palette: 'earth' (the biome chart's own) or 'sulfur' (Io).
    palette: 'earth',
    // The sea's colours: its surface tint and the fog you see from inside it
    // (a methane sea, Titan, is dark amber).
    waterColor: '#0066cc', waterFog: '#00264d', waterTint: 0.1,
    // Is there a sea at all? A dry world (Mars, Venus) has none: no water
    // plane, and no shoreline or seafloor colours.
    sea: true,
    // A SPHERE WORLD: the same ground laid on a planet of this radius, so
    // latitude has a climate (cooler toward the poles, ice at them) and a
    // globe built from the terrain shows the same places. Off, the ground
    // is flat and the climate is the same everywhere but for its regions.
    sphere: false,
    planetRadius: 50, // km
    wireframe: false,
  },
})

// Weather is ONE dial at the top (coverage drives transmission, gloom and
// depth) plus the shaping ones: wind slides the sky, evolve reshapes it,
// cirrus thins it, orographic asks the terrain for its own height sampler so
// the towers build over the mountains that are actually there.
const { sky } = tosi({
  sky: {
    coverage: 0.1, altitude: 280, timeOfDay: 18.5, orographic: 0.8, wind: 10, cirrus: 0.25, evolve: 0.5, eye: 205,
    // What the deck is actually given: the two dials above, thinned by how
    // much air there is (see cloudAir). Not saved in presets.
    cloudCover: 0.1, cloudOrographic: 0.8,
    storm: false,
    // The atmosphere. `atmosphere` is how much air the WORLD has (0 is the
    // Moon: black noon, stars out); the tints colour the scattered light only.
    world: 'Earth', atmosphere: 1, dust: 0, turbidity: 10, rayleigh: 2, mieCoefficient: 0.005, luminance: 1,
    zenithTint: '#ffffff', horizonTint: '#ffffff', tintStrength: 0,
    // The stars: size (1 = the default point), brightness, and the faint floor.
    // Does anything GROW here? Off, the decorator places rocks alone.
    decoLife: true,
    decoBudget: 2000, decoRadius: 900, decoShadows: false, decoClump: 0.85, decoClumpSize: 0,
    starSize: 1, starGain: 0.9, starFloor: 0.4, starSharpness: 3, twinkle: 0.35,
    // Extra (cosmetic) moons: a set, swung round the sky together.
    moons: 'Big moon', moonAz: 0, moonEl: 0,
    // The cloud deck's colours (a Venus deck is sulfur-yellow), and the sun:
    // its apparent size and how bright its light looks.
    deckColor: '#ffffff', deckUnderColor: '#3a4350', sunSize: 1, sunBrightness: 1,
    // The lightning storm: where it forms, how big, the cover it adds and how
    // often it strikes (a multiple of the natural rate). Presets set these.
    stormX: 1600, stormZ: 300, stormRadius: 900, stormCoverage: 1.7, lightningRate: 1, stormRain: 0.9,
    // The world preset in force (see PRESETS below).
    preset: 'Earth',
  },
})

// A world is a few dials at once — the preset writes them, and the sliders
// stay live afterwards so you can walk away from the preset.
// Mars is almost no AIR and a lot of DUST: its sky is bright because of the
// dust, not the gas (under 1% of Earth's). Gas scatters blue; dust scatters a
// bright haze the tint colours.
const WORLDS = {
  Earth: { atmosphere: 1, dust: 0, zenithTint: '#ffffff', horizonTint: '#ffffff', tintStrength: 0 },
  Mars: { atmosphere: 0.15, dust: 0.85, zenithTint: '#c8a070', horizonTint: '#e0b080', tintStrength: 1 },
  Alien: { atmosphere: 1, dust: 0, zenithTint: '#60c080', horizonTint: '#b0e0a0', tintStrength: 0.7 },
  Airless: { atmosphere: 0, dust: 0, zenithTint: '#ffffff', horizonTint: '#ffffff', tintStrength: 0 },
}
// SIZE is the inverse of the shader's sharpness: a gaussian's width goes as
// 1/sqrt(sharpness), so size 2 is twice the width of the default point.
sky.starSize.observe(() => {
  sky.starSharpness.value = 3 / Math.max(0.05, sky.starSize.value) ** 2
})
// COSMETIC MOONS — where, how big, what colour. The phase is not a setting:
// swing them round with the slider and watch it follow from the sun.
const MOONS = {
  None: [],
  'Big moon': [{ azimuth: 0, elevation: 25, size: 4, color: '#d8d4cc' }],
  'Mars pair': [
    { azimuth: 0, elevation: 20, size: 0.6, color: '#b09a88' },
    { azimuth: 40, elevation: 32, size: 0.25, color: '#c8b8a8' },
  ],
  // The Moon's sky: Earth, about 3.7 times the size our Moon looks from here.
  // (Moons ride the star sphere; this sits up ahead of the default view in
  // the morning.)
  Earth: [{ azimuth: 60, elevation: 22, size: 3.7, color: '#6f9ad8' }],
  // A giant planet from a close moon: Jupiter from Io is about 19 degrees
  // across. `gas` makes the body a banded gas giant (see gas-giant).
  // `frame: 'ground'`: the moon is tide-locked, so its planet never sets.
  Jupiter: [{ frame: 'ground', azimuth: 90, elevation: 13, size: 19, gas: 'jupiter', axisTilt: 8 }],
  Saturn: [{ frame: 'ground', azimuth: 90, elevation: 12, size: 11, gas: 'saturn', axisTilt: 20 }],
  Neptune: [{ frame: 'ground', azimuth: 90, elevation: 12, size: 8, gas: 'neptune', axisTilt: 25 }],
  'Alien trio': [
    { azimuth: 0, elevation: 30, size: 6, color: '#e0b090' },
    { azimuth: 25, elevation: 12, size: 2, color: '#a0c8ff' },
    { azimuth: -30, elevation: 40, size: 1, color: '#d0ffd0' },
  ],
}
const skybox = b3dSkybox({
  timeOfDay: sky.timeOfDay,
  realtimeScale: 0,
  starfieldCube: '/sky/nebula',
  starfieldData: '/sky/stars',
  starfieldTilt: '12,25,58',
  atmosphere: sky.atmosphere,
  dust: sky.dust,
  turbidity: sky.turbidity,
  rayleigh: sky.rayleigh,
  mieCoefficient: sky.mieCoefficient,
  luminance: sky.luminance,
  zenithTint: sky.zenithTint,
  horizonTint: sky.horizonTint,
  tintStrength: sky.tintStrength,
  starfieldSharpness: sky.starSharpness,
  starfieldGain: sky.starGain,
  starfieldFloor: sky.starFloor,
  starfieldTwinkle: sky.twinkle,
  sunSize: sky.sunSize,
  sunBrightness: sky.sunBrightness,
})

const moonEls = []
function placeMoons() {
  const set = MOONS[sky.moons.value] ?? []
  while (moonEls.length > set.length) moonEls.pop().remove()
  set.forEach((m, i) => {
    if (moonEls[i] == null) {
      moonEls[i] = b3dMoon()
      skybox.append(moonEls[i])
    }
    // Every field, so a body left over from another set does not keep its
    // colour or stay a gas giant.
    Object.assign(moonEls[i], { color: '#dddddd', gas: '', axisTilt: 0, frame: 'stars' }, m, {
      azimuth: m.azimuth + sky.moonAz.value,
      elevation: m.elevation + sky.moonEl.value,
    })
  })
}
sky.moons.observe(placeMoons)
sky.moonAz.observe(placeMoons)
sky.moonEl.observe(placeMoons)
placeMoons()
sky.world.observe(() => {
  const w = WORLDS[sky.world.value]
  if (w) for (const k of Object.keys(w)) sky[k].value = w[k]
})

// PRESETS: A WHOLE WORLD AS DATA (board #2442). Terrain, sea, climate,
// weather, atmosphere, stars, moons, the cloud deck's colour and the sun, as
// one plain object you can pick, save and share. A preset only lists what it
// changes; applying one resets everything else to Earth first, so nothing
// leaks from the last world.
//
// THE SUN'S BRIGHTNESS FOLLOWS THE EYE, not the inverse square. A world
// further from its star gets far less light (Titan about 1/90 of Earth's),
// but vision is logarithmic and adapts, and even from Pluto the sun is some
// 300 times brighter than the full moon. So its disc shrinks for real (1 /
// distance) while its light only dims gently, on a log curve.
const PRESET_KEYS = {
  demo: ['seed', 'grossScale', 'detailScale', 'horizScale', 'grossAmplitude', 'detailAmplitude', 'seaLevel', 'sea', 'waterColor', 'waterFog', 'waterTint', 'palette', 'volcano', 'volcanoes', 'craters', 'temperature', 'moisture', 'volcanicScale', 'season', 'seasonality', 'sphere', 'planetRadius'],
  sky: ['coverage', 'altitude', 'timeOfDay', 'orographic', 'wind', 'cirrus', 'evolve', 'atmosphere', 'dust', 'turbidity', 'rayleigh', 'mieCoefficient', 'luminance', 'zenithTint', 'horizonTint', 'tintStrength', 'starSize', 'starGain', 'starFloor', 'twinkle', 'moons', 'moonAz', 'moonEl', 'deckColor', 'deckUnderColor', 'sunSize', 'sunBrightness', 'decoBudget', 'decoLife', 'stormX', 'stormZ', 'stormRadius', 'stormCoverage', 'lightningRate', 'stormRain',
    // LAST: switching the storm on builds it from the values above.
    'storm'],
}
const STATE = { demo, sky }
// `scale: 2`: temperature is 0 = 0 °C, 1 = 50 °C. Presets saved before that
// carry the biome chart's 0…1 axis and are converted when read.
function capturePreset(name) {
  const p = { name, scale: 2 }
  for (const [group, keys] of Object.entries(PRESET_KEYS)) {
    p[group] = {}
    for (const k of keys) p[group][k] = STATE[group][k].value
  }
  return p
}
function migratePreset(p) {
  if (p == null || p.scale === 2) return p
  const t = p.demo?.temperature
  return {
    ...p,
    scale: 2,
    demo: typeof t === 'number' ? { ...p.demo, temperature: +((t - 0.36) / 0.8).toFixed(3) } : p.demo,
  }
}
function applyPreset(p) {
  for (const [group, keys] of Object.entries(PRESET_KEYS)) {
    const vals = p[group] ?? {}
    for (const k of keys) if (k in vals) STATE[group][k].value = vals[k]
  }
}
// How bright a sun LOOKS from `au` astronomical units (1 = Earth).
const sunLight = (au) => Math.max(0.35, 1 - 0.3 * Math.log10(au * au))
const EARTH = capturePreset('Earth') // the defaults above
const BUILT_IN = {
  Earth: EARTH,
  // Thin, dusty air (its sky is bright from DUST, not gas), red desert, no
  // seas, a smaller sun, two little moons, and Olympus Mons for the volcano.
  Mars: {
    name: 'Mars',
    // A HACK, for the colour: 40 °C, where Mars is really about -60 °C
    // (-1.2). The palette's red dust is the warm end of its driest row, so a
    // true Mars would land on the cold end and go grey. Wants its own palette.
    // (No volcano for now: Earth's province, a stand-in until each world
    // gets its own landforms.)
    demo: { seaLevel: 0, temperature: 0.8, moisture: 0, sea: false, volcano: false, craters: 0.35 },
    sky: { coverage: 0.06, cirrus: 0.6, atmosphere: 0.03, dust: 0.85, zenithTint: '#c8a070', horizonTint: '#e0b080', tintStrength: 1, deckColor: '#f0e0d0', deckUnderColor: '#8a7060', sunSize: 0.66, sunBrightness: sunLight(1.52), moons: 'Mars pair', decoBudget: 1500, decoLife: false },
  },
  // ONE GIANT LIGHTNING STORM under a closed deck of sulfur-yellow cloud,
  // cover maxed: a storm 8 km across centred on you, striking four times as
  // often, dark between flashes. Climb out through the whiteout and the bigger sun is
  // there.
  Venus: {
    name: 'Venus',
    // Venus's surface is YOUNG (resurfaced by volcanism): few craters.
    // Mostly smooth lava plains (Magellan radar): gentle relief. And no rain:
    // Venus's sulfuric acid evaporates long before it reaches the ground.
    // 465 °C, and here the real number works: the chart clamps to its hot row.
    demo: { seaLevel: 0, temperature: 9.3, moisture: 0, sea: false, volcano: false, craters: 0.06, grossAmplitude: 90, detailAmplitude: 8 },
    sky: { storm: true, stormX: 0, stormZ: 0, stormRadius: 8000, stormCoverage: 0.4, lightningRate: 4, stormRain: 0, wind: 3, coverage: 2, altitude: 900, orographic: 0, dust: 0.4, zenithTint: '#e8c880', horizonTint: '#f0d890', tintStrength: 0.85, deckColor: '#f2e2a8', deckUnderColor: '#c0a060', sunSize: 1.39, sunBrightness: sunLight(0.72), moons: 'None', decoBudget: 0 },
  },
}
// Presets for the outer worlds sit below BUILT_IN's literal so they can use
// sunLight; they are added to it here.
Object.assign(BUILT_IN, {
  // Airless, saturated with craters, cold grey regolith, stars at noon, and
  // Earth in the sky. (Lava tubes to come.)
  Moon: {
    name: 'Moon',
    // FLATTISH, so the craters carry the shape (Earth's 230 m relief buried
    // them). Jagged highlands would be a mountain province on top.
    demo: { sea: false, volcano: false, craters: 0.9, temperature: -0.45, moisture: 0, grossAmplitude: 30, detailAmplitude: 4 },
    sky: { atmosphere: 0, dust: 0, coverage: 0, cirrus: 0, orographic: 0, sunSize: 1, sunBrightness: 1, moons: 'Earth', decoBudget: 1500, decoLife: false, wind: 0 },
  },
  // Sulfur and fire: yellow, orange and white ground, black lava, a field of
  // hot volcanoes, a tiny sun, and Jupiter filling a fifth of the sky. No air
  // to speak of. (Plumes to come.)
  Io: {
    name: 'Io',
    // (A hack: 9 °C picks the sulfur palette's middle row. Io is about -130 °C.)
    demo: { sea: false, volcano: false, volcanoes: 8, craters: 0, temperature: 0.175, moisture: 0, palette: 'sulfur' },
    sky: { atmosphere: 0, dust: 0, coverage: 0, cirrus: 0, orographic: 0, sunSize: 0.19, sunBrightness: sunLight(5.2), moons: 'Jupiter', decoBudget: 0, wind: 0 },
  },
  // Thick orange haze over dark methane seas, a tiny, dim sun barely there.
  Titan: {
    name: 'Titan',
    // (A hack: 16 °C, not Titan's -180 °C (-3.6). The real number reads as
    // ice everywhere and would freeze the sea, which is methane and should
    // not. Wants its own palette and a freezing point per sea.)
    demo: { sea: true, seaLevel: 0.45, volcano: false, craters: 0.05, temperature: 0.325, moisture: 0.02, waterColor: '#1a0e04', waterTint: 0.75, waterFog: '#2a1a08' },
    sky: { atmosphere: 1, dust: 1, zenithTint: '#b87830', horizonTint: '#d09040', tintStrength: 1, coverage: 0.5, cirrus: 0.3, orographic: 0.2, deckColor: '#d8a060', deckUnderColor: '#806030', sunSize: 0.11, sunBrightness: sunLight(9.5), moons: 'None', decoBudget: 0, wind: 2 },
  },
})

const CUSTOM_KEY = 'land-and-sky:presets'
function loadCustom() {
  try {
    return JSON.parse(localStorage.getItem(CUSTOM_KEY) || '{}') || {}
  } catch {
    return {}
  }
}
function saveCustom(p) {
  try {
    localStorage.setItem(CUSTOM_KEY, JSON.stringify({ ...loadCustom(), [p.name]: p }))
  } catch {
    // private window: the preset still applies, it just is not kept
  }
}
const presetNames = () => [...Object.keys(BUILT_IN), ...Object.keys(loadCustom())]
// NO AIR, NO CLOUD. The cloud dials say how cloudy the weather is; how much
// cloud that makes depends on there being air to hold it. Full from about a
// third of Earth's air up, none below a twentieth, so Mars (0.03) has none.
function cloudAir() {
  const k = Math.min(1, Math.max(0, (sky.atmosphere.value - 0.05) / 0.25))
  sky.cloudCover.value = sky.coverage.value * k
  sky.cloudOrographic.value = sky.orographic.value * k
}
sky.coverage.observe(cloudAir)
sky.orographic.observe(cloudAir)
sky.atmosphere.observe(cloudAir)
cloudAir()
sky.preset.observe(() => {
  const p = BUILT_IN[sky.preset.value] ?? migratePreset(loadCustom()[sky.preset.value])
  if (p == null) return
  applyPreset(EARTH)
  applyPreset(p)
})

// The volcano is authored ONCE and switched in and out. Applied here as well
// as from the toggle because it is ON by default, and the toggle's handler only
// runs when someone flips it.
const theVolcano = volcano({ x: 600, z: -400, radius: 420, height: 260, craterRadius: 55, craterDepth: 50 })
// Where the sea is: a FRACTION of the terrain's height, or, on a world
// with no sea, far below everything (no water to see).
function seaY() {
  return demo.sea.valueOf() ? demo.seaLevel * demo.grossAmplitude : -10000
}
// The BIOME's datum is separate: it also classifies by altitude above it, so
// on a dry world it sits just under the deepest crater floors (no shore, no
// seafloor colours) rather than kilometres down (which read as bare peaks).
function biomeSea() {
  return demo.sea.valueOf() ? demo.seaLevel * demo.grossAmplitude : -250
}

// Temperature falls with height above the SEA; with no sea there is no
// datum, so a dry world keeps its preset temperature at every height.
function lapse() {
  // (Not 0: the terrain reads 0 as UNSET and falls back to its 0.004, an
  // arctic world. And not 1e-6, which the attribute path rounds to 0.)
  return demo.sea.valueOf() ? 0.5 / demo.grossAmplitude : 0.0002
}

// A FIELD of volcanoes (Io): seeded positions around you, small paterae
// with hot floors. Each is the ordinary volcano landform.
function volcanoField(n, seed) {
  const out = []
  let s = seed * 9301 + 49297
  const rnd = () => ((s = (s * 9301 + 49297) % 233280) / 233280)
  for (let i = 0; i < n; i++) {
    const a = rnd() * Math.PI * 2
    const d = 250 + rnd() * 1600
    out.push(volcano({ x: Math.cos(a) * d, z: Math.sin(a) * d, radius: 160 + rnd() * 220, height: 30 + rnd() * 90, craterRadius: 50 + rnd() * 60, craterDepth: 20 + rnd() * 30 }))
  }
  return out
}

// Landforms COMPOSE: the craters, then the volcanoes on top of them.
function applyVolcano(on) {
  const parts = []
  const provinces = []
  const density = demo.craters.valueOf()
  if (density > 0) parts.push(craterField({ seed: demo.seed.valueOf(), density, maxRadius: 500 }))
  for (const v of volcanoField(demo.volcanoes.valueOf(), demo.seed.valueOf())) {
    parts.push(v.landform)
    provinces.push(v.province)
  }
  if (on) {
    parts.push(theVolcano.landform)
    provinces.push(theVolcano.province)
  }
  terrain.landform = parts.length === 0 ? null : parts.length === 1 ? parts[0] : composeLandforms(...parts)
  terrain.provinceField = provinces.length === 0 ? null : provinces.length === 1 ? provinces[0] : mergeProvinces(...provinces)
}
demo.volcanoes.observe(() => {
  applyVolcano(demo.volcano.valueOf())
  terrain.regenerate()
})

// THE GROUND'S PALETTE, swapped on the terrain's biome shader (it reads its
// palette every frame). Io's sulfur: frost white, pale and deep yellow,
// orange-red, and black lava for the variation.
const PALETTES = {
  sulfur: {
    a: [[0.92, 0.9, 0.82], [0.9, 0.84, 0.42], [0.86, 0.68, 0.18], [0.74, 0.38, 0.14]],
    b: [[0.8, 0.78, 0.7], [0.14, 0.12, 0.1], [0.7, 0.55, 0.15], [0.3, 0.14, 0.06]],
  },
}
let earthPalette = null
function applyPalette() {
  const plugin = terrain.biomePlugin
  if (plugin == null) return false
  if (earthPalette == null) earthPalette = { a: plugin.palette, b: plugin.paletteB }
  const p = PALETTES[demo.palette.valueOf()]
  // One row of four, repeated for every moisture row (Io has no weather).
  plugin.palette = p ? Array.from({ length: 5 }, () => p.a).flat() : earthPalette.a
  plugin.paletteB = p ? Array.from({ length: 5 }, () => p.b).flat() : earthPalette.b
  return true
}
demo.palette.observe(applyPalette)
demo.craters.observe(() => {
  applyVolcano(demo.volcano.valueOf())
  terrain.regenerate()
})
// A new seed re-rolls the craters too (registered before the terrain's own
// seed observer below, so the landform is current when it regenerates).
demo.seed.observe(() => applyVolcano(demo.volcano.valueOf()))

// 'on'|'off' on the element, a boolean on the toggle — bridged here.
const decorator = b3dDecorator({ budget: sky.decoBudget, radius: sky.decoRadius, clump: sky.decoClump, clumpSize: sky.decoClumpSize })
sky.decoShadows.observe(() => {
  decorator.shadows = sky.decoShadows.value ? 'on' : 'off'
})
// A dead world keeps its rocks: the rules without the plants.
const applyLife = () => {
  decorator.rules = sky.decoLife.value ? null : ROCK_RULES
  decorator.rebuild?.()
}
sky.decoLife.observe(applyLife)
applyLife()

let water
const terrain = b3dTerrain({
  seed: demo.seed,
  biome: 'on',
  surfaceType: demo.sphere.valueOf() ? 'sphere' : 'cylinder',
  radius: demo.sphere.valueOf() ? demo.planetRadius.valueOf() * 1000 : 1000,
  cylinderHeight: 1000,
  tileSize: 128,
  lodLevels: 3,
  splitFactor: 2,
  reach: 5000,
  grossScale: demo.grossScale,
  detailScale: demo.detailScale,
  horizScale: demo.horizScale,
  grossAmplitude: demo.grossAmplitude,
  detailAmplitude: demo.detailAmplitude,
  wireframe: demo.wireframe,
  // The biome classifier's snow line anchors to the SAME sea level as the
  // water plane — keep them equal or the islands between the old and new
  // water line render as snowcaps. The lapse rate MUST scale to the
  // vertical range (0.5 / amplitude gives temperate valleys and cold
  // summits — the element's own attribute note): the 0.004 default assumes
  // a small world, and at v-size 400 every peak would still read as snow.
  biomeSeaLevel: biomeSea(),
  biomeLapseRate: lapse(),
  biomeTemperature: demo.temperature,
  biomeMoisture: demo.moisture,
  biomeVolcanicScale: demo.volcanicScale,
  biomeSeason: demo.season,
  biomeSeasonality: demo.seasonality,
})

applyVolcano(demo.volcano.valueOf())
// Observed rather than handled on the toggle, so a PRESET switching the
// volcano regenerates the terrain too.
demo.volcano.observe(() => {
  applyVolcano(demo.volcano.valueOf())
  terrain.regenerate()
})

// A LIGHTNING STORM, as weather rather than scenery (WEATHER-DESIGN): a cell
// with coverage and storminess, drifting with the wind. It gathers IN FRONT
// of you (the view faces east) over twenty seconds, so switching it on shows
// you a storm, then drifts away east. Lightning, thunder, rain and strike
// shadows all come from the cell. Toggling it on again starts a new one.
let storm = null
sky.storm.observe(() => {
  storm?.remove()
  storm = null
  if (sky.storm.value) {
    storm = b3dWeatherCell({ x: sky.stormX.value, z: sky.stormZ.value, radius: sky.stormRadius.value, coverage: sky.stormCoverage.value, storminess: 1, precipitation: sky.stormRain.value, drift: 'wind', grow: 20 })
    scene.append(storm)
  }
})

const scene = b3d(
  {
    // The SCENE's wind matches the deck's (toward +X), so a drifting storm
    // travels with the clouds it is made of.
    windSpeed: sky.wind,
    windBearingDeg: 90,
    // Sections as icon TABS (one at a time); 'fold' shows them as headers.
    panelSections: 'tabs',
    // Lightning strikes wherever a weather cell is stormy (the storm toggle).
    // Controls live in the dual-presence scene panel: a ⚙ toggles them on flat
    // screens, and the SAME panel floats in front of you in VR.
    scenePanel: () => [
      label3d({ text: 'World', icon: 'earth', collapsible: true, open: true }),
      // Pick a world; everything you DO with presets is one menu.
      row3d(
        { weights: [4, 1] },
        select3d({ label: 'preset', value: sky.preset, options: presetNames() }),
        button3d({
          label: '⋯',
          menu: [
            {
              label: 'Save as new preset',
              icon: 'plus',
              handleSelect: () => {
                const name = `Custom ${Object.keys(loadCustom()).length + 1}`
                saveCustom(capturePreset(name))
                sky.preset.value = name
                scene.refreshScenePanel?.()
              },
            },
            {
              label: 'Copy preset (JSON)',
              icon: 'uploadCloud',
              handleSelect: () => navigator.clipboard?.writeText(JSON.stringify(capturePreset(sky.preset.value), null, 2)),
            },
            {
              label: 'Paste preset (JSON)',
              icon: 'downloadCloud',
              handleSelect: async () => {
                try {
                  const p = JSON.parse(await navigator.clipboard.readText())
                  p.name = p.name || 'Pasted'
                  saveCustom(p)
                  sky.preset.value = p.name
                  scene.refreshScenePanel?.()
                } catch {
                  // not a preset on the clipboard
                }
              },
            },
          ],
        })
      ),
      // The clocks live with the world: the day, the year, and how much
      // the year matters.
      slider3d({ label: 'time of day', value: sky.timeOfDay, min: 0, max: 24, step: 0.25 }),
      // The year: 0.25 midsummer, 0.5 autumn, 0.75 midwinter. 'season strength'
      // is how hard it swings; at 0 the year does nothing.
      slider3d({ label: 'time of year', value: demo.season, min: 0, max: 1, step: 0.01 }),
      // The swing either side of the yearly mean. Stored in the biome chart's
      // units, where 1 spans 62.5 °C.
      slider3d({ label: 'season strength', value: demo.seasonality, min: 0, max: 0.6, step: 0.01, format: (v) => '±' + Math.round(v * 62.5) + ' °C' }),
      // Flat, or the same ground on a planet: you start at 45° north, and
      // the pole is a quarter of the way round.
      toggle3d({ label: 'sphere world', value: demo.sphere }),
      slider3d({ label: 'planet radius', value: demo.planetRadius, min: 2, max: 6400, scale: 'log', format: (v) => Math.round(v) + ' km' }),
      label3d({ text: 'Terrain', icon: 'terrain', collapsible: true }),
      slider3d({ label: 'gross scale', value: demo.grossScale, min: 0.005, max: 0.3, scale: 'log' }),
      slider3d({ label: 'detail scale', value: demo.detailScale, min: 0.02, max: 1, scale: 'log' }),
      slider3d({ label: 'h size', value: demo.horizScale, min: 0.25, max: 10, scale: 'log2' }),
      slider3d({ label: 'v size', value: demo.grossAmplitude, min: 0, max: 400, step: 1 }),
      slider3d({ label: 'sea level', value: demo.seaLevel, min: 0, max: 1, step: 0.02 }),
      slider3d({ label: 'v detail', value: demo.detailAmplitude, min: 0, max: 50, step: 0.5 }),
      slider3d({ label: 'seed', value: demo.seed, min: 0, max: 999, step: 1 }),
      // THE FIRST PROVINCE — an authored volcano forced through the live
      // terrain, with the volcanism field that makes it glow. The kitchen
      // sink grows from here.
      toggle3d({ label: 'volcano province', value: demo.volcano }),
      slider3d({ label: 'craters', value: demo.craters, min: 0, max: 1, step: 0.05 }),
      slider3d({ label: 'volcanoes', value: demo.volcanoes, min: 0, max: 16, step: 1 }),
      select3d({ label: 'ground palette', value: demo.palette, options: ['earth', ...Object.keys(PALETTES)] }),
      // Beside the volcano: the other thing you switch on to watch happen.
      label3d({ text: 'Climate', icon: 'thermometer', collapsible: true }),
      slider3d({ label: 'temperature', value: demo.temperature, min: -1, max: 1, step: 0.02, format: (v) => Math.round(v * 50) + ' °C' }),
      slider3d({ label: 'moisture', value: demo.moisture, min: 0, max: 1, step: 0.01 }),
      // How much one region differs from the next, and how big a region is.
      // Straight onto the shader's settings: the decorator reads the same ones.
      slider3d({ label: 'moisture variation', value: terrain.biomePlugin?.params.mNoiseAmp ?? 0.5, min: 0, max: 1, step: 0.01, handleChange: (v) => { if (terrain.biomePlugin) terrain.biomePlugin.params.mNoiseAmp = v } }),
      slider3d({ label: 'region size', value: 0.001 / (terrain.biomePlugin?.params.mNoiseScale ?? 0.001), min: 0.1, max: 10, scale: 'log', format: (v) => v.toFixed(1) + ' km', handleChange: (v) => { if (terrain.biomePlugin) terrain.biomePlugin.params.mNoiseScale = 0.001 / v } }),
      slider3d({ label: 'volcanic scale', value: demo.volcanicScale, min: 0.005, max: 0.15, scale: 'log' }),
      label3d({ text: 'Weather', icon: 'cloud', collapsible: true }),
      // With the clouds it is made of, not under Terrain where it used to be.
      toggle3d({ label: 'lightning storm', value: sky.storm }),
      slider3d({ label: 'cloud cover', value: sky.coverage, min: 0, max: 2, step: 0.02 }),
      slider3d({ label: 'cloud base', value: sky.altitude, min: 60, max: 1400, step: 10 }),
      slider3d({ label: 'orographic', value: sky.orographic, min: 0, max: 1, step: 0.05 }),
      slider3d({ label: 'wind', value: sky.wind, min: 0, max: 40, step: 1 }),
      // Signed: positive streaks ALONG the wind, negative ACROSS it.
      slider3d({ label: 'cirrus', value: sky.cirrus, min: -1, max: 1, step: 0.05 }),
      slider3d({ label: 'evolve', value: sky.evolve, min: 0, max: 1, step: 0.05 }),
      label3d({ text: 'Atmosphere', icon: 'sky', collapsible: true }),
      slider3d({ label: 'air', value: sky.atmosphere, min: 0, max: 1, step: 0.01 }),
      slider3d({ label: 'dust', value: sky.dust, min: 0, max: 1, step: 0.01 }),
      slider3d({ label: 'tint', value: sky.tintStrength, min: 0, max: 1, step: 0.05 }),
      slider3d({ label: 'turbidity', value: sky.turbidity, min: 1, max: 40, step: 0.5 }),
      slider3d({ label: 'rayleigh', value: sky.rayleigh, min: 0, max: 4, step: 0.05 }),
      slider3d({ label: 'mie', value: sky.mieCoefficient, min: 0, max: 0.05, step: 0.001 }),
      slider3d({ label: 'luminance', value: sky.luminance, min: 0.1, max: 2, step: 0.05 }),
      label3d({ text: 'Stars', icon: 'star', collapsible: true }),
      slider3d({ label: 'star size', value: sky.starSize, min: 0.4, max: 3, step: 0.05 }),
      slider3d({ label: 'star brightness', value: sky.starGain, min: 0, max: 3, step: 0.05 }),
      slider3d({ label: 'faint stars', value: sky.starFloor, min: 0, max: 1, step: 0.02 }),
      slider3d({ label: 'twinkle', value: sky.twinkle, min: 0, max: 1, step: 0.05 }),
      label3d({ text: 'Moons', icon: 'moon', collapsible: true }),
      select3d({ label: 'moons', value: sky.moons, options: Object.keys(MOONS) }),
      slider3d({ label: 'moon azimuth', value: sky.moonAz, min: -180, max: 180, step: 1 }),
      slider3d({ label: 'moon elevation', value: sky.moonEl, min: -60, max: 60, step: 1 }),
      label3d({ text: 'Vegetation', icon: 'tree', collapsible: true }),
      // THE BUDGET is the performance dial: a count, not a density. Watch the
      // Perf Stats panel's decorator row (placed, draw calls, build ms).
      slider3d({ label: 'rocks & trees', value: sky.decoBudget, min: 0, max: 20000, step: 500 }),
      toggle3d({ label: 'plants (off = rocks only)', value: sky.decoLife }),
      slider3d({ label: 'reach (m)', value: sky.decoRadius, min: 200, max: 3000, step: 100 }),
      // Tighter or looser: how strongly things gather, and how big a clump
      // is (0 = auto, sized from how far apart things are).
      slider3d({ label: 'clumping', value: sky.decoClump, min: 0, max: 1, step: 0.05 }),
      slider3d({ label: 'clump size (m)', value: sky.decoClumpSize, min: 0, max: 300, step: 10 }),
      toggle3d({ label: 'tree shadows', value: sky.decoShadows }),
      label3d({ text: 'Camera', icon: 'camera', collapsible: true }),
      slider3d({ label: 'eye height', value: sky.eye, min: 5, max: 1500, step: 10 }),
      toggle3d({ label: 'wireframe', value: demo.wireframe }),
      // Ambient occlusion is off here (the heavy scene). Perf Stats has the
      // switch, in every demo and in VR; a second one here drifted out of step
      // with it.
    ],
    sceneCreated(el, BABYLON) {
      // The terrain's biome shader exists only once it has built: apply the
      // preset's palette as soon as it does.
      const paletteWait = el.scene.onBeforeRenderObservable.add(() => {
        if (applyPalette()) el.scene.onBeforeRenderObservable.remove(paletteWait)
      })
      // A LOOK-AROUND CAMERA: you stand at eye height and turn in place.
      //
      // This was an ArcRotateCamera orbiting a target 300 m ahead, with the eye
      // height pinned by moving the target each frame. Orbit semantics leaked
      // straight through: up-arrow moves an orbit camera UP OVER its target,
      // which points the view DOWN — Tonio: "When I up-arrow the view nose-dives.
      // Down arrow tilts upward." — and left/right swung the eye round a 300 m
      // circle instead of turning it.
      //
      // So: a FreeCamera at the eye, arrows bound to ROTATION (up = look up;
      // Babylon's keysRotateUp lowers rotation.x, which pitches up) and its
      // move keys cleared so arrows never walk you. Dragging looks around.
      // Facing EAST (+X; +Z is north), just below the horizon.
      const cam = new BABYLON.FreeCamera('look', new BABYLON.Vector3(0, sky.eye.valueOf(), 0), el.scene)
      cam.setTarget(new BABYLON.Vector3(1, sky.eye.valueOf() - 0.08, 0))
      const keys = cam.inputs.attached.keyboard
      keys.keysUp = []
      keys.keysDown = []
      keys.keysLeft = []
      keys.keysRight = []
      keys.keysUpward = []
      keys.keysDownward = []
      keys.keysRotateLeft = [37]
      keys.keysRotateRight = [39]
      keys.keysRotateUp = [38]
      keys.keysRotateDown = [40]
      keys.rotationSpeed = 0.6
      cam.minZ = 0.1
      cam.maxZ = 12000
      cam.attachControl(el.parts.canvas, true)
      el.setActiveCamera(cam)
      // THE EYE IS PINNED: the slider means what it says.
      el.scene.registerBeforeRender(() => {
        cam.position.y = sky.eye.valueOf()
        // In a headset the view is the XR camera on its rig, which took its
        // height from this camera ONCE, on entry: the slider moved nothing.
        // The rig is the floor, so it sits a nominal eye height lower.
        const rig = el.xrFrames?.rig
        if (rig != null) rig.position.y = Math.max(0, sky.eye.valueOf() - 1.6)
      })
    },
  },
  // shadowMaxZ: the cascades must reach the ground from the eye, which starts
  // ~200 m up — at the 100 m default nothing in view casts or receives.
  b3dSun({ activeDistance: 80, shadowMaxZ: 1200 }),
  // THE PAIR: a 256 cube for the nebulae, a data cube the shader decodes into
  // points. Split because they are different KINDS of thing — one is
  // low-frequency and one is not — and the points stay points at any zoom.
  skybox,
  b3dLightning({ seed: 3, rate: sky.lightningRate }),
  // Sunlight breaking through gaps in the deck; strongest in the rain.
  b3dLightShafts({}),
  // Rain and snow come from the WEATHER: nothing falls until a storm is
  // overhead, and it eases in and out as the storm passes.
  b3dAmbient({ preset: 'rain', weather: 'rain', radius: 14 }),
  b3dAmbient({ preset: 'snow', weather: 'snow', radius: 14 }),
  // Bounce from the ground, or every vertical face (a rock's sides) is black.
  b3dLight({ intensity: 0.5, groundColor: '#57523f' }),
  b3dFog({ syncSkybox: true, start: 1000, end: 4000 }),
  terrain,
  // The layer case: a cloud DECK over the peaks, orographic so the towers
  // build over the actual mountains. Blob clouds (b3d-clouds) remain the
  // right tool for cloud you fly BETWEEN.
  // Rocks and trees by climate: pines in the cold, palms on warm shores, cacti
  // in hot dry country, boulders on the steep. See b3d-decorator.
  decorator,
  b3dCloudDeck({
    altitude: sky.altitude,
    coverage: sky.cloudCover,
    orographic: sky.cloudOrographic,
    wind: sky.wind,
    cirrus: sky.cirrus,
    evolve: sky.evolve,
    color: sky.deckColor,
    underColor: sky.deckUnderColor,
  }),
  // The sea follows BOTH dials: seaLevel (a fraction) times v-size, so the
  // ocean scales with the mountains instead of sitting at a fixed height
  // while the world reshapes around it. `follow` keeps it under the camera;
  // the ripples stay anchored in world space.
  water = b3dWater({ y: seaY(), waterSize: 8000, follow: true, twoSided: true, shore: 'on', shoreFine: true, waterColor: demo.waterColor, colorBlendFactor: demo.waterTint, fogColor: demo.waterFog }),
)

preview.append(scene)

// Flat or a sphere: both are generation attributes, so the terrain rebuilds
// itself on its next render.
function applySurface() {
  terrain.surfaceType = demo.sphere.valueOf() ? 'sphere' : 'cylinder'
  terrain.radius = demo.sphere.valueOf() ? demo.planetRadius.valueOf() * 1000 : 1000
}
demo.sphere.observe(applySurface)
demo.planetRadius.observe(applySurface)

// Regenerate the terrain when its dials move, and keep the sea at the same
// FRACTION of the terrain's height.
for (const key of ['seed', 'grossScale', 'detailScale', 'horizScale', 'grossAmplitude', 'detailAmplitude', 'wireframe', 'seaLevel', 'sea']) {
  demo[key].observe(() => {
    // The biome's sea level is a LIVE dial on the terrain — write it BEFORE
    // regenerate() so the adopted generation key is the one the rebuild
    // satisfies (the reverse order cost a second full pool re-cut per step,
    // found by the 0.8.2 review gate).
    terrain.biomeSeaLevel = biomeSea()
    terrain.biomeLapseRate = lapse()
    terrain.regenerate()
    water.y = seaY()
  })
}
tosi-b3d { width: 100%; height: 100%; }

Drag the terrain dials and the world reshapes under you. Cover the sky, drop the cloud base into the valleys, scrub to midnight — the encoded galaxy comes out with the moon riding it, the deck moonlit above.

The kitchen sink grows from here