Skip to main content
GameDev.net gamedev.net

Procedural Clouds

by GameDev.net · GLSL ES 3.00 (WebGL2) · 25 Aug 2026

Use in your engine

Run the shader to adjust these controls.

What it demonstrates

Layered noise on its own is a grey haze. Clouds appear when that density field is thresholded into shape and shaded by how far past the threshold each fragment sits. This shader separates the three decisions a cloud effect actually needs: how much sky is covered, how hard the edges are, and how fast the field moves.

Source Revision 1

Author notes are linked to specific lines.

Shader inputs

void mainImage(out vec4 fragColor, in vec2 fragCoord)

Called once per pixel. Write the colour to fragColor.

iResolution vec3
Viewport size in pixels (z is the pixel aspect ratio).
iTime float
Seconds since the shader started.
iTimeDelta float
Seconds since the previous frame.
iFrameRate float
Frames per second, smoothed.
iFrame int
Frames rendered since the start.
iMouse vec4
Mouse position: xy while held, zw of the last click.
iDate vec4
Year, month, day, and seconds within the day.
iChannel0 sampler2D
Texture bound to channel 0.
iChannel1 sampler2D
Texture bound to channel 1.
iChannel2 sampler2D
Texture bound to channel 2.
iChannel3 sampler2D
Texture bound to channel 3.
iChannelResolution vec3[4]
Pixel size of each bound channel texture.
iChannelTime float[4]
Playback time of each channel, in seconds.
iSampleRate float
Audio sample rate, always 44100.
Main Image7
1 uniform float uCoverage; // @param 0.0..1.0 = 0.55 "Coverage"
2 uniform vec2 uWind; // @param -1.0..1.0 = 0.30, 0.06 "Wind"
3 uniform float uSoftness; // @param 0.02..0.40 = 0.16 "Edge softness"
4 uniform float uScale; // @param 1.0..8.0 = 2.5 "Cloud scale"
5
6 float hash21(vec2 cell) {
7 return fract(sin(dot(cell, vec2(12.9898, 78.233))) * 43758.5453);
8 }
9
10 float valueNoise(vec2 p) {
11 vec2 cell = floor(p);
12 vec2 local = fract(p);
13 vec2 weight = local * local * (3.0 - 2.0 * local);
14 float bottom = mix(hash21(cell), hash21(cell + vec2(1.0, 0.0)), weight.x);
15 float top = mix(hash21(cell + vec2(0.0, 1.0)), hash21(cell + vec2(1.0, 1.0)), weight.x);
16 return mix(bottom, top, weight.y);
17 }
18
19 float fbm(vec2 p) {
20 float sum = 0.0;
21 float amplitude = 1.0;
22 float normalization = 0.0;
23 for (int i = 0; i < 6; i++) {
24 sum += amplitude * valueNoise(p);
25 normalization += amplitude;
26 amplitude *= 0.5;
27 p *= 2.0;
28 }
29 return sum / normalization;
30 }
31
32 void mainImage(out vec4 fragColor, in vec2 fragCoord) {
33 vec2 uv = fragCoord / iResolution.xy;
34 vec2 p = vec2(uv.x * iResolution.x / iResolution.y, uv.y) * uScale;
35 vec2 drift = uWind * iTime * 0.06;
36 float base = fbm(p + drift);
37 float detail = fbm(p * 2.3 + drift * 1.7 + 5.0);
38 float density = mix(base, detail, 0.35);
39 float threshold = 1.0 - uCoverage;
40 float mask = smoothstep(threshold - uSoftness, threshold + uSoftness, density);
41 float lit = smoothstep(threshold, threshold + 0.30, density);
42 vec3 sky = mix(vec3(0.66, 0.80, 0.94), vec3(0.16, 0.34, 0.70), uv.y);
43 vec3 cloud = mix(vec3(0.52, 0.55, 0.64), vec3(1.0, 0.99, 0.96), lit);
44 vec3 color = mix(sky, cloud, mask);
45 fragColor = vec4(color, 1.0);
46 }
47

Learn from this shader

How it works

Two fbm evaluations are combined, the second at higher frequency and drifting faster, so silhouette and surface detail move at different rates instead of reading as one flat texture sliding past. Coverage is applied as a threshold rather than a brightness change: density is compared against one minus coverage, so raising coverage lowers the bar a fragment must clear to be cloud. Softness is the width of that comparison, which separates hard cartoon shapes from a diffuse overcast sky. Shading uses a second, wider comparison against the same threshold, so the brightest white sits deep inside the body and thin edges stay grey.

Try changing

Drive Coverage from zero to one and watch cloud grow from isolated wisps to closed overcast without the pattern changing shape. Set Edge softness low for a stylised look and high for atmospheric haze. Change Wind to move the field diagonally, and note the two layers separating as it does. In a fork, add a third slower layer for a horizon band.

Using it in a game

This is a skybox or backdrop material, not volumetric cloud: there is no light scattering through depth here. It suits stylised skies, fog planes, and minimap overlays. Feed the mask into alpha for a transparent cloud plane rather than compositing against a sky colour inside the shader.

Explore the techniques

Continue with curated explanations and progressively related examples.

LicenseMIT
Views0
Forks0

Discussion

Loading comments...