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) { /* 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.
// 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:
// 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
// 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
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:
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 * 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;
// Wrap clouds
if (cloud.mesh.position.z > this.spread / 2) {
cloud.mesh.position.z -= this.spread;
}
}
if (this.clouds[0]) {
this.clouds[0].mesh.material.uniforms.time.value = time;
}
}
dispose() {
for (const cloud of this.clouds) {
this.scene.remove(cloud.mesh);
cloud.mesh.geometry.dispose();
}
this.clouds = [];
}
}
function seededRandom(seed) {
const s = Math.sin(seed * 127.1 + 311.7) * 43758.5453;
return s - Math.floor(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.Color().setHSL(0, 0, 0.9 + seededRandom(seed + i * 7) * 0.1),
});
const sprite = new THREE.Sprite(material);
const sx = 30 + seededRandom(seed + i * 11) * 50;
sprite.scale.set(sx, sx * (0.3 + seededRandom(seed + i * 13) * 0.3), 1);
sprite.position.set(
(seededRandom(seed + i * 2) - 0.5) * this.spread,
this.altitude + seededRandom(seed + i * 3) * 30,
(seededRandom(seed + i * 4) - 0.5) * this.spread,
);
this.scene.add(sprite);
this.clouds.push(sprite);
}
}
_generateCloudTexture(size) {
const canvas = document.createElement('canvas');
canvas.width = canvas.height = size;
const ctx = canvas.getContext('2d');
// Radial gradient base
const grad = ctx.createRadialGradient(size/2, size/2, 0, size/2, size/2, size/2);
grad.addColorStop(0, 'rgba(255,255,255,0.9)');
grad.addColorStop(0.4, 'rgba(255,255,255,0.6)');
grad.addColorStop(0.7, 'rgba(240,240,255,0.2)');
grad.addColorStop(1, 'rgba(240,240,255,0)');
ctx.fillStyle = grad;
ctx.fillRect(0, 0, size, size);
// Add noise bumps for cloudlike edges
const imgData = ctx.getImageData(0, 0, size, size);
for (let y = 0; y < size; y++) {
for (let x = 0; x < size; x++) {
const idx = (y * size + x) * 4;
const nx = x / size * 6, ny = y / size * 6;
const n = simpleFBM2D(nx, ny, 4) * 0.3;
imgData.data[idx + 3] = Math.max(0, imgData.data[idx + 3] + n * 255);
}
}
ctx.putImageData(imgData, 0, 0);
const tex = new THREE.CanvasTexture(canvas);
tex.needsUpdate = true;
return tex;
}
update(time, windSpeed = 5) {
for (const sprite of this.clouds) {
sprite.position.x += windSpeed * 0.02;
if (sprite.position.x > this.spread / 2) sprite.position.x -= this.spread;
}
}
dispose() {
for (const s of this.clouds) { this.scene.remove(s); s.material.dispose(); }
this.clouds = [];
}
}
function simpleFBM2D(x, y, octaves) {
let sum = 0, amp = 1, freq = 1, max = 0;
for (let i = 0; i < octaves; i++) {
sum += (Math.sin(x * freq * 127.1 + y * freq * 311.7) * 0.5 + 0.5) * amp;
max += amp; amp *= 0.5; freq *= 2;
}
return 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) {
// sunElevation: -0.1 (below horizon) to 1.0 (noon)
if (sunElevation < 0) {
// Night: dark blue-grey
return {
sunColor: new THREE.Color(0x112244),
ambientColor: new THREE.Color(0x0a0a1a),
cloudTint: new THREE.Color(0x1a1a2e),
};
} else if (sunElevation < 0.1) {
// Golden hour / sunset
return {
sunColor: new THREE.Color(0xff6622),
ambientColor: new THREE.Color(0x553322),
cloudTint: new THREE.Color(0xff8844),
};
} else if (sunElevation < 0.3) {
// Morning / late afternoon
return {
sunColor: new THREE.Color(0xffcc88),
ambientColor: new THREE.Color(0x667799),
cloudTint: new THREE.Color(0xffeedd),
};
} else {
// Midday
return {
sunColor: new THREE.Color(0xfff8e7),
ambientColor: new THREE.Color(0xb0c4de),
cloudTint: new THREE.Color(0xffffff),
};
}
}
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, // See references/cloud-shaders.md
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: 6000,
type: 'cumulonimbus', detailStrength: 0.5, absorptionCoeff: 0.08,
description: 'Towering storm clouds with dark bases and bright anvils',
},
sunset: {
coverage: 0.4, cloudBase: 1500, cloudTop: 3000,
type: 'stratocumulus', detailStrength: 0.35, absorptionCoeff: 0.03,
sunElevation: 0.05,
description: 'Golden hour stratocumulus lit from below',
},
highCirrus: {
coverage: 0.3, cloudBase: 8000, cloudTop: 12000,
type: 'cirrus', detailStrength: 0.6, absorptionCoeff: 0.01,
description: 'Delicate ice crystal wisps at high altitude',
},
mackerelSky: {
coverage: 0.5, cloudBase: 3000, cloudTop: 5000,
type: 'altocumulus', detailStrength: 0.45, absorptionCoeff: 0.03,
description: 'Rippled altocumulus creating a textured sky pattern',
},
};
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;
// Sky gradient background
scene.background = createSkyGradient();
// Ground
const ground = new THREE.Mesh(
new THREE.PlaneGeometry(2000, 2000),
new THREE.MeshStandardMaterial({ color: 0x4a7c3f, roughness: 0.9 })
);
ground.rotation.x = -Math.PI / 2;
ground.receiveShadow = true;
scene.add(ground);
// Lighting
const sun = new THREE.DirectionalLight(0xfff4e5, 1.5);
sun.position.set(200, 300, 150);
scene.add(sun);
scene.add(new THREE.HemisphereLight(0x87ceeb, 0x4a7c3f, 0.6));
// Clouds — select path based on capability
let cloudSystem;
if (gpuAvailable) {
// Volumetric raymarching (see references for full setup)
cloudSystem = new MeshCloudSystem(scene, { cloudBase: 80, cloudCount: 25 });
} else {
cloudSystem = new MeshCloudSystem(scene, { cloudBase: 80, cloudCount: 20 });
}
cloudSystem.generate(12345);
// Animate
const clock = new THREE.Clock();
renderer.setAnimationLoop(() => {
const t = clock.getElapsedTime();
cloudSystem.update(t, 8);
controls.update();
renderer.render(scene, camera);
});
window.addEventListener('resize', () => {
camera.aspect = innerWidth / innerHeight;
camera.updateProjectionMatrix();
renderer.setSize(innerWidth, innerHeight);
});
}
function createSkyGradient() {
const canvas = document.createElement('canvas');
canvas.width = 2; canvas.height = 256;
const ctx = canvas.getContext('2d');
const grad = ctx.createLinearGradient(0, 0, 0, 256);
grad.addColorStop(0, '#0a4a8a'); // Zenith
grad.addColorStop(0.5, '#5b9bd5'); // Mid-sky
grad.addColorStop(0.8, '#c8ddf0'); // Horizon
grad.addColorStop(1, '#e8dcc8'); // Below horizon
ctx.fillStyle = grad;
ctx.fillRect(0, 0, 2, 256);
const tex = new THREE.CanvasTexture(canvas);
tex.mapping = THREE.EquirectangularReflectionMapping;
return tex;
}
init();
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
particlesPerCloudfor distant clouds. - Sort back-to-front per frame for correct transparency (or use additive blending).
Shared tips:
depthWrite: falseon 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
coverageto 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.