threejs-volumetric-clouds skill (Threejs-Awesome-Graphics-Agent-Skills)

From Public Agent Wiki

What it does. Implement volumetric cloud systems in Three.js. Use for weather-driven density, bounded raymarching, shape/detail erosion, vertical profiles, lighting cones, silver lining, temporal reconstruction, cloud shadows, multiple layers, and scalable quality modes. Part of scottstts/Threejs-Awesome-Graphics-Agent-Skills (scottstts/Threejs-Awesome-Graphics-Agent-Skills).

Upstream scottstts/Threejs-Awesome-Graphics-Agent-Skills
Skill file skills/threejs-volumetric-clouds/SKILL.md
License MIT
Author Scott Sun (scottstts)
Fetched 2026-09-10

Install

  • npx skills add scottstts/Threejs-Awesome-Graphics-Agent-Skills --skill threejs-volumetric-clouds, or copy the skill folder into ~/.claude/skills/threejs-volumetric-clouds/.
  • Raw file: curl -sL https://raw.githubusercontent.com/scottstts/Threejs-Awesome-Graphics-Agent-Skills/HEAD/skills/threejs-volumetric-clouds/SKILL.md

SKILL.md (verbatim)

name: threejs-volumetric-clouds
description: Implement volumetric cloud systems in Three.js. Use for weather-driven density, bounded raymarching, shape/detail erosion, vertical profiles, lighting cones, silver lining, temporal reconstruction, cloud shadows, multiple layers, and scalable quality modes.

Volumetric Clouds

Cloud quality comes from density organization, lighting, and temporal stability—not from increasing march steps over unstructured noise.

This skill contains exemplary examples and assets beyond descriptive guidance, they're worth studying, referencing, or even copying. Use them sufficiently when relevant and do NOT blindly skip them.

System order

  1. Define the cloud volume and layer bounds.
  2. Generate or source weather, base-shape, detail, and turbulence fields.
  3. Build a density function with vertical and weather profiles.
  4. Raymarch only the bounded occupied segment.
  5. Integrate transmittance and lighting front-to-back.
  6. Reconstruct low-resolution output temporally.
  7. Project a separate low-cost cloud-shadow solution.

Read references/weather-volume-and-reconstruction.md before implementing or auditing the cloud system.

Read the weather volume cloud entry and its source/ modules for weather-layer ownership, spherical shell bounds, authored shape/detail sampling, cloud shadow maps, temporal upscale, atmospheric composition, and package-owned diagnostics.

Required controls

  • coverage, cloud type, precipitation, and anvil bias;
  • base/top altitude and vertical density profile;
  • shape/detail scales and erosion;
  • wind for each field;
  • primary step count, light step count, and empty-space policy;
  • history weight and disocclusion threshold;
  • cloud-shadow extent, resolution, and update rate.

Failure conditions

  • density is only fbm(position);
  • the raymarch traverses the full camera range;
  • detail noise adds density instead of eroding shaped masses;
  • temporal history is accepted across disocclusion;
  • shadows use the full beauty raymarch;
  • every cloud layer shares the same wind and density profile.

Routing boundary

Use $threejs-atmosphere-aerial-perspective for molecular/aerosol scattering without weather density. Use $threejs-procedural-vfx for emissive aurora curtain slabs or bounded interactive voxel fire and smoke with velocity, pressure, emitter, and collision fields. This skill owns weather-shaped cloud volumes, reconstruction, cloud lighting, and cloud shadows.

Other files in this skill

assets/weather-volume-clouds/THIRD_PARTY_LICENSES.md (verbatim)

Third-party licenses

The atmosphere lookup tables and cloud textures in this directory are third-party assets used under the MIT License reproduced below.

MIT License

Copyright (c) 2025 Su

Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions:

The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software.

THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.

references/weather-volume-and-reconstruction.md (verbatim)

Weather-shaped cloud volume and reconstruction

Use this reference for planetary or large-world volumetric clouds built from packed weather layers, generated shape and detail fields, bounded integration, temporal reconstruction, and low-cost cloud shadows.

