camera shake implementation: why does everyone's version feel slightly wrong and how do I fix mine

278 views 3 replies

spent the last few days ripping out my camera shake system and rebuilding it from scratch because something about it just felt off and I couldn't articulate why for weeks.

the original was the classic additive approach: trauma float from 0–1, perlin noise sampled at trauma * shakeFrequency intensity, applied directly to camera position/rotation offsets each frame. it worked but felt weirdly disconnected from the game, like the camera was doing its own thing separate from the physics of what was happening.

the thing I figured out: most naive shake implementations are frame-rate dependent in subtle ways even when you think they're not. if you're multiplying by Time.deltaTime in some places but not others you get this thing where the shake feels loose on high refresh monitors and stiff on 60hz. drove me nuts for days.

what actually helped was switching to a spring-based approach. instead of directly setting the offset, you push the camera to a target offset with an impulse and let a damped spring system resolve it back to zero. the spring parameters (stiffness, damping) naturally control how snappy vs floaty it feels, and the whole thing is inherently frame-rate independent if your spring integration is correct.

// rough idea, not copy-paste ready
springVelocity += (-springPosition * stiffness - springVelocity * damping) * deltaTime;
springPosition += springVelocity * deltaTime;
cameraOffset = springPosition * traumaIntensity;

also stacking multiple trauma sources additively with individual decay rates makes big hits and small hits feel different without any extra logic. satisfying physics simulation

the Perlin noise approach isn't wrong but the spring method feels way more grounded, like the camera has actual mass. curious if anyone else went down this road or if you're doing something totally different. also anyone using the Cinemachine Impulse system and finding it flexible enough, or does it always end up being too constrained for custom feel?

developer overthinking simple problem

tbh the single thing that fixed my camera shake was switching from random offset each frame to sampling perlin noise and incrementing through it over time. random offsets snap toward zero unpredictably because there's no continuity between samples — your brain expects organic motion and gets slot machine output instead.

var noise := FastNoiseLite.new()
var trauma: float = 0.0
var noise_t: float = 0.0
const SHAKE_SPEED = 50.0
const MAX_OFFSET = 20.0

func add_trauma(amount: float) -> void:
    trauma = min(trauma + amount, 1.0)

func _process(delta: float) -> void:
    trauma = max(trauma - delta * 0.8, 0.0)
    noise_t += delta * SHAKE_SPEED
    var shake := pow(trauma, 2.0)
    $Camera2D.offset = Vector2(
        noise.get_noise_2d(noise_t, 0.0) * shake * MAX_OFFSET,
        noise.get_noise_2d(noise_t, 100.0) * shake * MAX_OFFSET
    )

also: stack trauma, don't replace it. multiple hits in quick succession should compound. once i did both of these things my shake went from 'slightly off' to 'actually feels right' immediately.

satisfying game screen shake juicy

something that helped me that i don't see come up: apply shake to a child transform of the camera rig, not the camera itself. if your camera has any smoothing or follow logic running on it, shaking the camera directly means the shake gets partially absorbed by the smoothing, especially noticeable during fast movement where the follow system is actively correcting. putting the shake on a dedicated child node that the smoothing ignores means it hits at full intensity regardless of what the parent camera is doing.

also ended up splitting trauma into two channels, positional and rotational, with separate decay rates. the rotation lingers slightly longer and it feels more physical somehow. probably placebo but the before/after was pretty clear in playtesting.

Replying to StormLark: tbh the single thing that fixed my camera shake was switching from random offset...

yep, perlin sampling was the fix for me too. the other thing that made a massive difference: non-linear trauma decay. instead of linear falloff, ease out with a square or cube function so the intensity drops off naturally rather than just shrinking at a fixed rate. combine that with perlin-sampled offsets and the whole thing starts feeling like something a camera op actually did on purpose rather than a random number generator.

also worth keeping trauma and displacement on separate axes. don't tie the rotation and translation magnitudes together. a heavy impact might want a lot of translational shake but very little rotation, and vice versa for a distant explosion. separating them gives you way more expressive range for basically no extra complexity.

Moonjump
Forum Search Shader Sandbox
Sign In Register