Camera Shake or "Much Damage, Such WoW"

I was watching a GDC talk about camera shake and figured it would be a fun thing to implement the camera shake. Enjoy :slight_smile:

http://server1.lazy-kitty.com/tests/camera_shake_2019_08_09/

image

3 Likes

Is your implementation Open Source?
It just occured that there is an explosion in my tunnel now which causes a light to fall down. I think a little camera shake goes nicely with that :slightly_smiling_face:

Hey @weiserhei, glad that you’re interested. For now my engine is closed source. However, I plan to release it as open-source eventually. If I gave you the code - you would probably have to spend more time to adapt it than to write a shake from scratch yourself. That being said, here’s the relevant piece of code that might give you some pointers:

import Vector3 from "../../core/geom/Vector3.js";

import SimplexNoise from 'simplex-noise';
import { clamp, makeCubicCurve, seededRandom } from "../../core/math/MathUtils.js";
import { Behavior } from "../../engine/intelligence/behavior/Behavior.js";
import { BehaviorStatus } from "../../engine/intelligence/behavior/BehaviorStatus.js";
import Quaternion from "../../core/geom/Quaternion.js";

export class CameraShakeTraumaBehavior extends Behavior {

    /**
     *
     * @param {CameraShakeBehavior} shakeBehavior
     * @param {number} decay amount by which trauma decays per second
     */
    constructor({
                    shakeBehavior,
                    decay = 1,
                }) {
        super();


        this.decay = decay;
        this.trauma = 0;

        this.shakeBehavior = shakeBehavior;

        this.formula = makeCubicCurve(0, 0.1, 0.1, 1);
    }

    tick(timeDelta) {
        const shake = this.formula(clamp(this.trauma, 0, 1));

        this.trauma = clamp(this.trauma - timeDelta * this.decay, 0, 1);


        this.shakeBehavior.strength = shake;

        return BehaviorStatus.Running;
    }
}

export class CameraShakeBehavior extends Behavior {
    /**
     *
     * @param {number} maxPitch
     * @param {number} maxYaw
     * @param {number} maxRoll
     * @param {number} maxOffsetX
     * @param {number} maxOffsetY
     * @param {number} maxOffsetZ
     * @param {number} strength
     * @param {TopDownCameraController} controller
     */
    constructor(
        {
            maxPitch = 0,
            maxYaw = 0,
            maxRoll = 0,
            maxOffsetX = 0,
            maxOffsetY = 0,
            maxOffsetZ = 0,
            strength = 0,

            controller
        }
    ) {
        super();

        /**
         *
         * @type {TopDownCameraController}
         */
        this.controller = controller;

        this.time = 0;

        this.timeScale = 1;

        this.strength = strength;

        this.shake = new CameraShake();

        this.shake.limitsRotation.set(maxPitch, maxYaw, maxRoll);
        this.shake.limitsOffset.set(maxOffsetX, maxOffsetY, maxOffsetZ);

        this.__target = new Vector3();
        this.__rotation = new Vector3();
    }

    initialize() {
        super.initialize();

        //remember controller transform
        this.__rotation.set(this.controller.pitch, this.controller.yaw, this.controller.roll);
        this.__target.copy(this.controller.target);
    }

    tick(timeDelta) {
        this.time += timeDelta * this.timeScale;

        const offset = new Vector3();
        const rotation = new Vector3();

        //read out shake values
        this.shake.read(this.strength, this.time, offset, rotation);

        const q = new Quaternion();

        q.fromEulerAngles(this.__rotation.x, this.__rotation.y, this.__rotation.z);

        offset.applyQuaternion(q);

        //update controller
        this.controller.target.set(
            this.__target.x + offset.x,
            this.__target.y + offset.y,
            this.__target.z + offset.z,
        );

        this.controller.pitch = this.__rotation.x + rotation.x;
        this.controller.yaw = this.__rotation.y + rotation.y;
        this.controller.roll = this.__rotation.z + rotation.z;

        return BehaviorStatus.Running;
    }
}

/**
 * Based on a 2016 GDC talk by Squirrel Eiserloh "Math for Game Programmers: Juicing Your Cameras With Math"
 */
export class CameraShake {
    constructor() {


        this.time = 0;

        /**
         * Shake rotational limits, yaw, pitch and roll
         * @type {Vector3}
         */
        this.limitsRotation = new Vector3();

        /**
         * Shake offset limits
         * @type {Vector3}
         */
        this.limitsOffset = new Vector3();


        const r = seededRandom(1);

        this.noiseRotataion = new SimplexNoise(r);
        this.noiseOffset = new SimplexNoise(r);

    }

    /**
     *
     * @param {number} value between 0 and 1
     * @param {number} time
     * @param {Vector3} offset
     * @param {Vector3} rotation
     */
    read(value, time, offset, rotation) {

        const t = time;

        const nR = this.noiseRotataion;

        rotation.set(
            this.limitsRotation.x * value * (nR.noise2D(t, 1) * 2 - 1),
            this.limitsRotation.y * value * (nR.noise2D(t, 2) * 2 - 1),
            this.limitsRotation.z * value * (nR.noise2D(t, 3) * 2 - 1),
        );

        const nO = this.noiseOffset;

        offset.set(
            this.limitsOffset.x * value * (nO.noise2D(t, 1) * 2 - 1),
            this.limitsOffset.y * value * (nO.noise2D(t, 2) * 2 - 1),
            this.limitsOffset.z * value * (nO.noise2D(t, 3) * 2 - 1)
        );
    }


}
1 Like

Or you could just use a Sine wave on the camera’s rotational axes if I recall correctly. Please correct me if I’m wrong. :slight_smile:

Yeah, you can. Noise is better because it is not regular, like a sine function. There are a ton of simplex/perlin/whatever noise NPM packages, just have a look and pick one you like. I used simplex-noise package. It’s quite small and has a robust implementation.

1 Like

Thanks @Usnul, thats some quality code! Although I cant just drop it in, which means its going on the todo list :grimacing:

1 Like