procedural-clouds
git clone https://github.com/CK42BB/procedural-clouds-threejs
git clone --depth=1 https://github.com/CK42BB/procedural-clouds-threejs ~/.claude/skills/ck42bb-procedural-clouds-threejs-procedural-clouds
SKILL.mdProcedural 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
(64 for quality, 32 for performance).MAX_STEPS - 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
for distant clouds.particlesPerCloud - Sort back-to-front per frame for correct transparency (or use additive blending).
Shared tips:
on all cloud materials — clouds don't occlude each other properly via depth.depthWrite: false- 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
to create interesting negative space.coverage - Grey mush at sunset: Must tint cloud color by sun angle. Apply
and increase scattering at low elevation.cloudColorForTimeOfDay() - Banding in raymarching: Add jitter to initial ray offset:
. Blue noise texture gives best results.t += hash(screenUV) * stepSize - 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
— Complete GLSL vertex/fragment shaders for all three paths, WGSL compute noise, god ray post-process.references/cloud-shaders.md
— Detailed profiles for all 10 cloud genera with density field parameters, lighting settings, and artistic direction.references/cloud-types.md