Heartbeat damage vignette
Health drops from 100% to 10% on both halves of this walk.
Left: the red edge follows health in a straight line.
Right: it beats. Lub-dub, faster and deeper as health falls, from 72 to 150 BPM.
The problem
A red vignette that just fades in with damage tells you health is low once. After that it's a static frame and the eye stops reading it. A pulse keeps saying it, and the pulse rate itself carries the number.
How it works
The base is three.js VignetteShader (Eskil's vignette):
uv = (vUv − 0.5) × offset
weight = dot(uv, uv)
At offset 1 the corners reach 0.5 and the screen centre stays 0. A bigger offset pushes the edge inward. The stock shader mixes toward grey; I mix toward a dark red.
Three inputs drive it, with I = 1 − health:
- Heart rate: BPM = 72 + 78 × (I / 0.9)^1.3. That gives 72 at full health and 150 at 10%.
- Beat curve: two peaks per beat. "Lub" rises in about 25 ms and decays over 70 ms. "Dub" is 0.6 as tall and comes 0.3 × √period later: 0.27 s at 72 BPM, 0.19 s at 150.
- The vignette: offset = 1 + 1.3·I + 0.5·I·pulse, strength = 0.9·I × (0.65 + 0.6·pulse), and 0.55·I desaturation.
What I hit
Heart rate changes every time health drops. If the beat is sin(2π · BPM/60 · t), each change jumps the phase and the screen skips a beat. I integrate BPM/60 over time in a 1 ms table instead. A rate change only bends the curve.
My first decay was 85 ms. At 150 BPM the dub disappeared into the lub's tail, and the waveform showed one blunt bump per beat. With 70 ms and 50 ms decays both peaks stay readable at the top rate.
My build
three.js 0.170, one ShaderPass after tone mapping, so the red is a screen colour and the scene lights don't shift it. A dusk colonnade and plaza, all procedural, with the camera walking 18 m. Health drops in 6 hits over 9 s. The left half runs the same pass with offset = 1 + 1.3·I, strength = 0.9·I, no beat and no desaturation.
Trade-offs
- One full-screen pass: a dot product and a mix per pixel
- Everything comes from 1 number (health), easy to wire to gameplay
- A screen effect, so it covers the corners where HUD and enemies often are
- A constant beat can tire players; cap it or fade it after a while
- Red alone fails for colour-blind players; the pulse and desaturation help
When to use it
Action and survival games where the player should feel low health without looking at a bar. Give it an accessibility toggle.
Source and credits
Repo: github.com/mrdoob/three.js (MIT license), branch dev, commit fa01dd6
Code: the vignette is three.js examples/jsm/shaders/VignetteShader.js (Eskil's vignette: uv = (vUv − 0.5) × offset, weight = dot(uv, uv)), run through examples/jsm/postprocessing/ShaderPass.js. Changes: it mixes toward dark red instead of grey, a strength multiplier, desaturation, and offset/strength driven by health and a heartbeat curve. The heartbeat curve, BPM mapping, scene and HUD are my own. No external assets.