/SKILL.md
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".
npx skillsauth add ck42bb/procedural-clouds-threejs procedural-cloudsInstall this skill globally with one command. Works with Claude Code, Cursor, and Windsurf.
3 of 9 scanners reported clean
Some scanners were skipped, did not run, or reported a non-clean status. Review each row below.
Generate visually stunning procedural clouds in Three.js with artistic emphasis — volumetric raymarching on WebGPU, billboard/mesh fallbacks on WebGL2.
┌──────────────────────────────────────────────────────┐
│ 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 │
└──────────────────────────────────────────────────────┘
| 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.
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 };
}
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;
}
The highest-quality path renders clouds by marching rays through a density field defined by 3D noise. Implemented as a fullscreen post-process pass.
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);
}
// 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;
}
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 };
}
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);
}
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);
}
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;
}
Cloud lighting is the single most important factor for beauty. All three paths share the same lighting concepts.
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
}
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
}
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),
};
}
}
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,
});
}
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',
},
};
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();
| 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:
MAX_STEPS (64 for quality, 32 for performance).Mesh cluster optimization:
InstancedMesh.particlesPerCloud for distant clouds.Shared tips:
depthWrite: false on all cloud materials — clouds don't occlude each other properly via depth.coverage to create interesting negative space.cloudColorForTimeOfDay() and increase scattering at low elevation.t += hash(screenUV) * stepSize. Blue noise texture gives best results.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.development
Maintainer-only workflow for handling GitHub Secret Scanning alerts on OpenClaw. Use when Codex needs to triage, redact, clean up, and resolve secret leakage found in issue comments, issue bodies, PR comments, or other GitHub content.
development
Maintainer workflow for OpenClaw releases, prereleases, changelog release notes, and publish validation. Use when Codex needs to prepare or verify stable or beta release steps, align version naming, assemble release notes, check release auth requirements, or validate publish-time commands and artifacts.
development
Run, watch, debug, and extend OpenClaw QA testing with qa-lab and qa-channel. Use when Codex needs to execute the repo-backed QA suite, inspect live QA artifacts, debug failing scenarios, add new QA scenarios, or explain the OpenClaw QA workflow. Prefer the live OpenAI lane with regular openai/gpt-5.4 in fast mode; do not use gpt-5.4-pro or gpt-5.4-mini unless the user explicitly overrides that policy.
development
End-to-end Parallels smoke, upgrade, and rerun workflow for OpenClaw across macOS, Windows, and Linux guests. Use when Codex needs to run, rerun, debug, or interpret VM-based install, onboarding, gateway smoke tests, latest-release-to-main upgrade checks, fresh snapshot retests, or optional Discord roundtrip verification under Parallels.