Live concept-proxy screenshot · not canonical evidence

Sky, Atmosphere, and Haze

Implement physically coherent sky, atmosphere, and haze in Three.js r185 native WebGPU/TSL using unit-consistent scattering LUTs, depth-aware aerial perspective, ellipsoid-aware geometry, explicit invalidation, and measured pipeline evidence.

$threejs-sky-atmosphere-and-haze 1 primary implementation 1 flagship 1 secondary surface native evidence pending Latest skill update commit 9077075 ↗ SKILL.md on GitHub ↗ raw (for agents) ↗

The approach, mathematically

The sky is single-scattered sunlight through an exponentially falling atmosphere. Rayleigh (molecules) and Mie (aerosols) contributions integrate along the view ray:

$$L(\lambda) = \int_0^{s_{atm}} T(0,s)\,\big(\beta_R(\lambda)\,p_R(\theta) + \beta_M\,p_M(\theta)\big)\,T_{sun}(s)\,ds$$

with Rayleigh scattering's $\lambda^{-4}$ law giving the blue sky and red sunsets:

$$\beta_R(\lambda) \propto \lambda^{-4}, \qquad p_R(\theta) = \tfrac{3}{16\pi}(1+\cos^2\theta)$$

The skill precomputes transmittance and scattering into LUTs by compute pass (a function of altitude and sun angle), then applies depth-aware aerial perspective to scene geometry: distant objects blend toward in-scattered airlight as $L' = L\,T(d) + L_{air}(d)$.

Preview and evidence ledger

Every image identifies what it proves. Page screenshots demonstrate the published presentation only; generated inputs demonstrate asset channels only; canonical acceptance still requires render-target readback and a schema-v2 bundle.

Canonical runtime evidence pending1 published image

The full skill

The complete SKILL.md as loaded by agents — verbatim, rendered.

Sky, Atmosphere, and Haze

Throughput is won by architecture before code. The taught path is native WebGPU on Three.js r185, WebGPURenderer from three/webgpu, TSL from three/tsl, node materials, one RenderPipeline, and compute-generated Hillaire/Bruneton-family scattering products sampled by bounded nodes per pixel. A WebGL backend is a hard blocker for this skill; there is no alternate renderer branch here.

Read references/atmosphere-system-contract.md before implementation. It defines the LUT contracts, capability gate, authored workload trials, measurement obligations, color/output ownership, depth contract, diagnostics, and the techniques replaced by the WebGPU/TSL architecture.

Shared Lighting Transport Boundary

For any material, cloud, water, vegetation, or weather consumer, first read the router's physics-domain and interaction contract. Publish one versioned LightingTransportSnapshot from this atmosphere model. The snapshot declares:

  • physics-context/origin epoch, sampleInstant: PhysicsInstant, descriptor validity whose temporal domain is a PhysicsTimeInterval, producer and parameter revisions, spatial support, update cadence, and interpolation/error policy;
  • sample-to-sun unit direction in physicsFrameId and disc angular radius;
  • whether the calibrated solar source is normal irradiance or finite-disc radiance, including its per-channel SI unit and solid-angle conversion; an authored dimensionless relative scale is an internal nonphysical model input only and leaves snapshot radiance/irradiance channels absent;
  • each returned SampledChannel's quantity kind, SI unit, filter/error, and, where applicable, spectral/angular basis and conversion revision; a provider-level transform registry never overrides channel metadata;
  • atmospheric direct-sun transmittance/irradiance, sky radiance/irradiance, and segment transmittance/inscattering providers with their domain and error;
  • typed provider request signatures with position, direction/normal or segment endpoints, footprint/solid-angle support, frame, and PhysicsInstant;
  • applied versioned attenuationFactorIds for every output and explicit skyIncludesDirectSolarDisc state for sky irradiance. A boolean "attenuated" flag is insufficient.

Consumers must choose either a pre-attenuated direct-light value or an unattenuated source plus the atmosphere transmittance provider. They may not use both. Cloud optical-depth shadows, opaque-geometry visibility, and water extinction are separate factors and each is applied exactly once. Do not bake cloud attenuation into this atmosphere snapshot or let a material reapply aerial perspective already owned by the image path.