Contents

  1. Preserve the four-layer vector model
  2. Understand the packed interval subtlety
  3. Generate field assets once
  4. Weather coverage response
  5. Base shape and turbulence
  6. Detail changes topology by height
  7. Planetary ray interval
  8. Primary march policy
  9. Lighting contract
  10. Quarter-resolution temporal upscale
  11. Cloud shadow representation
  12. Quality tiers must remove expensive mechanisms intentionally
  13. Required diagnostics
  14. Failure diagnosis

1. Preserve the four-layer vector model

Evaluate four layers in parallel as vec4 channels. Do not collapse them into one scalar weather field before applying per-layer altitude, profile, shape, and detail controls.

Default active layers:

Channel Altitude Height Density Shape Detail Coverage width Shadow
R low 750 m 650 m 0.2 1.0 1.0 0.6 yes
G middle 1000 m 1200 m 0.2 1.0 1.0 0.6 yes
B high 7500 m 500 m 0.003 0.4 0.0 0.5 no
A spare disabled disabled default default default default no

Each layer also owns:

type Layer = {
  weatherChannel: "r" | "g" | "b" | "a"
  weatherExponent: number
  shapeAlteringBias: number
  densityProfile: {
    exponentialTerm: number
    exponent: number
    linearTerm: number
    constantTerm: number
  }
}

Default density profile:

profile(h) =
  expTerm * exp(exponent * h)
  + linearTerm * h
  + constantTerm

default = 0.75 * h + 0.25

The profile is not a generic bottom/top smoothstep. It is a compact artist-authored function that can produce rising, falling, or exponential density by layer.

2. Understand the packed interval subtlety

The layer system sorts all lower/upper altitude endpoints and merges occupied ranges. It then packs up to three intervals where the active-layer balance is zero.

Those packed intervals are empty gaps, despite names such as minIntervalHeights and insideLayerIntervals.

Beauty and shadow marches use:

if (height is inside any packed gap) {
  skip density work;
}

For the default layers, the low and middle ranges merge into one occupied band from 750–2200 m, followed by an empty gap before the 7500–8000 m high layer.

If adapting the system:

  1. merge occupied layer ranges on CPU;
  2. pack the complementary gaps;
  3. verify the debug view labels them as skipped intervals;
  4. do not accidentally skip the occupied bands.

3. Generate field assets once

Render procedural textures into repeatable targets, then sample them during the expensive march.

Local weather RGBA

The procedural weather texture assigns:

R: low-cloud Worley FBM
G: middle-cloud Worley FBM
B: high-cloud anisotropic Perlin
A: auxiliary Perlin variation

Low and middle fields are intentionally separated:

middle = smoothstep(1.0, 1.4, WorleyFBM(point + 0.5))
low = saturate(
  smoothstep(0.8, 1.4, WorleyFBM(point))
  - middle
)

The generation pass later forces alpha to 1, so the default spare A layer is not a second generated weather population. Preserve that behavior when using the complete packed-layer contract.

Base shape volume

The 3D base texture combines Perlin-Worley and Worley FBM:

perlinWorley =
  remap(perlin, 0, 1, worleyFBM, 1)

baseShape =
  remap(perlinWorley, worleyFBM - 1, 1)

The octave weights are dominated by low frequencies:

0.625, 0.25, 0.125

Detail volume

The detail volume is Worley-only and builds three progressively finer FBM bands from frequencies 2, 4, 8, 16, again weighted toward low frequencies.

Turbulence

The 2D turbulence texture stores a normalized curl field derived from three offset Perlin channels. It warps shape coordinates; it is not multiplied into final density as arbitrary noise.

Generate these textures once or only when their recipes change. A 3D render-target path requires one draw per Z layer, so regenerating each frame is unacceptable.

4. Weather coverage response

For each sample:

heightFraction =
  remapClamped(height, layerMin, layerMax)

Round cloud growth toward the upper portion:

biased = heightFraction ^ shapeAlteringBias
x = clamp(2 * biased - 1, -1, 1)
heightScale = 1 - x²

Sample the four selected weather channels, apply each layer’s exponent, then modulate by global coverage:

