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

Smoke grenade 1: a ball of smoke

You will build: Draw the expanding smoke ball: noisy-edged density, marched inside its bounding sphere, evenly grey, in front of a sky gradient.

The picture produced by the shader in the lesson "Smoke grenade 1: a ball of smoke"
The target picture. In the lesson you write the shader that draws it, and a match bar shows how close you are.

The smoke grenade in a shooter is a perfect volume-rendering exercise: a dense grey ball that blooms out of nothing, with fluffy edges you can see the sky through. We build it in three steps, and the last one adds bullet holes.

Start with the density of the ball. Take a sphere (R - r is positive inside and grows towards the middle) and erode its edge with noise, so the surface is cloudy instead of smooth: (R - r) * 1.2 + (fbm3(...) - 0.5) * 1.3. The radius R grows with time as 1.7 * (1 - exp(-time * 4)): fast at first, then it settles, like the grenade bursting open.

The ray only needs to be marched inside the ball. Intersect the ray with a bounding sphere (the usual quadratic: b = dot(ro - c, rd), disc = b*b - c), and spend all 44 steps between the two crossing points. Rays that miss it skip the loop entirely. That is a big saving.

float b = dot(ro - c0, rd);
float cc = dot(ro - c0, ro - c0) - 1.9 * 1.9;
float disc = b * b - cc;                  // > 0 means the ray crosses the bounding sphere
if (disc > 0.0)
{
    float s = sqrt(disc);
    float t = -b - s;                     // where it enters
    float dt = 2.0 * s / 44.0;            // 44 steps across the sphere
    // ...
}

This first version is lit evenly: every step is the same light grey. Behind the smoke is a sunset sky.

The numbers you need

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

Density
baseD(p): R = 1.7 * (1.0 - exp(-time * 4.0)); r = length(p - float3(0.0, 0.25, 0.0)); d = (R - r) * 1.2 + (fbm3(p * 1.6 + float3(0.0, time * 0.12, 0.0)) - 0.5) * 1.3; return saturate(d * 2.5) * smoothstep(-1.0, -0.8, p.y)
Camera
ro = float3(0.0, 0.5, -4.5), rd = normalize(float3(p.x, p.y - 0.12, 1.6)), p = (uv - 0.5) * float2(aspect, 1.0) * 2.0
Bounding sphere
center float3(0.0, 0.25, 0.0), radius 1.9: b = dot(ro - c0, rd); cc = dot(ro - c0, ro - c0) - 1.9 * 1.9; disc = b * b - cc
Steps
s = sqrt(disc); t = -b - s; dt = 2.0 * s / 44.0; pos = ro + rd * (t + dt * (float(i) + 0.5)) for i = 0 to 43
Each step
if d > 0.01: a = 1.0 - exp(-d * dt * 6.0); L += T * a * float3(0.8, 0.8, 0.8); T *= 1.0 - a (T = 1.0, L = black before the loop)
Backdrop
lerp(float3(1.0, 0.82, 0.55), float3(0.35, 0.58, 0.9), saturate(rd.y * 1.6 + 0.15))
Result
backdrop * 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

Go deeper

Part of the learning path: Game effects: smoke, fire and snow.