EnvironmentForcingSnapshot is a separate thermodynamic/mechanical interface sampled at sampleInstant: PhysicsInstant; use PhysicsTimeInterval only for an actual validity or graph-stage interval. If temperature, humidity, pressure, aerosol loading, or wind drive atmosphere parameters, record that forcing revision and the transfer model; do not treat RGB extinction coefficients as a wind or humidity provider.

After every physical owner commits, publish one view-independent PhysicsPresentationCandidate with requestedPresentationInstant: PhysicsInstant, containing the atmosphere's base-provider presentedStatePairs, resourceLeases, and eventSequenceRanges. Each pair gives previousPresented.provenance and currentPresented.provenance their own full PresentationSampleProvenance; never share one bracket, clock map, or interpolation alpha across the two states. Each arm also carries its own presentedInstant: PhysicsInstant. Static or low-rate products still declare explicit hold or not-interpolated provenance.

For each target/view, the camera owner publishes CameraViewPublication with previousRenderSampleInstant: PhysicsInstant, currentRenderSampleInstant: PhysicsInstant, and globalToRenderPrevious/globalToRenderCurrent plus view/projection matrices, jitter, viewport, and depth state. Sky, aerial, shadow, cache, visibility, and reset preparation then publishes a ViewPreparationPublication against that camera record and the immutable candidate, owning visibilityPublicationRefs, accelerationPublicationRefs, shadowViewPublicationRefs, cachePublicationRefs, reactiveEpochs, reactivePublications, resetDependencies, full resourceLeases for newly created camera-dependent generations, and resourceLeaseRefs. Beyond identity, target/view scope, seal metadata, and scoped eventSequenceRanges, the sealed PhysicsPresentationSnapshot carries candidateId, cameraPublicationId, viewPreparationId, presentedStatePairRefs, and resourceLeaseRefs; it never copies PresentedStatePair records or globalToRender transforms. Leases remain live through every consumer.

Any tier transition that changes physics-facing state or providers, update cadence, represented support or spectral/spatial/temporal filter, error bounds, inventories, stable IDs/RNG streams, or event and exact-once application-ledger cursors requires the contract's shared QualityTransition. Commit its conservative map atomically at a safe graph-step boundary and retain old resources through the completion join. A strictly render-only LUT, resolution, sampling, or composition change may remain local only when all physics-provider semantics, PhysicsGraph ownership, committed physical versions, IDs, cursors, and physical error bounds remain unchanged.

Canonical implementation contract: examples/webgpu-lut-atmosphere/. Run node examples/webgpu-lut-atmosphere/validation.js after edits.

Legacy WebGL implementation (deprecated, do not extend): examples/lut-aerial-perspective/atmosphere-effect.js.

Required Architecture

Lead with precomputed scattering, not per-pixel full scattering integration:

atmosphere parameters + planet/ellipsoid transform
  -> compute transmittance LUT
  -> compute multiscatter / irradiance LUTs
  -> compute sky-view LUT for the active camera/sun frame
  -> compute aerial inscattering plus RGB optical-depth froxels, or prove an
     endpoint-transmittance reconstruction
  -> scene pass() with shared color/depth ownership
  -> TSL sky and aerial-perspective nodes sample LUTs
  -> one HDR RenderPipeline output path

This architecture amortizes expensive optical-depth integration into compute dispatches and replaces nested view/light marches in every visible pixel with texture lookups, segment transmittance, and depth-aware composition.