factor = 1 - coverage * heightScale
density =
  remapClamped(
    mix(localWeather, 1, coverageFilterWidth),
    factor,
    factor + coverageFilterWidth
  )

Global coverage therefore shifts/remaps local weather. It is not a final density multiplier.

Debug separately:

raw local-weather channels
weather after exponent
heightScale
coverage factor
coverage-remapped density

5. Base shape and turbulence

Advect fields through offsets rather than regenerating them:

localWeatherOffset += localWeatherVelocity * dt
shapeOffset += shapeVelocity * dt
detailOffset += detailVelocity * dt
turbulenceOffset += turbulenceVelocity * dt

The density path adds a radial “evolution” offset related to weather speed, then optional turbulence:

surfaceNormal = normalize(position)
evolution = -surfaceNormal * length(weatherOffset) * 20000

turbulence =
  displacement
  * (curlTexture * 2 - 1)
  * lowHeightMask

The turbulence mask fades out by roughly the lower 30% of each layer, so it distorts bases and growth without scrambling the entire cloud.

Base shape:

shapePosition =
  (position + evolution + turbulence)
  * shapeRepeat
  + shapeOffset

density =
  remapClamped(
    weatherDensity,
    (1 - shapeNoise) * shapeAmount,
    1
  )

Shape amount is per layer. High cirrus uses less base-shape influence.

6. Detail changes topology by height

The detail modifier is not uniform erosion.

top modifier = detail^6
bottom modifier = 1 - detail

modifier =
  mix(
    top modifier,
    bottom modifier,
    remapClamped(heightFraction, 0.2, 0.4)
  )

This makes upper cloud detail fluffy and lower detail whippy/eroded. Then:

modifier *= shapeDetailAmount
density =
  remapClamped(
    density * 2,
    modifier * 0.5,
    1
  )

Detail is skipped at coarse mip levels using a jittered threshold. Do not spend high-frequency texture reads when the sample footprint cannot resolve them.

Finally:

densityVector =
  saturate(
    densityVector
    * densityScales
    * profile(heightFraction)
  )

totalDensity = sum(densityVector)
layerWeight = densityVector / totalDensity
scattering = totalDensity * scatteringCoefficient
extinction =
  totalDensity * absorptionCoefficient
  + scattering

Guard zero total density before using layer weights in an independent implementation.

7. Planetary ray interval

Intersect view rays with concentric spheres at:

planet radius
minimum cloud altitude
maximum cloud altitude
shadow top altitude

Select near/far based on camera state:

below clouds
inside total cloud layer
above clouds
ray intersects ground

Then clamp the far distance against opaque scene depth. The beauty march never runs beyond the nearest opaque surface.

Return explicit diagnostic flags:

ground intersection
scene occlusion
camera region
near distance
far distance
selected sphere intersections

8. Primary march policy

High/default values:

max iterations: 500
minimum step: 50 m
maximum step: 1000 m
maximum ray distance: 200 km
perspective step scale: 1.01
minimum density: 1e-5
minimum extinction: 1e-5
minimum transmittance: 1e-2

Initial step size grows with ray entry distance:

step =
  minStep
  + (perspectiveScale - 1) * rayNear

Jitter the first step with spatiotemporal blue noise. Double the jitter distance when needed to suppress spatial aliasing.

At each sample:

  1. skip packed empty altitude gaps;
  2. sample rough weather;
  3. if all layer densities are below threshold, take a longer mip-aware step;
  4. otherwise sample base shape, optional turbulence, and detail;
  5. if extinction is significant, evaluate lighting and integrate;
  6. terminate at the transmittance threshold.

Long empty-space steps can band near a dense crossing. Treat this as a known defect, not a solved feature. A robust adaptation can binary-search the first dense crossing.

9. Lighting contract

Per occupied sample, evaluate:

sun irradiance
sky irradiance
short optical-depth march toward sun
Beer shadow-map optical depth beyond that short march
multi-scattering approximation
optional ground bounce
sky gradient contribution
powder attenuation

The phase function defaults to two Henyey-Greenstein lobes. An optional fitted large-particle phase path exists, but it requires adequate multiple scattering to remain plausible.

