Game Feel · Camera · common
Screen Shake
add trauma on impact, shake by trauma squared, let it decay
Trauma-based screen shake: hits add trauma (0–1) that decays over time, and the camera is offset by trauma² × smooth noise for impact.
The cyan player takes a quick double hit, then a single one, on a loop. The dashed frame stays still while the world shakes inside it. The meter on the right shows trauma (yellow) and the resulting shake (orange, trauma²). Turn off squaring to see small trauma wobble too much, or switch to random noise to see the jitter it causes.
What it is
Screen shake sells impact: explosions, heavy landings, big hits. Shaking by a random offset each frame looks jittery, and stacking several shakes is hard to control. A cleaner model keeps a single trauma value between 0 and 1. Events add to it (a small hit +0.2, an explosion +0.6), it decays linearly over time, and the shake each frame is derived from it.
Use shake = trauma² (or ³). Squaring makes small trauma nearly invisible and large trauma violent, so light hits stay subtle and stacked hits escalate. Multiply the shake by a maximum offset and angle, and by smooth noise sampled over time, one independent channel per axis. Noise gives continuous motion instead of teleporting the camera every frame.
Apply the result as an offset on top of the camera's normal position (after the deadzone follow), then remove it next frame. Rotation (roll) shake reads strongly in 2D; in 3D small yaw and pitch also work. Offer a setting to reduce or disable shake for accessibility.
Key formulas
- Trauma update
- Offset per axis
N is smooth noise in −1..1 with an independent seed per axis; f is the shake frequency (10–25 Hz).
Rules of thumb
- Trauma per event: 0.1–0.3 for small hits, 0.5–0.8 for explosions. Decay 0.8–1.5 per second.
- Max offset about 1–3% of the view height; max roll 2–5°.
- Noise frequency 10–25 Hz. Lower feels like rumble, higher like vibration.
- Always offer a shake intensity option, including off.
Implementation
const MAX_OFFSET = 0.3, MAX_ROLL = THREE.MathUtils.degToRad(3)
const DECAY = 1.0, FREQ = 15
let trauma = 0, time = 0 // noise1D: any smooth 1D noise in -1..1
export const addTrauma = (amount: number) =>
(trauma = Math.min(1, trauma + amount))
function updateShake(dt: number, base: THREE.Vector3, settings = 1) {
time += dt
trauma = Math.max(0, trauma - DECAY * dt)
const shake = trauma * trauma * settings
const t = time * FREQ
camera.position.copy(base) // follow result, never mutated
camera.position.x += MAX_OFFSET * shake * noise1D(t, 1)
camera.position.y += MAX_OFFSET * shake * noise1D(t, 2)
camera.rotation.z = MAX_ROLL * shake * noise1D(t, 3)
}Common mistakes
- Using
Math.random()per frame, which looks like jitter and depends on frame rate. - Adding the shake offset to the follow position permanently, so the camera drifts away.
- Shaking linearly with trauma, so every tiny hit wobbles the screen.
- Shaking for gameplay events the player does not cause, until it becomes noise and fatigue.
- No accessibility toggle. Strong shake causes discomfort for many players.
Prompt
Implement trauma-based camera shake in TypeScript for a Three.js game. Keep trauma in [0, 1]; addTrauma(amount) adds and clamps; each frame subtract decay * dt (default 1/s). Compute shake = trauma ** 2 times a user intensity setting, and offset the camera from its follow position by maxOffset * shake * noise(t * freq, seed) on x and y, plus roll maxRoll * shake * noise(t * freq, 3), using a smooth 1D value noise with a seed per channel. Restore the base transform before applying the offset each frame. Write Vitest tests: trauma from 0.6 reaches 0 after 0.6 s at 30 and 144 fps, it never exceeds 1, and the noise is continuous.
Related
Further reading
Updated 2026-11-01