Skip to content

Cloud Shaders

Shaders for cloud rendering including 2D procedural layers and full volumetric 3D clouds with ray marching.

import {
cloudLayerVertexShader,
cloudLayerFragmentShader,
volumetricCloudVertexShader,
volumetricCloudFragmentShader,
} from '@strata-game-library/shaders';

Efficient billboarded cloud layers:

UniformTypeDescription
uTimefloatAnimation time
uCloudColorvec3Cloud color
uSkyColorvec3Sky color for blending
uCloudScalefloatNoise scale
uCloudSpeedvec2Movement direction
uCloudCoveragefloatCloud coverage (0-1)
uCloudDensityfloatCloud opacity
import { cloudLayerVertexShader, cloudLayerFragmentShader } from '@strata-game-library/shaders';
const cloudMaterial = new THREE.ShaderMaterial({
vertexShader: cloudLayerVertexShader,
fragmentShader: cloudLayerFragmentShader,
uniforms: {
uTime: { value: 0 },
uCloudColor: { value: new THREE.Color(1, 1, 1) },
uSkyColor: { value: new THREE.Color(0.5, 0.7, 1) },
uCloudScale: { value: 0.001 },
uCloudSpeed: { value: new THREE.Vector2(0.01, 0.005) },
uCloudCoverage: { value: 0.5 },
uCloudDensity: { value: 0.8 },
},
transparent: true,
side: THREE.DoubleSide,
});
const clouds = new THREE.Mesh(
new THREE.PlaneGeometry(10000, 10000),
cloudMaterial
);
clouds.position.y = 500;
clouds.rotation.x = -Math.PI / 2;

Full 3D volumetric cloud rendering with ray marching:

UniformTypeDescription
uTimefloatAnimation time
uCameraPosvec3Camera position
uSunDirectionvec3Sun direction
uSunColorvec3Sun light color
uAmbientColorvec3Ambient sky color
uCloudMinfloatCloud layer bottom
uCloudMaxfloatCloud layer top
uDensityfloatCloud density
uCoveragefloatCloud coverage
uStepsintRay march steps
uLightStepsintLight scattering steps
import { volumetricCloudVertexShader, volumetricCloudFragmentShader } from '@strata-game-library/shaders';
const volumetricCloudMaterial = new THREE.ShaderMaterial({
vertexShader: volumetricCloudVertexShader,
fragmentShader: volumetricCloudFragmentShader,
uniforms: {
uTime: { value: 0 },
uCameraPos: { value: camera.position },
uSunDirection: { value: new THREE.Vector3(0.5, 0.8, 0.2).normalize() },
uSunColor: { value: new THREE.Color(1, 0.95, 0.8) },
uAmbientColor: { value: new THREE.Color(0.5, 0.6, 0.8) },
uCloudMin: { value: 1000 },
uCloudMax: { value: 3000 },
uDensity: { value: 0.05 },
uCoverage: { value: 0.5 },
uSteps: { value: 64 },
uLightSteps: { value: 8 },
},
transparent: true,
});
float fbm(vec3 p, int octaves) {
float value = 0.0;
float amplitude = 0.5;
float frequency = 1.0;
for (int i = 0; i < octaves; i++) {
value += amplitude * noise(p * frequency);
amplitude *= 0.5;
frequency *= 2.0;
}
return value;
}
float cloudDensity(vec3 p) {
// Remap height to 0-1 within cloud layer
float heightFrac = (p.y - uCloudMin) / (uCloudMax - uCloudMin);
// Height falloff (thicker at bottom, whispy at top)
float heightMod = smoothstep(0.0, 0.2, heightFrac) * smoothstep(1.0, 0.8, heightFrac);
// Base cloud shape
float baseCloud = fbm(p * 0.0003 + uTime * 0.01, 4);
// Detail noise
float detail = fbm(p * 0.003, 3) * 0.3;
// Combine
float density = (baseCloud + detail) * heightMod;
// Apply coverage
density = smoothstep(1.0 - uCoverage, 1.0, density);
return density * uDensity;
}
float lightMarch(vec3 position) {
vec3 lightDir = uSunDirection;
float stepSize = (uCloudMax - position.y) / float(uLightSteps);
float totalDensity = 0.0;
for (int i = 0; i < uLightSteps; i++) {
position += lightDir * stepSize;
totalDensity += cloudDensity(position) * stepSize;
}
// Beer-Lambert law
return exp(-totalDensity);
}
float henyeyGreenstein(float cosTheta, float g) {
float g2 = g * g;
return (1.0 - g2) / (4.0 * PI * pow(1.0 + g2 - 2.0 * g * cosTheta, 1.5));
}
// Cumulus (puffy)
uniforms.uCoverage.value = 0.4;
uniforms.uDensity.value = 0.08;
// Stratus (flat layers)
uniforms.uCoverage.value = 0.7;
uniforms.uDensity.value = 0.03;
// Stormy
uniforms.uCoverage.value = 0.9;
uniforms.uDensity.value = 0.15;
uniforms.uAmbientColor.value.set(0.3, 0.3, 0.4);