procedural-clouds

SKILL.md

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

  1. Flat/boring clouds: Insufficient octaves in FBM. Use 5+ octaves for the detail pass and vary coverage to create interesting negative space.
  2. Grey mush at sunset: Must tint cloud color by sun angle. Apply cloudColorForTimeOfDay() and increase scattering at low elevation.
  3. Banding in raymarching: Add jitter to initial ray offset: t += hash(screenUV) * stepSize. Blue noise texture gives best results.
  4. Transparent sorting artifacts (mesh path): Sort instances back-to-front, or use additive blending (loses dark cloud bases).
  5. 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.
Weekly Installs
4
GitHub Stars
28
First Seen
Feb 14, 2026
Installed on
opencode4
gemini-cli4
github-copilot4
codex4
kimi-cli4
amp4