Multi-scattering uses octave accumulation:

for each octave:
  contribution +=
    attenuationA
    * exp(-opticalDepth * attenuationB)
    * phase(cosTheta, attenuationC)
  attenuation *= 0.5

Default high quality uses eight octaves.

Energy-conserving integration:

stepT = exp(-extinction * stepLength)
stepScatter =
  (radiance - radiance * stepT)
  / max(extinction, epsilon)

accumulatedRadiance += accumulatedT * stepScatter
accumulatedT *= stepT

Representative depth is a transmittance-weighted sample distance. It is used for aerial perspective and temporal velocity, not merely visualized.

10. Quarter-resolution temporal upscale

The temporal-upscale path renders the current clouds at one quarter linear resolution:

lowWidth = ceil(fullWidth / 4)
lowHeight = ceil(fullHeight / 4)

A 4×4 Bayer pattern chooses one current full-resolution pixel per low-resolution texel over 16 frames. Projection jitter follows the same offset.

Current targets store:

RGBA cloud radiance/transmittance
RGB representative depth + velocity
optional shadow length

Resolve:

  1. use the newly rendered current texel when its Bayer index matches the frame;
  2. otherwise choose the closest-depth sample in a 3×3 neighborhood;
  3. reproject with velocity;
  4. reject history outside the viewport;
  5. variance-clip history against current neighbors;
  6. write the resolved result and swap history buffers.

For full-resolution TAA, blend clipped history toward current with default temporalAlpha = 0.1.

Reset history on:

camera cut
resolution or render-scale change
weather/shape discontinuity
layer topology change
projection mode change

11. Cloud shadow representation

The shadow system is not a grayscale beauty march. Each cascade stores:

R front depth
G mean extinction
B maximum accumulated optical depth
A optical-depth tail estimate after early termination

Beauty lighting reconstructs optical depth beyond a local short sun march from this compact representation.

Shadow marching uses structured volume sampling:

  1. choose one of three icosahedral structure normals from ray direction and jitter;
  2. intersect regularly spaced planes perpendicular to that normal;
  3. march samples on those planes.

This intentionally trades some spatial aliasing for strong temporal stability, which matters for low-resolution cascaded shadow maps.

Default shadow budget:

3 cascades
512 × 512 maps
50 iterations
100–1000 m step size
minimum transmittance 1e-4

Low tier:

2 cascades
256 × 256 maps
25 iterations
minimum transmittance 1e-2

12. Quality tiers must remove expensive mechanisms intentionally

The low tier disables:

light shafts
shape detail
turbulence
ground bounce
accurate sun/sky lookup

It keeps:

weather-shaped density
base 3D shape
one short sun march
temporal reconstruction
cloud shadows at reduced quality

This is a legitimate fallback because silhouette, weather control, and directional self-shadowing survive.

13. Required diagnostics

Expose:

weather RGBA
per-layer height fractions
packed empty intervals
coverage-remapped density
base shape
detail modifier
turbulence displacement
final per-layer density vector
total scattering/extinction
ray near/far and scene clamp
primary/shape/detail sample counts
sun optical depth
Beer shadow-map channels
transmittance
representative depth
velocity
history UV
variance bounds
history rejection
shadow cascade index
shadow structured-sampling planes

14. Failure diagnosis

clouds disappear between low and high layers:
  occupied ranges were mistaken for packed empty gaps

all cloud types share one silhouette:
  layer vectors were summed before profile/shape controls

porous smoke:
  detail was added uniformly instead of height-dependent remapping

boiling motion:
  field offsets use unrelated directions/speeds or textures regenerate

bright flat interior:
  short sun optical depth or shadow map is missing

dark featureless cloud:
  multi-scattering, sky light, or powder balance is absent

edge trails:
  representative depth/velocity is wrong or history lacks variance clipping

flickering cloud shadows:
  beauty jitter was reused instead of temporally stable structured sampling

cost scales with view distance:
  shell interval, scene depth clamp, or empty-gap skipping is broken

Back to scottstts/Threejs-Awesome-Graphics-Agent-Skills or Agent skills.