- name
- procedural-weather
- description
- Generate dynamic procedural weather effects in Three.js using WebGPU compute with WebGL2 fallback. Covers rain (drizzle to monsoon), snow (flurries to blizzard), hail, fog and mist (ground fog, volumetric), lightning (bolt, sheet, cloud-internal), dust/sandstorms, wind-driven debris, rainbows, aurora borealis, and a weather state machine for smooth transitions. All precipitation uses GPU particle systems with collision, splashes, and accumulation. Provides per-effect lighting changes, ambient sound cue hints, and camera post-processing (wet lens, frost). Use when building weather systems, atmospheric effects, environmental ambience, climate simulation, or any scene needing dynamic weather. Triggers: "procedural weather", "rain effect", "snow particles", "lightning", "fog shader", "weather system", "storm", "blizzard", "sandstorm", "aurora", "rainbow", "precipitation", "weather transition", "wet lens".
# Procedural Weather
Dynamic, layered weather effects in Three.js — GPU particle precipitation, volumetric
fog, lightning, and smooth state transitions.
## Architecture Overview
```
┌──────────────────────────────────────────────────────┐
│ Weather System │
│ │
│ WeatherController (state machine) │
│ ├── current state + target state │
│ ├── transition progress (0→1) │
│ └── drives all subsystems: │
│ │
│ ┌─ Precipitation ──────────────────────────────┐ │
│ │ GPU particles: rain, snow, hail │ │
│ │ Ground splashes, accumulation │ │
│ └──────────────────────────────────────────────┘ │
│ ┌─ Atmosphere ─────────────────────────────────┐ │
│ │ Fog, mist, dust, haze │ │
│ │ Volumetric (WebGPU) or exponential (WebGL) │ │
│ └──────────────────────────────────────────────┘ │
│ ┌─ Electrical ─────────────────────────────────┐ │
│ │ Lightning bolts, sheet flashes │ │
│ │ Cloud-internal illumination │ │
│ └──────────────────────────────────────────────┘ │
│ ┌─ Optical ────────────────────────────────────┐ │
│ │ Rainbows, aurora, god rays │ │
│ │ Wet lens, frost overlay │ │
│ └──────────────────────────────────────────────┘ │
│ ┌─ Environment ────────────────────────────────┐ │
│ │ Sky darkening, light color shift │ │
│ │ Wind direction (shared across systems) │ │
│ └──────────────────────────────────────────────┘ │
└──────────────────────────────────────────────────────┘
```
## 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) {}
if (!renderer) {
renderer = new THREE.WebGLRenderer({ canvas, antialias: true });
renderer.toneMapping = THREE.ACESFilmicToneMapping;
}
renderer.setSize(innerWidth, innerHeight);
renderer.setPixelRatio(Math.min(devicePixelRatio, 2));
return { renderer, gpuAvailable };
}
```
## Shared Wind System
Wind drives all weather subsystems — precipitation angle, fog drift, debris direction.
```javascript
class WindSystem {
constructor() {
this.direction = new THREE.Vector3(1, 0, 0.3).normalize();
this.baseSpeed = 5; // m/s
this.gustSpeed = 0;
this.gustFrequency = 0.5;
this.turbulence = 0.1;
this._time = 0;
}
update(dt) {
this._time += dt;
// Gusts: low-frequency intensity variation
const gustEnvelope = Math.sin(this._time * this.gustFrequency) * 0.5 + 0.5;
this.gustSpeed = gustEnvelope * this.baseSpeed * 0.6;
}
get speed() { return this.baseSpeed + this.gustSpeed; }
// Wind force vector for particle displacement
get force() {
const s = this.speed;
const turb = new THREE.Vector3(
Math.sin(this._time * 2.3) * this.turbulence,
0,
Math.cos(this._time * 1.7) * this.turbulence
);
return this.direction.clone().multiplyScalar(s).add(turb);
}
}
```
## Precipitation System
GPU particle system for rain, snow, and hail. Particles spawn in a box above the
camera, fall with gravity + wind, and recycle when below ground.
### Particle Buffer Layout
```javascript
function createPrecipitationBuffers(maxParticles) {
// Each particle: vec4(x, y, z, life) + vec4(vx, vy, vz, size)
const positions = new Float32Array(maxParticles * 4);
const velocities = new Float32Array(maxParticles * 4);
return { positions, velocities, count: maxParticles };
}
```
### Rain System
```javascript
class RainSystem {
constructor(scene, options = {}) {
this.scene = scene;
this.count = options.count ?? 50000;
this.spawnRadius = options.spawnRadius ?? 40;
this.spawnHeight = options.spawnHeight ?? 30;
this.dropLength = options.dropLength ?? 0.3;
this.intensity = options.intensity ?? 1.0; // 0-1
this._buildGeometry();
}
_buildGeometry() {
// Each raindrop: a short line segment (2 vertices)
const positions = new Float32Array(this.count * 6); // 2 verts × xyz
const randoms = new Float32Array(this.count * 2); // per-drop seed + phase
for (let i = 0; i < this.count; i++) {
const x = (Math.random() - 0.5) * this.spawnRadius * 2;
const y = Math.random() * this.spawnHeight;
const z = (Math.random() - 0.5) * this.spawnRadius * 2;
// Top vertex
positions[i * 6] = x;
positions[i * 6 + 1] = y;
positions[i * 6 + 2] = z;
// Bottom vertex (offset by drop length)
positions[i * 6 + 3] = x;
positions[i * 6 + 4] = y - this.dropLength;
positions[i * 6 + 5] = z;
randoms[i * 2] = Math.random(); // seed
randoms[i * 2 + 1] = Math.random(); // phase offset
}
const geometry = new THREE.BufferGeometry();
geometry.setAttribute('position', new THREE.BufferAttribute(positions, 3));
geometry.setAttribute('aRandom', new THREE.BufferAttribute(randoms, 2, false, this.count));
this.material = new THREE.ShaderMaterial({
uniforms: {
time: { value: 0 },
intensity: { value: this.intensity },
windForce: { value: new THREE.Vector3() },
gravity: { value: -15.0 },
spawnHeight: { value: this.spawnHeight },
spawnRadius: { value: this.spawnRadius },
dropLength: { value: this.dropLength },
opacity: { value: 0.35 },
cameraPos: { value: new THREE.Vector3() },
},
vertexShader: RAIN_VERT, // See references/weather-shaders.md
fragmentShader: RAIN_FRAG, // See references/weather-shaders.md
transparent: true,
depthWrite: false,
blending: THREE.AdditiveBlending,
});
this.mesh = new THREE.LineSegments(geometry, this.material);
this.mesh.frustumCulled = false;
this.scene.add(this.mesh);
}
update(time, wind, cameraPos) {
this.material.uniforms.time.value = time;
this.material.uniforms.windForce.value.copy(wind.force);
this.material.uniforms.cameraPos.value.copy(cameraPos);
this.material.uniforms.intensity.value = this.intensity;
// Recenter spawn box around camera
this.mesh.position.x = cameraPos.x;
this.mesh.position.z = cameraPos.z;
}
dispose() {
this.scene.remove(this.mesh);
this.mesh.geometry.dispose();
this.material.dispose();
}
}
```
### Snow System
Snow uses `Points` with per-particle flutter and tumble. Larger particles, slower fall,
more wind influence.
```javascript
class SnowSystem {
constructor(scene, options = {}) {
this.scene = scene;
this.count = options.count ?? 20000;
this.spawnRadius = options.spawnRadius ?? 50;
this.spawnHeight = options.spawnHeight ?? 25;
this.intensity = options.intensity ?? 1.0;
this._build();
}
_build() {
const positions = new Float32Array(this.count * 3);
const seeds = new Float32Array(this.count * 3); // seed, phase, size
for (let i = 0; i < this.count; i++) {
positions[i * 3] = (Math.random() - 0.5) * this.spawnRadius * 2;
positions[i * 3 + 1] = Math.random() * this.spawnHeight;
positions[i * 3 + 2] = (Math.random() - 0.5) * this.spawnRadius * 2;
seeds[i * 3] = Math.random();
seeds[i * 3 + 1] = Math.random() * Math.PI * 2;
seeds[i * 3 + 2] = 0.5 + Math.random() * 1.5; // size variation
}
const geometry = new THREE.BufferGeometry();
geometry.setAttribute('position', new THREE.BufferAttribute(positions, 3));
geometry.setAttribute('aSeed', new THREE.BufferAttribute(seeds, 3));
this.material = new THREE.ShaderMaterial({
uniforms: {
time: { value: 0 },
intensity: { value: this.intensity },
windForce: { value: new THREE.Vector3() },
gravity: { value: -1.5 },
spawnHeight: { value: this.spawnHeight },
spawnRadius: { value: this.spawnRadius },
flutterAmp: { value: 2.0 },
opacity: { value: 0.85 },
pointSize: { value: 3.0 },
cameraPos: { value: new THREE.Vector3() },
},
vertexShader: SNOW_VERT, // See references/weather-shaders.md
fragmentShader: SNOW_FRAG, // See references/weather-shaders.md
transparent: true,
depthWrite: false,
});
this.mesh = new THREE.Points(geometry, this.material);
this.mesh.frustumCulled = false;
this.scene.add(this.mesh);
}
update(time, wind, cameraPos) {
this.material.uniforms.time.value = time;
this.material.uniforms.windForce.value.copy(wind.force);
this.material.uniforms.cameraPos.value.copy(cameraPos);
this.material.uniforms.intensity.value = this.intensity;
this.mesh.position.x = cameraPos.x;
this.mesh.position.z = cameraPos.z;
}
dispose() {
this.scene.remove(this.mesh);
this.mesh.geometry.dispose();
this.material.dispose();
}
}
```
### Splash / Impact System
Ground splashes when rain hits surfaces — instanced ring sprites at impact points.
```javascript
class SplashSystem {
constructor(scene, maxSplashes = 500) {
this.scene = scene;
this.max = maxSplashes;
const geo = new THREE.PlaneGeometry(0.15, 0.15);
geo.rotateX(-Math.PI / 2);
this.material = new THREE.ShaderMaterial({
uniforms: { time: { value: 0 } },
vertexShader: SPLASH_VERT,
fragmentShader: SPLASH_FRAG,
transparent: true,
depthWrite: false,
});
this.mesh = new THREE.InstancedMesh(geo, this.material, maxSplashes);
this.mesh.frustumCulled = false;
this.lifetimes = new Float32Array(maxSplashes);
this.nextIdx = 0;
this.scene.add(this.mesh);
}
spawn(position) {
const dummy = new THREE.Object3D();
dummy.position.copy(position);
dummy.position.y += 0.01;
dummy.scale.setScalar(0.5 + Math.random() * 0.5);
dummy.updateMatrix();
this.mesh.setMatrixAt(this.nextIdx, dummy.matrix);
this.lifetimes[this.nextIdx] = 1.0;
this.mesh.instanceMatrix.needsUpdate = true;
this.nextIdx = (this.nextIdx + 1) % this.max;
}
update(dt) {
for (let i = 0; i < this.max; i++) {
if (this.lifetimes[i] > 0) {
this.lifetimes[i] -= dt * 4; // ~0.25s lifetime
}
}
this.material.uniforms.time.value += dt;
}
}
```
## Fog & Atmosphere
### Exponential Fog (WebGL — built-in)
```javascript
function applyFog(scene, density = 0.01, color = 0xcccccc) {
scene.fog = new THREE.FogExp2(color, density);
scene.background = new THREE.Color(color);
}
```
### Ground Fog (Shader-based)
Height-attenuated fog that pools in valleys. Applied as a post-process or scene material.
```javascript
function createGroundFog(size = 200, maxHeight = 8) {
const geo = new THREE.PlaneGeometry(size, size, 1, 1);
geo.rotateX(-Math.PI / 2);
const material = new THREE.ShaderMaterial({
uniforms: {
time: { value: 0 },
fogColor: { value: new THREE.Color(0xdddddd) },
maxHeight: { value: maxHeight },
density: { value: 0.8 },
windDir: { value: new THREE.Vector2(1, 0) },
windSpeed: { value: 2.0 },
},
vertexShader: `
varying vec3 vWorldPos;
varying vec2 vUv;
void main() {
vWorldPos = (modelMatrix * vec4(position, 1.0)).xyz;
vUv = uv;
gl_Position = projectionMatrix * modelViewMatrix * vec4(position, 1.0);
}
`,
fragmentShader: GROUND_FOG_FRAG, // See references/weather-shaders.md
transparent: true,
depthWrite: false,
side: THREE.DoubleSide,
});
const mesh = new THREE.Mesh(geo, material);
mesh.position.y = maxHeight * 0.5;
mesh.renderOrder = 999;
return mesh;
}
```
### Dust / Sandstorm
Dense particle field with color tinting and reduced visibility.
```javascript
function createDustStorm(scene, options = {}) {
const count = options.count ?? 30000;
const color = options.color ?? new THREE.Color(0xc4a060);
const positions = new Float32Array(count * 3);
const seeds = new Float32Array(count);
for (let i = 0; i < count; i++) {
positions[i * 3] = (Math.random() - 0.5) * 80;
positions[i * 3 + 1] = Math.random() * 15;
positions[i * 3 + 2] = (Math.random() - 0.5) * 80;
seeds[i] = Math.random();
}
View on GitHub