Shader Example: Raymarching Cloud
Live Demo
Fragment Shader
Edit the shader and press Run (or Ctrl+Enter) to recompile. The source below the demo is the same text the playground is running.
Snippets
Shadertoy
// Shadertoy: 3D Raymarched Cloud Example
// iResolution, iTime, iMouse available
//
// Hold the left mouse button and drag to orbit: left and right turns, up and
// down moves the camera above or below the deck.
#define MAX_STEPS 112
#define LIGHT_STEPS 6
#define FAR 90.0
#define PI 3.14159265359
// The slab the cloud layer occupies. The march is clipped to it, so steps are
// spent inside the cloud rather than in the empty air in front of it.
#define CLOUD_BOTTOM -0.6
#define CLOUD_TOP 3.0
float hash(vec3 p)
{
p = fract(p * 0.3183099 + 0.1);
p *= 17.0;
return fract(p.x * p.y * p.z * (p.x + p.y + p.z));
}
float noise(vec3 x)
{
vec3 i = floor(x);
vec3 f = fract(x);
f = f * f * (3.0 - 2.0 * f);
float n000 = hash(i + vec3(0,0,0));
float n100 = hash(i + vec3(1,0,0));
float n010 = hash(i + vec3(0,1,0));
float n110 = hash(i + vec3(1,1,0));
float n001 = hash(i + vec3(0,0,1));
float n101 = hash(i + vec3(1,0,1));
float n011 = hash(i + vec3(0,1,1));
float n111 = hash(i + vec3(1,1,1));
return mix(
mix(mix(n000, n100, f.x), mix(n010, n110, f.x), f.y),
mix(mix(n001, n101, f.x), mix(n011, n111, f.x), f.y),
f.z
);
}
float fbm(vec3 p)
{
float v = 0.0;
float a = 0.5;
for (int i = 0; i < 5; i++)
{
v += a * noise(p);
p *= 2.02;
a *= 0.5;
}
return v;
}
float cloudDensity(vec3 p)
{
// Move clouds over time
p.xz += vec2(iTime * 0.6, iTime * 0.2);
// The large shape decides both where there is cloud and how tall it stands.
// Using it only as a density threshold inside a fixed slab gives an even
// blanket with a flat top - which is what this did, and why side light
// produced no shadows: there was nothing standing up to cast one.
float shape = fbm(p * 0.32);
float coverage = smoothstep(0.40, 0.72, shape);
if (coverage <= 0.001) return 0.0;
// Dense columns tower; thin ones stay low.
float topY = mix(0.5, CLOUD_TOP, coverage);
float heightMask = smoothstep(CLOUD_BOTTOM, 0.3, p.y)
* (1.0 - smoothstep(topY - 1.0, topY, p.y));
if (heightMask <= 0.001) return 0.0;
// Detail erodes the edges, where coverage is still climbing, and leaves the
// dense core alone so it does not dissolve into noise.
float detail = fbm(p * 1.45 + 11.0);
float d = coverage - 0.42 * detail * (1.0 - coverage);
return clamp(d * heightMask * 2.0, 0.0, 1.0);
}
// Henyey-Greenstein. Cloud droplets scatter strongly forwards, which is what
// puts the bright rim on a cloud seen against the sun. Without a phase function
// every part of the cloud is lit the same from every direction and it reads
// flat however good the shadowing is.
float hg(float cosTheta, float g)
{
float g2 = g * g;
return (1.0 - g2) / (4.0 * PI * pow(max(1.0 + g2 - 2.0 * g * cosTheta, 1e-4), 1.5));
}
// Optical depth from a point towards the sun. Steps grow, so the near field -
// where self-shadowing actually shows - is sampled finely and the far field
// cheaply. Weighting each sample by its own step length is what makes this a
// depth rather than an average.
float lightDepth(vec3 pos, vec3 lightDir)
{
float depth = 0.0;
float t = 0.15;
for (int i = 0; i < LIGHT_STEPS; i++)
{
depth += cloudDensity(pos + lightDir * t) * t;
t *= 1.9;
}
return depth;
}
mat3 camera(vec3 ro, vec3 ta)
{
vec3 f = normalize(ta - ro);
vec3 r = normalize(cross(vec3(0,1,0), f));
vec3 u = cross(f, r);
return mat3(r, u, f);
}
void mainImage(out vec4 fragColor, in vec2 fragCoord)
{
vec2 uv = (fragCoord - 0.5 * iResolution.xy) / iResolution.y;
vec3 lightDir = normalize(vec3(0.82, 0.26, 0.28));
// Above the deck, looking down across it. The sun rakes the tops and the
// gaps between billows fall into shadow, which is where the shape comes
// from. The camera used to sit at y = 1.5, inside a layer spanning -1 to
// 2.8, so every ray ran the same length through the same soup and the whole
// frame came out an even grey.
vec3 ro = vec3(0.0, 3.7, -14.0);
vec3 ta = vec3(0.0, 2.9, 3.0);
if (iMouse.z > 0.0)
{
float mx = (iMouse.x / iResolution.x - 0.5) * 6.2831;
float my = iMouse.y / iResolution.y;
ro = vec3(13.0 * sin(mx), mix(-1.5, 9.0, my), -13.0 * cos(mx));
}
mat3 cam = camera(ro, ta);
vec3 rd = normalize(cam * vec3(uv, 1.5));
// Sky
float cosL = dot(rd, lightDir);
float sun = max(cosL, 0.0);
vec3 skyCol = mix(vec3(0.86, 0.87, 0.92), vec3(0.16, 0.36, 0.76),
clamp(rd.y * 2.4 + 0.10, 0.0, 1.0));
skyCol += vec3(1.0, 0.80, 0.55) * pow(sun, 200.0) * 1.6;
skyCol += vec3(1.0, 0.74, 0.48) * pow(sun, 6.0) * 0.16;
// A strong forward lobe with a weak backward one: the forward lobe is the
// silver lining, the backward lobe keeps the shadowed side from going dead.
float phase = mix(hg(cosL, 0.76), hg(cosL, -0.28), 0.3);
// Clip the march to the cloud slab.
float tEnter = 0.0;
float tExit = -1.0;
if (abs(rd.y) > 1e-4)
{
float tA = (CLOUD_TOP - ro.y) / rd.y;
float tB = (CLOUD_BOTTOM - ro.y) / rd.y;
tEnter = max(0.0, min(tA, tB));
tExit = min(FAR, max(tA, tB));
}
else if (ro.y > CLOUD_BOTTOM && ro.y < CLOUD_TOP)
{
tEnter = 0.0;
tExit = FAR;
}
float transmittance = 1.0;
vec3 accum = vec3(0.0);
if (tExit > tEnter)
{
float stepLen = max((tExit - tEnter) / float(MAX_STEPS), 0.05);
// Jitter the first sample, or the fixed step draws visible shells
// through the cloud where every ray crosses a boundary together.
float t = tEnter + hash(vec3(fragCoord, iTime)) * stepLen;
for (int i = 0; i < MAX_STEPS; i++)
{
if (t > tExit || transmittance < 0.01) break;
vec3 p = ro + rd * t;
float d = cloudDensity(p);
if (d > 0.005)
{
float sunT = exp(-lightDepth(p, lightDir) * 2.9);
// Powder. Light arriving at the outside of a clump has not been
// scattered yet, so an edge reads darker than the lit core
// behind it. Without it a cloud looks like a cutout.
float powder = 1.0 - exp(-d * 9.0);
vec3 sunLight = vec3(1.0, 0.88, 0.70) * sunT * (phase * 2.2 + 0.50) * powder;
vec3 ambient = mix(vec3(0.20, 0.26, 0.40), vec3(0.74, 0.82, 0.98),
smoothstep(CLOUD_BOTTOM, CLOUD_TOP, p.y)) * 0.30;
// Beer-Lambert over the distance actually travelled. This used a
// hardcoded 0.18 while the march stepped anywhere from 0.08 to
// 0.3, so the opacity never matched the geometry.
float alpha = 1.0 - exp(-d * 3.2 * stepLen);
accum += (sunLight + ambient) * alpha * transmittance;
transmittance *= (1.0 - alpha);
}
t += stepLen;
}
}
vec3 col = accum + skyCol * transmittance;
// Simple tonemap + gamma
col = col / (1.0 + col);
col = pow(col, vec3(0.4545));
fragColor = vec4(col, 1.0);
}