Advanced shaders · lesson 7 of 19 · Advanced · about 5 min

Clouds 3: light and shadow inside the cloud

You will build: Light the cloud: for every step inside the cloud, sample the density towards the sun to get a shadow, and add the lit color to the pixel.

The picture produced by the shader in the lesson "Clouds 3: light and shadow inside the cloud"
The target picture. In the lesson you write the shader that draws it, and a match bar shows how close you are.

A flat white cloud looks like a cutout. What makes clouds look deep is that the light also has to travel through them: the side facing the sun is bright, the inside and the far side are in shadow, and thin edges glow.

To know how much sunlight reaches a point inside the cloud, we ask the density once more, a little way towards the sun: sh = dens(pos + sun * 0.5). If there is lots of cloud between the point and the sun, sh is large and the point is dark. This is a cheap approximation of a real light march (the real thing takes 6-8 samples; one is enough to look good).

The color of the light at that point is a blend from a cool, bluish shadow color to warm white: lerp(shadow, sunlit, saturate(1.0 - sh * 1.5)).

Now each step adds light as well as blocking it. A step's contribution to the pixel is its color times the light that survived to reach the camera (T) times how much it blocks (a):

float sh = dens(pos + sun * 0.5);
float3 lit = lerp(float3(0.5, 0.6, 0.78), float3(1.0, 0.97, 0.9), saturate(1.0 - sh * 1.5));
float a = 1.0 - exp(-d * 0.4);
L += T * a * lit;          // light collected on this step
T *= 1.0 - a;              // and it blocks what is behind
// at the end:  col = sky * T + L;

Skipping the density-towards-the-sun lookup in empty air (if (d > 0.01)) keeps this fast: most steps are in clear sky.

The numbers you need

Everything the picture depends on is given, so the challenge is the code, not guessing.

Setup
as in Clouds 2, but the sun is sun = normalize(float3(0.5, 0.7, 0.4))
Skip empty air
only if d > 0.01
Shadow
sh = dens(pos + sun * 0.5)
Light color
lit = lerp(float3(0.5, 0.6, 0.78), float3(1.0, 0.97, 0.9), saturate(1.0 - sh * 1.5))
Step
a = 1.0 - exp(-d * 0.4); L += T * a * lit; T *= 1.0 - a (L starts at float3(0.0, 0.0, 0.0))
Final color
sky * T + L

Try it in your own language

The lesson is written once and shown in every language. Pick yours: Unity (Built-in), Unity (URP), Unreal, Shadertoy / WebGL, OpenGL (LWJGL, raylib), three.js, GameMaker, LÖVE (Lua), Godot, Apple (iOS, macOS), WebGPU / Bevy.

Start coding