Taos Engine ▦ Taos: API Documentation

5 — Atmosphere & Clouds

Live demo: run this tutorial in your browser — built from src/05_atmosphere_and_clouds.ts.

Tutorials 1–4 used a flat sky color. Here we replace it with a physically-based atmosphere (so the horizon curves and the sky reddens at sunset) and wrap volumetric clouds around the globe. This is the heaviest integration in the series; the production reference is samples/planet_explorer.ts and its sky helper samples/lib/geo_sky.ts.

The two pieces#

  1. Atmosphere — a sky dome + aerial-perspective haze, driven by AtmosphereFeature. For a globe you tell it the planet radius and center, so the horizon dips correctly as you climb.
  2. CloudsHQCloudFeature, a volumetric raymarched cloud deck that can be lit by the atmosphere and self-shadow.

Both ride on the same deferred preset and the same GeoScene from tutorial 1.

Step 1 — swap the flat sky for an atmosphere#

Two changes to the tutorial-1 setup. First, drop the flat sky color and let the preset use an atmosphere sky; second, turn on aerial perspective — it's opt-in (off by default), and an atmosphere sky is exactly what justifies it:

deferredPreset({
  sky: { kind: 'atmosphere' },        // physical scattering instead of a flat color
  ao: 'gtao',
  lighting: { enableAerial: true },   // atmospheric haze — opt-in, pairs with the atmosphere sky
})(engine);

{ kind: 'atmosphere' } registers AtmosphereFeature for you. For a flat world that's all you need. For a globe, you additionally want the sky dome to use the real planet radius so the limb and the dipping horizon are correct — that's where you reach past the preset and configure the feature directly.

Tune the aerial haze for your scale. enableAerial: true starts with planet-scale params (fogScale: 80, maxOpacity: 1) — great looking down at the whole globe, but at city/street scale they drown everything in haze. For a ground-level view, dial the distance haze down once the lighting pass exists:

const lighting = engine.getFeature<DeferredLightingFeature>(DeferredLightingFeature.name);
// ...on the first frame the pass is available:
lighting!.pass!.setAerialAtmosphere({
  betaR: [5.5e-6, 13.0e-6, 22.4e-6], betaM: 21.0e-6,
  rE: 6360000, rA: 6420000, hRayleigh: 8500, hMie: 1200,
  sunIntensity: 20, fogScale: 5, maxOpacity: 0.4,   // a faint city-scale veil
});

Driving the atmosphere on a planet#

The planet sample registers the atmosphere LUTs and feature explicitly so it can feed them the active body's radius and center each frame. The essential wiring:

import { AtmosphereFeature, AtmosphereLutsFeature } from 'taos/engine/index.js';
import { WGS84_A } from 'taos/geo/index.js';

// LUTs first (multiscatter / transmittance). Required before the cloud coupling.
engine.addFeature(new AtmosphereLutsFeature());

const atmosphere = new AtmosphereFeature({
  directional: () => sun,        // the sun that drives scattering
  useSkyViewLut: true,           // sky-view LUT acceleration
});
engine.addFeature(atmosphere);

// Tell the sky dome it's wrapping a planet (radius + a ~60 km shell).
atmosphere.pass!.setPlanet({ radius: WGS84_A, atmosphereHeight: 60000 });
atmosphere.pass!.setGroundSurface(true);   // paint the globe where terrain hasn't streamed

Each frame, as the camera moves and (with reanchoring) the world origin shifts, update the planet center in world space and the ground radius under the camera so the horizon stays put:

engine.beforeFrame(() => {
  const centerWorld = geo.frame.worldFromEcefPoint({ x: 0, y: 0, z: 0 });  // Earth center in world space
  atmosphere.pass!.setPlanetCenter(centerWorld);
  // setOverrides({ rE, rA }) refines the ground/atmosphere radii at the camera if needed.
});

geo_sky.ts packages all of this — planet vs. flat fallback, aerial-perspective fade with altitude, airless-body handling — into a reusable CurvedHorizonSky helper. Lift it wholesale rather than re-deriving the radius math; this tutorial shows the shape so the sample reads clearly.

Sun direction conventions. DirectionalLight uses the light-travel vector (pointing the way photons go). The atmosphere internally uses the toward-sun vector; the preset/feature wires the conversion. Just set the light normally.

