- 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
```javascript
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) { /* fallback */ }
if (!renderer) {
renderer = new THREE.WebGLRenderer({ canvas, antialias: true });
renderer.toneMapping = THREE.ACESFilmicToneMapping;
renderer.toneMappingExposure = 1.2;
}
renderer.setSize(innerWidth, innerHeight);
renderer.setPixelRatio(Math.min(devicePixelRatio, 2));
return { renderer, gpuAvailable };
}
```
## 3D Noise Foundation
All cloud rendering depends on layered 3D noise. These functions are shared across
all three rendering paths.
```javascript
// GPU-friendly 3D hash (no lookup tables)
// Used in shaders — JavaScript equivalent for CPU cloud mesh placement
function hash3(x, y, z) {
let h = x * 127.1 + y * 311.7 + z * 74.7;
return (Math.sin(h) * 43758.5453) % 1;
}
// 3D value noise
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)), lerp(ux, h(0,1,0), h(1,1,0))),
lerp(uy, lerp(ux, h(0,0,1), h(1,0,1)), lerp(ux, h(0,1,1), h(1,1,1)))
);
}
function lerp(t, a, b) { return a + t * (b - a); }
// FBM for cloud density
function cloudFBM(x, y, z, octaves = 5, lac = 2.0, gain = 0.5) {
let sum = 0, amp = 1, freq = 1, max = 0;
for (let i = 0; i < octaves; i++) {
sum += noise3D(x * freq, y * freq, z * freq) * amp;
max += amp; amp *= gain; freq *= lac;
}
return 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:
```javascript
// GLSL-style pseudocode for the density function (full GLSL in references)
float cloudDensity(vec3 p, float time) {
// Altitude shaping — confine to cloud layer
float altFade = smoothstep(cloudBase, cloudBase + 200.0, p.y)
* smoothstep(cloudTop, cloudTop - 200.0, p.y);
// Large-scale shape (coverage map)
float shape = fbm3D(p * 0.0003 + wind * time, 3);
shape = remap(shape, coverageThreshold, 1.0, 0.0, 1.0); // coverage control
// Detail erosion (carves edges)
float detail = fbm3D(p * 0.003 + wind * time * 2.0, 5);
float density = shape - detail * detailStrength;
return max(density * altFade, 0.0);
}
```
### Raymarching Loop
```javascript
// Core raymarching pattern (see references/cloud-shaders.md for full GLSL)
vec4 raymarchClouds(vec3 ro, vec3 rd) {
float t = intersectCloudLayer(ro, rd); // Ray-slab intersection
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) {
// Light marching — secondary ray toward sun
float lightEnergy = lightMarch(p);
// Phase function (Henyey-Greenstein)
float phase = henyeyGreenstein(dot(rd, sunDir), 0.3)
+ henyeyGreenstein(dot(rd, sunDir), 0.8) * 0.5;
// Color from scattering
vec3 cloudColor = sunColor * lightEnergy * phase + ambientSky * 0.15;
// Silver lining — bright edge when sun is behind cloud
float rim = pow(1.0 - abs(dot(rd, sunDir)), 4.0);
cloudColor += sunColor * rim * 0.3 * lightEnergy;
// Beer-Lambert absorption
float alpha = 1.0 - exp(-density * stepSize * absorptionCoeff);
result.rgb += cloudColor * alpha * (1.0 - result.a);
result.a += alpha * (1.0 - result.a);
}
t += stepSize;
}
return result;
}
```
### Fullscreen Cloud Pass Setup
```javascript
function createVolumetricCloudPass(camera, scene) {
const cloudMaterial = new THREE.ShaderMaterial({
uniforms: {
tDepth: { value: null }, // Scene depth texture
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: { value: 1500 }, // meters
cloudTop: { value: 3500 },
coverage: { value: 0.45 }, // 0-1, controls cloud amount
detailStrength: { value: 0.35 },
windDirection: { value: new THREE.Vector2(1, 0.3).normalize() },
windSpeed: { value: 15 },
absorptionCoeff: { value: 0.04 },
},
vertexShader: FULLSCREEN_VERT, // See references/cloud-shaders.md
fragmentShader: VOLUMETRIC_FRAG, // See references/cloud-shaders.md
transparent: true,
depthWrite: false,
});
const quad = new THREE.Mesh(
new THREE.PlaneGeometry(2, 2),
cloudMaterial
);
quad.frustumCulled = false;
return { quad, material: cloudMaterial };
}
```
### Light Marching & Scattering
The inner light march samples density toward the sun to compute self-shadowing:
```glsl
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.
```javascript
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 * 3 + 1) - 0.5) * this.spread;
const cy = this.cloudBase + seededRandom(seed + c * 3 + 2) * 30;
const mesh = new THREE.InstancedMesh(
sphereGeo, material, this.particlesPerCloud
);
const dummy = new THREE.Object3D();
const cloudType = seededRandom(seed + c * 7);
for (let i = 0; i < this.particlesPerCloud; i++) {
const profile = this._cloudProfile(cloudType, i, this.particlesPerCloud, seed + c * 100 + i);
dummy.position.set(
cx + profile.x,
cy + profile.y,
cz + profile.z
);
dummy.scale.set(profile.sx, profile.sy, profile.sz);
dummy.updateMatrix();
mesh.setMatrixAt(i, dummy.matrix);
}
mesh.instanceMatrix.needsUpdate = true;
this.scene.add(mesh);
this.clouds.push({ mesh, basePos: new THREE.Vector3(cx, cy, cz) });
}
}
// Cloud shape profiles — different particle distributions per cloud type
_cloudProfile(type, index, total, seed) {
const r = seededRandom;
if (type < 0.4) {
// Cumulus: dome top, flat base
const angle = r(seed) * Math.PI * 2;
const radius = r(seed + 1) * 15;
const y = Math.max(r(seed + 2) * 12 - 2, 0); // Flat base (no negative y)
return {
x: Math.cos(angle) * radius,
y: y,
z: Math.sin(angle) * radius,
sx: 5 + r(seed + 3) * 8,
sy: 3 + r(seed + 4) * 5 * (1 - index / total), // Taller at center
sz: 5 + r(seed + 5) * 8,
};
} else if (type < 0.7) {
// Stratus: wide, flat, layered
return {
x: (r(seed) - 0.5) * 40,
y: (r(seed + 1) - 0.5) * 3,
z: (r(seed + 2) - 0.5) * 40,
sx: 8 + r(seed + 3) * 12,
sy: 1.5 + r(seed + 4) * 2,
sz: 8 + r(seed + 5) * 12,
};
} else {
// Cirrus: wispy elongated streaks
const t = index / total;
return {
x: t * 30 - 15 + (r(seed) - 0.5) * 5,
y: (r(seed + 1) - 0.5) * 2,
z: (r(seed + 2) - 0.5) * 4,
sx: 3 + r(seed + 3) * 4,
sy: 0.5 + r(seed + 4) * 1,
sz: 1.5 + r(seed + 5) * 2,
};
}
}
_createMaterial() {
return new THREE.ShaderMaterial({
uniforms: {
sunDir: { value: new THREE.Vector3(0.3, 0.8, 0.5).normalize() },
sunColor: { value: new THREE.Color(0xfff8e7) },
ambientColor: { value: new THREE.Color(0xb0c4de) },
baseColor: { value: new THREE.Color(0xffffff) },
opacity: { value: 0.6 },
time: { value: 0 },
},
vertexShader: MESH_CLOUD_VERT, // See references/cloud-shaders.md
fragmentShader: MESH_CLOUD_FRAG, // See references/cloud-shaders.md
transparent: true,
depthWrite: false,
side: THREE.DoubleSide,
});
}
update(time, windDir, windSpeed) {
for (const cloud of this.clouds) {
cloud.mesh.position.x = cloud.basePos.x + Math.sin(time * 0.01 * windSpeed) * 5;
cloud.mesh.position.z = cloud.basePos.z + time * windSpeed * 0.1;
View on GitHub