Skip to main content

procedural-clouds

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".

Source facts

Repository
CK42BB/procedural-clouds-threejs
Last source activity
February 12, 2026 at 02:25
Detected SKILL.md language
English
Stars
49
Forks
6

Install options

The review-first prompt is selected by default. You can switch to a direct command or download a local copy.

Review the source files

Read SKILL.md and any companion files shown by SkillsMP before deciding whether to install.

Showing SKILL.md

SKILL.md
Source instructions · Read-only preview
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
This SKILL.md is very large, so SkillsMP previews the first section here. View on GitHub