Build Order

  1. Run $threejs-choose-skills preflight when atmosphere touches terrain, clouds, shadows, exposure, or post ownership.
  2. Define one atmosphere model shared by sky, aerial perspective, sun/moon discs, material irradiance, and lighting. Declare the integration length unit and coefficient unit together: beta [length^-1] * ds [length] must be dimensionless. Never combine meter radii with per-kilometer coefficients.
  3. Initialize WebGPURenderer, call await renderer.init(), and require renderer.backend.isWebGPUBackend === true before allocating resources.
  4. Generate 2D LUTs with TSL Fn().compute(count) dispatches through renderer.compute() or renderer.computeAsync(). Write 2D products with StorageTexture plus textureStore(); write volume products with r185 Storage3DTexture plus storageTexture3D(). Treat all products as NoColorSpace data. Set r185 2D StorageTexture.generateMipmaps = false and mipmapsAutoUpdate = false unless a measured path consumes authored mips. Set Storage3DTexture.generateMipmaps = false as well when only its base level is written; it has no mipmapsAutoUpdate property. After initialization use renderer.compute() for normal submission. In r185 computeAsync() only initializes on demand before enqueueing; it is not a GPU-completion fence. Later GPU submissions are queue-ordered, but CPU readback, timestamp resolution, resource reuse, and lifetime decisions need an explicit completion/readback mechanism.
  5. Use RenderPipeline, pass(), and mrt() when the image chain needs shared signals. In r185, PassNode.getViewZNode() is the standard/reversed perspective helper and getLinearDepthNode() returns normalized depth, not metric ray distance. Orthographic and logarithmic depth require their explicit TSL conversions and fixed projection tests.
  6. Compose sky radiance and surface-segment transmittance/inscattering with TSL nodes. Keep output scene-linear HDR until the single tone-map and output color transform owner.
  7. Validate inverse LUT mappings at texel centers, quadrature convergence, invalidation hashes, units, payload byte counts, energy bounds, depth mode, planet intersections, camera altitude, and fixed sun/camera cases before tuning color.

Use the canonical validation module for Phase 1 structural/equation gates:

node examples/webgpu-lut-atmosphere/validation.js

It verifies the LUT manifest, RGBA16F upload policy, unit conversion fixtures, manifest/live-model equality, CPU segment/intersection math, transmittance mapping, HG normalization through the accepted |g| <= 0.99 interval, and projection-specific depth reconstruction including nearest-covered-sample MSAA resolve. It also builds the TSL graphs. It does not initialize a GPU, submit compute, render a scene, or measure full-frame performance; those remain browser-harness obligations.

Capability Gate

Any path using compute/storage/MRT must gate after renderer initialization:

await renderer.init();

if ( renderer.backend.isWebGPUBackend !== true ) {
  throw new Error(
    'threejs-sky-atmosphere-and-haze requires a native WebGPU backend.'
  );
}

// Full tier: compute-generated LUTs, storage textures, MRT/depth sharing.

Quality tiers:

Numeric labels used here and in the reference: [Derived] follows from an equation or representation; [Gated] is an acceptance ceiling/floor; [Measured] must come from a named device capture; [Authored] is a starting quality or appearance choice, never a hardware fact.

Tier Requirements Authored starting dimensions
Full-detail trial Native WebGPU; two RGB aerial payloads or proven endpoint reconstruction [Authored] 256x64 transmittance, 64x32 multiscatter/irradiance, 192x108 sky-view, 192x108x32-48 aerial
Budgeted trial Native WebGPU; cached static LUTs and staggered view products [Authored] 192x48 transmittance, 128x64 sky-view, 160x90x24-32 aerial
Minimum-resident trial Native WebGPU; one scene pass, compact view products, no hidden full refresh [Authored] 128x32 transmittance, 96x48 sky-view, 96x54x16-24 aerial

Required Outputs

  • sky radiance and sun/moon disc transmittance/color;
  • camera-to-surface segment transmittance;
  • camera-to-surface segment inscattering;
  • optional sky irradiance for MeshStandardNodeMaterial or MeshPhysicalNodeMaterial lighting integration;
  • explicit conversion between render units and atmosphere meters/kilometers;
  • diagnostics for LUT coordinates, slices, intersections, depth class, and shell/post blend;
  • a dependency hash and last-update reason for every LUT;
  • reference-integrator errors for optical depth, radiance, horizon, and energy;
  • a declared aerial payload: RGB inscattering plus RGB optical depth, or a validated method that reconstructs chromatic segment transmittance. Packing RGB scattering and only scalar opacity into one RGBA volume is not silently equivalent.

