Advanced shaders · lesson 10 of 19 · Advanced · about 5 min
Smoke grenade 3: bullet holes
You will build: Carve four bullet tunnels into the smoke: each is a line fired at its own time, with a radius that shrinks as the smoke fills it. Light must come through.
The effect everyone remembers from smoke grenades: shoot into the smoke and a tunnel opens up, with the bright world visible through it. Then the smoke slowly closes the hole again.
A bullet is a straight line: it starts at a point o (the gun) and flies in direction dir. The distance from any point p in space to that line is a one-liner: length(cross(p - o, dir)) (with dir normalized). A tunnel is simply every point closer to the line than some radius.
To carve it, multiply the density by a number that is 0 inside the tunnel and 1 outside: d *= smoothstep(rh * 0.4, rh, dist). The smoothstep gives the hole a soft wall.
A shader has no memory, so how does it know when each shot happened? It does not need to: shots are a function of time. Shot number i is fired at 0.3 + i * 0.5 seconds, so its age is time - that. If the age is negative the shot has not happened yet. The hole's radius shrinks with age, rh = 0.34 * exp(-age * 0.4), so the smoke fills it back in.
The four bullets start at the camera (you shoot from where you stand) and aim at four different points on the smoke ball, spread by a few sines of i.
for (int i = 0; i < 4; i++)
{
float fi = float(i);
float age = time - (0.3 + fi * 0.5);
if (age > 0.0)
{
float2 hit = float2(0.7 * sin(fi * 2.4 + 0.5), 0.25 + 0.45 * sin(fi * 1.9));
float3 o = float3(0.0, 0.5, -4.5) + float3(0.15 * cos(fi * 3.0), 0.08 * sin(fi * 2.0), 0.0);
float3 dir = normalize(float3(hit, 0.0) - o);
float dist = length(cross(p - o, dir));
float rh = 0.34 * exp(-age * 0.4);
d *= smoothstep(rh * 0.4, rh, dist);
}
}The numbers you need
Everything the picture depends on is given, so the challenge is the code, not guessing.
- smokeD(p)
starts with d = baseD(p), then a loop over 4 bullets, then returns d- Shot i
fi = float(i); age = time - (0.3 + fi * 0.5); only if age > 0.0- Aim point
hit = float2(0.7 * sin(fi * 2.4 + 0.5), 0.25 + 0.45 * sin(fi * 1.9))- Start
o = float3(0.0, 0.5, -4.5) + float3(0.15 * cos(fi * 3.0), 0.08 * sin(fi * 2.0), 0.0)- Direction
dir = normalize(float3(hit, 0.0) - o)- Distance to the bullet line
dist = length(cross(p - o, dir))- Hole
rh = 0.34 * exp(-age * 0.4); d *= smoothstep(rh * 0.4, rh, dist)- Use it
the march uses smokeD(pos) for the density; the shadow lookup still uses baseD
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