Step 2 — add volumetric clouds#

Clouds need a set of 3D noise textures and an HQCloudFeature. Register the feature after the atmosphere LUTs so it can read the transmittance LUT for sunset tinting:

import { HQCloudFeature, DEFAULT_HQ_CLOUD_SETTINGS } from 'taos/engine/index.js';
import { createBakedCloudNoiseTextures } from './lib/cloud_noise_baked.js';

const noises = await createBakedCloudNoiseTextures(engine.ctx.device);

const clouds = new HQCloudFeature({
  noises,
  settings: {
    ...DEFAULT_HQ_CLOUD_SETTINGS,
    localClouds: true,        // a fly-through deck rather than an infinite sheet
    earthRadius: WGS84_A,     // curve the deck around the planet
    bottomAltitude: 1500,     // deck base, meters above the surface
    altitudeRange: 2500,      // deck thickness
    coverage: 0.88,           // how much sky is filled (0..1)
    density: 0.6,             // opacity of the filled parts
    coverageVariation: 0.2,   // weather-noise carving; the defaults (0.5/0.4) thin
    densityVariation: 0.25,   //   a high-coverage deck into sparse clumps
  },
  useAtmosphere: true,        // light clouds with the real sky ambient + aerial perspective
  useTransmittanceLut: true,  // tint the sun term through the atmosphere LUT (amber sunsets)
  accuratePhase: true,        // Mie phase → silver lining
  beerShadowMap: true,        // whole-column self-shadow
});
engine.addFeature(clouds);

Noise quality matters. HQ clouds want high-quality, single-tile 3D Perlin-Worley noise. The engine's built-in createCloudNoiseTextures is a small field that repeats ~4× per wrap — fine for godrays or a stylised deck, but on a planet-scale deck that recedes for kilometres the repetition reads as an obvious grid of identical puffs. The tutorial loads the standard baked 128³ shape + 32³ erosion textures instead (lib/cloud_noise_baked.ts, mirroring samples/hq_cloud). Supply your own baked Perlin-Worley volumes the same way for production.

The four boolean flags are the quality levers and are compile-time (set at construction). useAtmosphere is the single biggest one — it lights the shaded sides of clouds with the actual sky radiance instead of a constant.

Keeping the deck wrapped on the planet#

Like the atmosphere, the cloud deck must track the planet center and ground radius each frame, and fade out as you climb above it. The per-frame update from planet_explorer.ts:

engine.beforeFrame(() => {
  const centerWorld = geo.frame.worldFromEcefPoint({ x: 0, y: 0, z: 0 });
  clouds.settings.planetCenter = [centerWorld.x, centerWorld.y, centerWorld.z];
  clouds.settings.earthRadius = WGS84_A;

  // Fade the deck out once the camera flies well above it.
  const camWorld = cameraGO.localToWorld().data;
  const camAlt = /* camera height above the surface, meters */ 0;
  const deckTop = clouds.settings.bottomAltitude + clouds.settings.altitudeRange;
  const fade = 1 - Math.max(0, Math.min(1, (camAlt - deckTop - 3000) / 37000));
  clouds.settings.density = 0.35 * fade;
  clouds.enabled = fade > 0.01;
});

settings.* are live (read every frame); only the constructor booleans are baked. planet_explorer also dims the cloud ambient with the sun's elevation so the deck goes dark at night — see syncHqClouds there.

Feature order recap#

The registration order that makes the coupling work:

GeoFeature  (addFeatureBefore ShadowFeature)
ShadowFeature, GeometryFeature, ...   (stock, from the preset)
AtmosphereLutsFeature                 (before clouds)
AtmosphereFeature
HQCloudFeature                        (reads the LUT + sky)

geo.attach(engine, { cameraGO }) slots the GeoFeature into that first row (and installs the floating-origin reanchor); the atmosphere and cloud features are added explicitly, as shown earlier.

AtmosphereLutsFeature before HQCloudFeature is mandatory if you set useTransmittanceLut: true — without the LUT producer, the cloud feature binds a 1×1 white dummy and the sunset tint is a no-op.

Next#

  • Tutorial 6 — Clipping: cut away parts of the globe with geodetic planes and polygons.
  • The full, battle-tested version of everything here (day/night sun, moonlight, reflection probe, SDSM shadows, altitude fades) is planet_explorer.ts.