Workload And Performance Evidence

Do not inherit a device-class millisecond or memory table. Derive the workload:

payloadBytes = sum(width * height * depth * bytesPerTexel * residentCopies)
invocations = width * height * depth
workgroupInvocations = wgX * wgY * wgZ
r185FlattenedGroups = ceil(invocations / workgroupInvocations)
integratorSamples = updatedTexels * samplesPerTexel

This formula matches a numeric r185 Fn().compute(count, [wgX,wgY,wgZ]): the backend dispatches [r185FlattenedGroups,1,1], wrapping into Y only when the adapter's maxComputeWorkgroupsPerDimension is exceeded. Count unique kernels, not output textures; one aerial kernel may write both RGB inscattering and RGB optical depth.

The product supplies its own full-frame GPU p95, CPU-submit p95, presented-frame p95, and peak-live-byte budgets. Acceptance uses contemporaneous full-frame captures on the named adapter/browser/viewport/DPR/pass graph. Quality-table dimensions are [Authored] trials, not memory gates or timing evidence.

Do not spend full-resolution nested optical-depth marches per pixel. Every enabled tier must state texture dimensions, storage formats, dispatch counts, render resolution, update cadence, draw calls, peak live bytes, full-frame p50/p95, and resize/disposal behavior.

Color And Output

  • LUTs, density masks, depth, normals, weather inputs, and optical-depth data are NoColorSpace linear data. Albedo/color art textures use SRGBColorSpace.
  • Atmosphere radiance enters the image chain as scene-linear HDR. Use HalfFloatType working buffers until tone mapping.
  • Exactly one system owns tone mapping and one system owns output color conversion. If renderOutput() owns final presentation, set RenderPipeline.outputColorTransform = false; otherwise let the host RenderPipeline.outputColorTransform own conversion.
  • Exposure can scale physically authored radiance, but it must not hide wrong units, coefficients, or transmittance.

Failure Conditions

  • sky and terrain haze use different sun directions, coefficients, radii, or unit conversions;
  • per-pixel nested view/light marching is used as the primary production path;
  • aerial perspective is a uniform fog color or transparent sphere;
  • camera altitude is measured in a local flat frame during orbital/geospatial motion;
  • depth reconstruction ignores standard, reversed, logarithmic, orthographic, MSAA-resolved, or sky-pixel cases used by the host renderer;
  • reversed depth is manually flipped before an r185 conversion that already handles renderer.reversedDepthBuffer, or getLinearDepthNode() is treated as a metric distance;
  • direct sun, sky irradiance, segment transmittance, and inscattering are collapsed into one color;
  • a single scalar alpha is presented as chromatic RGB transmittance without an error gate;
  • an imported LUT is sampled without proving that radii, coefficients, density layers, phase convention, and solar/spectral basis equal the live model;
  • HG g approaches its singular |g|=1 limit without a declared accepted interval, stable denominator, and normalization/extreme-direction tests;
  • camera rotation, temporal projection jitter, or a floating-origin shift invalidates body-frame LUTs that are unchanged;
  • tone mapping or output color conversion happens more than once;
  • atmosphere fades abruptly at shell entry or switches ownership without a validated transition.

Routing Boundary

This skill owns molecular/aerosol sky scattering, sun/moon transmittance, material sky irradiance, and depth-based surface-segment aerial perspective. Use $threejs-image-pipeline for whole-frame HDR/depth/MRT ownership, $threejs-exposure-color-grading for metering and tone mapping, $threejs-volumetric-clouds for weather-shaped cloud density and cloud shadows, $threejs-procedural-planets for planet terrain/material fields, and $threejs-visual-validation for fixed-view diagnostics and GPU timing evidence. This skill is the unique producer of the atmosphere-derived LightingTransportSnapshot for a routed scene.

Secondary provider surfaces

Preserved concept proxies and generated-asset previews. They are excluded from primary completion counts and link to the canonical lab through the schema-v2 registry.