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 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 codingGo deeper
Part of the learning path: Game effects: smoke, fire and snow.