| name | procedural-clouds |
| description | Generate beautiful procedural clouds in Three.js using WebGPU raymarching with WebGL2 billboard/mesh fallbacks. Covers all 10 major cloud genera (cumulus, stratus, cirrus, cumulonimbus, stratocumulus, altocumulus, altostratus, nimbostratus, cirrostratus, cirrocumulus) with physically-inspired lighting including silver linings, god rays, sunset coloring, and Mie/Rayleigh scattering approximation. Provides volumetric raymarching, billboard impostor, and mesh-cluster rendering paths with animated drift, morphing, and dynamic formation/dissipation. Use when building skies, cloudscapes, weather systems, flight scenes, atmospheric backgrounds, or any scene requiring clouds. Triggers: "procedural clouds", "cloud rendering", "volumetric clouds", "skybox clouds", "cloudscape", "cumulus", "cirrus", "storm clouds", "cloud shader", "cloud billboard", "raymarched clouds", "cloud lighting", "god rays", "sky rendering".
|
Procedural Clouds
Generate visually stunning procedural clouds in Three.js with artistic emphasis —
volumetric raymarching on WebGPU, billboard/mesh fallbacks on WebGL2.
Architecture Overview
┌──────────────────────────────────────────────────────┐
│ Cloud Pipeline │
│ │
│ Rendering Paths (select by capability + budget): │
│ │
│ ┌─ VOLUMETRIC (WebGPU) ─────────────────────────┐ │
│ │ Fullscreen quad → raymarching fragment shader │ │
│ │ Noise: 3D worley/perlin compute textures │ │
│ │ Best quality, most expensive │ │
│ └───────────────────────────────────────────────┘ │
│ │
│ ┌─ MESH CLUSTER (WebGL2/WebGPU) ────────────────┐ │
│ │ Instanced soft-particle spheres │ │
│ │ Per-instance density, color, fade │ │
│ │ Good quality, moderate cost │ │
│ └───────────────────────────────────────────────┘ │
│ │
│ ┌─ BILLBOARD (WebGL2, mobile) ──────────────────┐ │
│ │ Camera-facing quads with noise texture │ │
│ │ Cheapest, suitable for backgrounds │ │
│ └───────────────────────────────────────────────┘ │
│ │
│ Shared Systems: │
│ Lighting ─ Drift ─ Time-of-Day ─ Formation │
└──────────────────────────────────────────────────────┘
Cloud Classification Quick Reference
| Genus | Altitude | Shape | Key Visual |
|---|
| Cumulus | Low (2km) | Puffy mounds | Flat base, cauliflower tops |
| Stratus | Low (2km) | Flat sheet | Uniform grey blanket |
| Stratocumulus | Low (2km) | Lumpy rolls | Patchy blanket with gaps |
| Cumulonimbus | Low→High | Towering anvil | Massive vertical, dark base |
| Altocumulus | Mid (2-6km) | Rippled patches | "Mackerel sky" pattern |
| Altostratus | Mid (2-6km) | Thin veil | Sun visible as bright spot |
| Nimbostratus | Mid (2-6km) | Thick dark sheet | Continuous rain cloud |
| Cirrus | High (6-12km) | Wispy streaks | Ice crystal hooks and mares' tails |
| Cirrostratus | High (6-12km) | Thin milky haze | Halo around sun |
| Cirrocumulus | High (6-12km) | Tiny ripples | Delicate fish-scale pattern |
Full profiles with shader parameters in references/cloud-types.md.
Renderer Setup
import * as THREE from 'three';
async function createRenderer(canvas) {
let renderer, gpuAvailable = false;
try {
const WebGPU = (await import('three/addons/capabilities/WebGPU.js')).default;
if (WebGPU.isAvailable()) {
const { default: WebGPURenderer } = await import(
'three/addons/renderers/webgpu/WebGPURenderer.js'
);
renderer = new WebGPURenderer({ canvas, antialias: true });
await renderer.init();
gpuAvailable = true;
}
} catch (e) { }
if (!renderer) {
renderer = new THREE.WebGLRenderer({ canvas, antialias: true });
renderer.toneMapping = THREE.ACESFilmicToneMapping;
renderer.toneMappingExposure = 1.2;
}
renderer.setSize(innerWidth, innerHeight);
renderer.(.(devicePixelRatio, ));
{ renderer, gpuAvailable };
}
3D Noise Foundation
All cloud rendering depends on layered 3D noise. These functions are shared across
all three rendering paths.
function hash3(x, y, z) {
let h = x * 127.1 + y * 311.7 + z * 74.7;
return (Math.sin(h) * 43758.5453) % 1;
}
function noise3D(x, y, z) {
const ix = Math.floor(x), iy = Math.floor(y), iz = Math.floor(z);
const fx = x - ix, fy = y - iy, fz = z - iz;
const ux = fx * fx * (3 - 2 * fx);
const uy = fy * fy * (3 - 2 * fy);
const uz = fz * fz * (3 - 2 * fz);
const h = (a, b, c) => hash3(ix + a, iy + b, iz + c);
return lerp(uz,
lerp(uy, lerp(ux, h(0,0,0), h(1,0,0)), (ux, (,,), (,,))),
(uy, (ux, (,,), (,,)), (ux, (,,), (,,)))
);
}
() { a + t * (b - a); }
() {
sum = , amp = , freq = , max = ;
( i = ; i < octaves; i++) {
sum += (x * freq, y * freq, z * freq) * amp;
max += amp; amp *= gain; freq *= lac;
}
sum / max;
}
Path 1: Volumetric Raymarching (WebGPU)
The highest-quality path renders clouds by marching rays through a density field defined
by 3D noise. Implemented as a fullscreen post-process pass.
Cloud Density Field
The density function defines cloud shape, coverage, and type:
float cloudDensity(vec3 p, float time) {
float altFade = smoothstep(cloudBase, cloudBase + 200.0, p.y)
* smoothstep(cloudTop, cloudTop - 200.0, p.y);
float shape = fbm3D(p * 0.0003 + wind * time, 3);
shape = remap(shape, coverageThreshold, 1.0, 0.0, 1.0);
float detail = fbm3D(p * 0.003 + wind * time * 2.0, 5);
float density = shape - detail * detailStrength;
return max(density * altFade, 0.0);
}
Raymarching Loop
vec4 raymarchClouds(vec3 ro, vec3 rd) {
float t = intersectCloudLayer(ro, rd);
vec4 result = vec4(0.0);
for (int i = 0; i < MAX_STEPS; i++) {
if (result.a > 0.99 || t > maxDist) break;
vec3 p = ro + rd * t;
float density = cloudDensity(p, time);
if (density > 0.001) {
float lightEnergy = lightMarch(p);
float phase = henyeyGreenstein(dot(rd, sunDir), 0.3)
+ henyeyGreenstein(dot(rd, sunDir), 0.8) * 0.5;
vec3 cloudColor = sunColor * lightEnergy * phase + ambientSky * 0.15;
float rim = pow(1.0 - abs(dot(rd, sunDir)), 4.0);
cloudColor += sunColor * rim * 0.3 * lightEnergy;
float alpha = 1.0 - exp(-density * stepSize * absorptionCoeff);
result.rgb += cloudColor * alpha * (1.0 - result.);
result. += alpha * ( - result.);
}
t += stepSize;
}
result;
}
Fullscreen Cloud Pass Setup
function createVolumetricCloudPass(camera, scene) {
const cloudMaterial = new THREE.ShaderMaterial({
uniforms: {
tDepth: { value: null },
cameraPos: { value: new THREE.Vector3() },
invProjection: { value: new THREE.Matrix4() },
invView: { value: new THREE.Matrix4() },
sunDir: { value: new THREE.Vector3(0.3, 0.8, 0.5).normalize() },
sunColor: { value: new THREE.Color(0xfff8e7) },
ambientSky: { value: new THREE.Color(0x6699cc) },
time: { value: 0 },
cloudBase: { : },
: { : },
: { : },
: { : },
: { : .(, ).() },
: { : },
: { : },
},
: ,
: ,
: ,
: ,
});
quad = .(
.(, ),
cloudMaterial
);
quad. = ;
{ quad, : cloudMaterial };
}
Light Marching & Scattering
The inner light march samples density toward the sun to compute self-shadowing:
float lightMarch(vec3 p) {
float accumDensity = 0.0;
float stepL = (cloudTop - cloudBase) / float(LIGHT_STEPS);
vec3 lightStep = normalize(sunDir) * stepL;
for (int i = 0; i < LIGHT_STEPS; i++) {
p += lightStep;
accumDensity += max(cloudDensity(p, time), 0.0) * stepL;
}
// Beer-powder approximation (brighter at thin edges)
float beer = exp(-accumDensity * absorptionCoeff);
float powder = 1.0 - exp(-accumDensity * absorptionCoeff * 2.0);
return mix(beer, beer * powder, 0.5);
}
Path 2: Mesh Cluster Clouds
For mid-range quality, build clouds from instanced soft-particle spheres. Each cloud
is a cluster of overlapping translucent spheres with noise-modulated opacity.
class MeshCloudSystem {
constructor(scene, options = {}) {
this.scene = scene;
this.cloudBase = options.cloudBase ?? 80;
this.spread = options.spread ?? 500;
this.cloudCount = options.cloudCount ?? 30;
this.particlesPerCloud = options.particlesPerCloud ?? 25;
this.clouds = [];
}
generate(seed = 0) {
const sphereGeo = new THREE.SphereGeometry(1, 12, 8);
const material = this._createMaterial();
for (let c = 0; c < this.cloudCount; c++) {
const cx = (seededRandom(seed + c * 3) - 0.5) * this.spread;
const cz = (seededRandom(seed + c * + ) - ) * .;
cy = . + (seed + c * + ) * ;
mesh = .(
sphereGeo, material, .
);
dummy = .();
cloudType = (seed + c * );
( i = ; i < .; i++) {
profile = .(cloudType, i, ., seed + c * + i);
dummy..(
cx + profile.,
cy + profile.,
cz + profile.
);
dummy..(profile., profile., profile.);
dummy.();
mesh.(i, dummy.);
}
mesh.. = ;
..(mesh);
..({ mesh, : .(cx, cy, cz) });
}
}
() {
r = seededRandom;
(type < ) {
angle = (seed) * . * ;
radius = (seed + ) * ;
y = .((seed + ) * - , );
{
: .(angle) * radius,
: y,
: .(angle) * radius,
: + (seed + ) * ,
: + (seed + ) * * ( - index / total),
: + (seed + ) * ,
};
} (type < ) {
{
: ((seed) - ) * ,
: ((seed + ) - ) * ,
: ((seed + ) - ) * ,
: + (seed + ) * ,
: + (seed + ) * ,
: + (seed + ) * ,
};
} {
t = index / total;
{
: t * - + ((seed) - ) * ,
: ((seed + ) - ) * ,
: ((seed + ) - ) * ,
: + (seed + ) * ,
: + (seed + ) * ,
: + (seed + ) * ,
};
}
}
() {
.({
: {
: { : .(, , ).() },
: { : .() },
: { : .() },
: { : .() },
: { : },
: { : },
},
: ,
: ,
: ,
: ,
: .,
});
}
() {
( cloud .) {
cloud... = cloud.. + .(time * * windSpeed) * ;
cloud... = cloud.. + time * windSpeed * ;
(cloud... > . / ) {
cloud... -= .;
}
}
(.[]) {
.[]..... = time;
}
}
() {
( cloud .) {
..(cloud.);
cloud...();
}
. = [];
}
}
() {
s = .(seed * + ) * ;
s - .(s);
}
Path 3: Billboard Clouds (Mobile/Background)
Camera-facing quads with procedural noise textures. Cheapest option for distant skies.
class BillboardCloudSystem {
constructor(scene, camera, options = {}) {
this.scene = scene;
this.camera = camera;
this.count = options.count ?? 20;
this.spread = options.spread ?? 400;
this.altitude = options.altitude ?? 100;
this.clouds = [];
}
generate(seed = 0) {
const texture = this._generateCloudTexture(256);
for (let i = 0; i < this.count; i++) {
const material = new THREE.SpriteMaterial({
map: texture,
transparent: true,
opacity: 0.5 + seededRandom(seed + i * 5) * 0.3,
depthWrite: false,
color: new THREE.().(, , + (seed + i * ) * ),
});
sprite = .(material);
sx = + (seed + i * ) * ;
sprite..(sx, sx * ( + (seed + i * ) * ), );
sprite..(
((seed + i * ) - ) * .,
. + (seed + i * ) * ,
((seed + i * ) - ) * .,
);
..(sprite);
..(sprite);
}
}
() {
canvas = .();
canvas. = canvas. = size;
ctx = canvas.();
grad = ctx.(size/, size/, , size/, size/, size/);
grad.(, );
grad.(, );
grad.(, );
grad.(, );
ctx. = grad;
ctx.(, , size, size);
imgData = ctx.(, , size, size);
( y = ; y < size; y++) {
( x = ; x < size; x++) {
idx = (y * size + x) * ;
nx = x / size * , ny = y / size * ;
n = (nx, ny, ) * ;
imgData.[idx + ] = .(, imgData.[idx + ] + n * );
}
}
ctx.(imgData, , );
tex = .(canvas);
tex. = ;
tex;
}
() {
( sprite .) {
sprite.. += windSpeed * ;
(sprite.. > . / ) sprite.. -= .;
}
}
() {
( s .) { ..(s); s..(); }
. = [];
}
}
() {
sum = , amp = , freq = , max = ;
( i = ; i < octaves; i++) {
sum += (.(x * freq * + y * freq * ) * + ) * amp;
max += amp; amp *= ; freq *= ;
}
sum / max;
}
Lighting Model
Cloud lighting is the single most important factor for beauty. All three paths share
the same lighting concepts.
Henyey-Greenstein Phase Function
Controls how light scatters through cloud particles. Two-lobe version for realism:
float henyeyGreenstein(float cosTheta, float g) {
float g2 = g * g;
return (1.0 - g2) / (4.0 * 3.14159 * pow(1.0 + g2 - 2.0 * g * cosTheta, 1.5));
}
// Two-lobe: forward scattering (silver linings) + back scattering (soft glow)
float cloudPhase(float cosTheta) {
return henyeyGreenstein(cosTheta, 0.6) * 0.7 // forward lobe
+ henyeyGreenstein(cosTheta, -0.3) * 0.3; // back lobe
}
Silver Lining Effect
When the sun is behind a cloud, edges glow brilliantly:
float silverLining(vec3 viewDir, vec3 sunDir, float density, float edgeDist) {
float backlit = max(dot(-viewDir, sunDir), 0.0);
float rim = pow(1.0 - edgeDist, 3.0); // Stronger at edges
return backlit * rim * exp(-density * 0.5); // Fades into thick cloud
}
Time-of-Day Coloring
Shift cloud colors based on sun elevation for sunrise/sunset/golden hour:
function cloudColorForTimeOfDay(sunElevation) {
if (sunElevation < 0) {
return {
sunColor: new THREE.Color(0x112244),
ambientColor: new THREE.Color(0x0a0a1a),
cloudTint: new THREE.Color(0x1a1a2e),
};
} else if (sunElevation < 0.1) {
return {
sunColor: new THREE.Color(0xff6622),
ambientColor: new THREE.Color(0x553322),
cloudTint: new THREE.Color(0xff8844),
};
} else if (sunElevation < 0.3) {
return {
sunColor: new THREE.(),
: .(),
: .(),
};
} {
{
: .(),
: .(),
: .(),
};
}
}
God Rays (Crepuscular Rays)
Post-process radial blur from sun position for volumetric light shafts:
function createGodRayPass() {
return new THREE.ShaderMaterial({
uniforms: {
tInput: { value: null },
sunScreenPos: { value: new THREE.Vector2(0.5, 0.7) },
exposure: { value: 0.3 },
decay: { value: 0.96 },
density: { value: 0.8 },
weight: { value: 0.4 },
samples: { value: 60 },
},
fragmentShader: GOD_RAY_FRAG,
vertexShader: FULLSCREEN_VERT,
});
}
Cloud Presets
Quick-start configurations. Full details in references/cloud-types.md.
const CLOUD_PRESETS = {
clearDay: {
coverage: 0.15, cloudBase: 2000, cloudTop: 3000,
type: 'cumulus', detailStrength: 0.4, absorptionCoeff: 0.04,
description: 'Scattered fair-weather cumulus, mostly blue sky',
},
partlyCloudy: {
coverage: 0.45, cloudBase: 1500, cloudTop: 3500,
type: 'cumulus', detailStrength: 0.3, absorptionCoeff: 0.04,
description: 'Classic partly cloudy — picturesque cumulus fields',
},
overcast: {
coverage: 0.85, cloudBase: 800, cloudTop: 2000,
type: 'stratus', detailStrength: 0.2, absorptionCoeff: 0.06,
description: 'Flat grey blanket, diffused light',
},
dramatic: {
coverage: 0.6, cloudBase: 1000, cloudTop: ,
: , : , : ,
: ,
},
: {
: , : , : ,
: , : , : ,
: ,
: ,
},
: {
: , : , : ,
: , : , : ,
: ,
},
: {
: , : , : ,
: , : , : ,
: ,
},
};
Complete Scene Assembly
async function init() {
const canvas = document.querySelector('#canvas');
const { renderer, gpuAvailable } = await createRenderer(canvas);
const scene = new THREE.Scene();
const camera = new THREE.PerspectiveCamera(60, innerWidth / innerHeight, 1, 10000);
camera.position.set(0, 20, 100);
const { OrbitControls } = await import('three/addons/controls/OrbitControls.js');
const controls = new OrbitControls(camera, renderer.domElement);
controls.enableDamping = true;
controls.maxPolarAngle = Math.PI * 0.49;
scene.background = createSkyGradient();
const ground = new THREE.Mesh(
.(, ),
.({ : , : })
);
ground.. = -. / ;
ground. = ;
scene.(ground);
sun = .(, );
sun..(, , );
scene.(sun);
scene.( .(, , ));
cloudSystem;
(gpuAvailable) {
cloudSystem = (scene, { : , : });
} {
cloudSystem = (scene, { : , : });
}
cloudSystem.();
clock = .();
renderer.( {
t = clock.();
cloudSystem.(t, );
controls.();
renderer.(scene, camera);
});
.(, {
camera. = innerWidth / innerHeight;
camera.();
renderer.(innerWidth, innerHeight);
});
}
() {
canvas = .();
canvas. = ; canvas. = ;
ctx = canvas.();
grad = ctx.(, , , );
grad.(, );
grad.(, );
grad.(, );
grad.(, );
ctx. = grad;
ctx.(, , , );
tex = .(canvas);
tex. = .;
tex;
}
();
Performance Guidelines
| Path | Cost | Max Clouds | Target FPS |
|---|
| Volumetric | High | Full sky coverage | 30+ (desktop) |
| Mesh Cluster | Medium | 20–40 cloud groups | 60 (desktop), 30 (mobile) |
| Billboard | Low | 50+ sprites | 60 everywhere |
Volumetric optimization:
- Reduce
MAX_STEPS (64 for quality, 32 for performance).
- Quarter-resolution render target, bilateral upsample.
- Temporal reprojection: reuse previous frame, march 1/4 of rays per frame.
- Blue noise dithering on step offset to hide banding.
Mesh cluster optimization:
- Merge particles into fewer draw calls via
InstancedMesh.
- Reduce
particlesPerCloud for distant clouds.
- Sort back-to-front per frame for correct transparency (or use additive blending).
Shared tips:
depthWrite: false on all cloud materials — clouds don't occlude each other properly via depth.
- Distance fade: dissolve clouds beyond a radius with alpha.
- Skybox fallback: for extreme distance, bake clouds into a cubemap.
Common Pitfalls
- Flat/boring clouds: Insufficient octaves in FBM. Use 5+ octaves for the detail pass and vary
coverage to create interesting negative space.
- Grey mush at sunset: Must tint cloud color by sun angle. Apply
cloudColorForTimeOfDay() and increase scattering at low elevation.
- Banding in raymarching: Add jitter to initial ray offset:
t += hash(screenUV) * stepSize. Blue noise texture gives best results.
- Transparent sorting artifacts (mesh path): Sort instances back-to-front, or use additive blending (loses dark cloud bases).
- Clouds clip through terrain: Cloud base must be above camera + terrain. Use depth buffer to composite volumetric clouds behind geometry.
References
references/cloud-shaders.md — Complete GLSL vertex/fragment shaders for all three paths, WGSL compute noise, god ray post-process.
references/cloud-types.md — Detailed profiles for all 10 cloud genera with density field parameters, lighting settings, and artistic direction.