Need a "shakycam" for your demo but the good old sin-cos-mess just doesn't cut it anymore? Today's your lucky day!

This is a public release of the camera shake data and code used in the demo A New World Awaits.

At its core Somewhat Realistic Camera Shake Tracks is just

The library plays back the canned shake track animations via a simple interpolateTrack() function. The animations are roughly 1000 frames (30 seconds) long. They were motion tracked from real video using Blender and they include a realistic roll component in addition to simple x and y displacement you'd get out of your regular noise function.


still.bin onehand.bin
slowwalk.bin fastwalk.bin


The raw motion tracked files and a preprocessor script extract_poses.py are included for completeness.

Download code & data: somewhat_realistic_camera_shake_tracks_v5.zip:

File listing for the zip:


How to start it

Loading a single track with loadCameraShakeTrack():

CameraShakeTrack stillTrack;
bool success = loadCameraShakeTrack("shaketracks/still.bin", &stillTrack);

Or all the tracks in one go:

enum ShakeType {
    CAMSHAKE_STILL = 0,
    CAMSHAKE_ONE_HAND = 1,
    CAMSHAKE_SLOW_WALK = 2,
    CAMSHAKE_FAST_WALK = 3,
    CAMSHAKE_MAX = 4,
};

static CameraShakeTrack shakeTracks[CAMSHAKE_MAX];

bool loadAllTracks()
{
    bool shakeSuccess = true;
    shakeSuccess = shakeSuccess && loadCameraShakeTrack("shaketracks/still.bin", &shakeTracks[CAMSHAKE_STILL]);
    shakeSuccess = shakeSuccess && loadCameraShakeTrack("shaketracks/onehand.bin", &shakeTracks[CAMSHAKE_ONE_HAND]);
    shakeSuccess = shakeSuccess && loadCameraShakeTrack("shaketracks/slowwalk.bin", &shakeTracks[CAMSHAKE_SLOW_WALK]);
    shakeSuccess = shakeSuccess && loadCameraShakeTrack("shaketracks/fastwalk.bin", &shakeTracks[CAMSHAKE_FAST_WALK]);

    if (!shakeSuccess) {
        puts("failed to load camera shake data");
        return false;
    }

    return true;
}

How to drive it

Call interpolateTrack() :

CameraShakePoint shake = interpolateTrack(track, secs);

// now do something with shake.x, shake.y and shake.roll

Or animate a camera pose with the 'shakePose()' helper function:

// Assume we have a camera struct 'cam' with a normalized direction 'pose.dir' and
// basis vectors 'cam.right' and 'cam.up'.

void animate(Camera& cam, double time)
{
    CameraShakeTrack& track = slowWalkTrack;

    // Copy 'cam' into 'pose'.

    CameraShakePose pose;
    pose.dir    = { cam.dir.x,   cam.dir.y,     cam.dir.z };
    pose.up     = { cam.up.x,    cam.up.y,      cam.up.z };
    pose.right  = { cam.right.x, cam.right.y,   cam.right.z };

    float strength = 2.0; // change this

    // Twist 'pose' according to the given track.
    shakePose(track, time, strength, &pose);

    // Copy 'pose' back into 'cam'.
    cam.dir     = vec3(pose.dir.x,   pose.dir.y,    pose.dir.z);
    cam.up      = vec3(pose.up.x,    pose.up.y,     pose.up.z);
    cam.right   = vec3(pose.right.x, pose.right.y,  pose.right.z);
}

libshakez

Below are listings of the two files that make up the libshakez animation library.

camerashake.h

#pragma once

#include <vector>
#include <cstdint>

#pragma pack(push, 1)
struct CameraShakeHeader {
    char fourcc[4];
    uint32_t version;
    uint32_t num_frames;
    uint32_t frame_width; // in pixels
    uint32_t frame_height;
    float focal_length; // in mm
    float framerate;
};

struct CameraShakePoint {
    float x = 0.f; // displacement to right, in pixels
    float y = 0.f; // displacement upwards, in pixels
    float roll = 0.f; // roll in radians
    float cosine = 0.f; // roll expressed as a normalized direction vector
    float sine = 0.f;
};
#pragma pack(pop)

static_assert(sizeof(CameraShakePoint) == 5 * sizeof(float));

struct CameraShakeTrack {
    CameraShakeHeader hdr;
    std::vector<CameraShakePoint> points;
};

// A simple orthogonal camera basis used in 'shakePose()'.
struct CameraShakePose {
    struct Vec {
        float x, y, z;
    };

    Vec dir;
    Vec up;
    Vec right;
};

// Loads a binary camera shake track file into 'outTrack'.
// Returns true on success.
bool loadCameraShakeTrack(const char* path, CameraShakeTrack* outTrack);

// Linearly interpolates the given track at time 'secs'.
CameraShakePoint interpolateTrack(const CameraShakeTrack& track, double secs);

// Shakes the given camera pose with the interpolated animation. Modifies 'pose' in-place.
// The pose is expected to be in a right-handed coordinate space like in OpenGL.
// The parameter 'strength' should be in range [0, 10].
void shakePose(const CameraShakeTrack& track, double secs, double strength, CameraShakePose* pose);

camerashake.cpp

#include <cmath>
#include "camerashake.h"

#ifndef _CRT_SECURE_NO_WARNINGS
#define _CRT_SECURE_NO_WARNINGS
#endif

bool loadCameraShakeTrack(const char* path, CameraShakeTrack* outTrack)
{
    FILE* fp = fopen(path, "rb");

    if (!fp) {
        fprintf(stderr, "Couldn't load '%s'!\n", path);
        return false;
    }

    if (fread(&outTrack->hdr, sizeof(CameraShakeHeader), 1, fp) == 0) {
        fputs("Couldn't load camera shake header", stderr);
        fclose(fp);
        return false;
    }

    if (strncmp(outTrack->hdr.fourcc, "SHAK", 4)) {
        fputs("Invalid file identifier\n", stderr);
        return false;
    }

    if (outTrack->hdr.version != 1) {
        fprintf(stderr, "Invalid version %d\n", outTrack->hdr.version);
        return false;
    }

    for (int i = 0; i < outTrack->hdr.num_frames; i++) {
        CameraShakePoint p{};
        size_t numElemsRead = fread(&p, sizeof(CameraShakePoint), 1, fp);
        if (numElemsRead == 0) {
            fprintf(stderr, "Couldn't read point #%d!\n", i);
            fclose(fp);
            return false;
        }
        outTrack->points.push_back(p);
    }

    fclose(fp);
    return true;
}

CameraShakePoint interpolateTrack(const CameraShakeTrack& track, double secs)
{
    double rate = double(track.hdr.framerate);
    double tframe = secs * rate;
    double fracframe = fmod(tframe, double(track.hdr.num_frames));
    double integer;
    double alpha = modf(fracframe, &integer);

    int first = int(fracframe);
    int second = first + 1;
    if (second >= track.hdr.num_frames)
        second = 0;

    const auto& a = track.points[first];
    const auto& b = track.points[second];

    CameraShakePoint c;
    c.x = (1. - alpha) * a.x + alpha * b.x;
    c.y = (1. - alpha) * a.y + alpha * b.y;
    c.cosine = (1. - alpha) * a.cosine + alpha * b.cosine;
    c.sine = (1. - alpha) * a.sine + alpha * b.sine;

    // Recompute an interpolated roll angle because 'c.roll' can't be interpolated directly.
    double rtan = c.sine / c.cosine;
    c.roll = float(atan(rtan));

    return c;
}

// We define some math helpers here to make the code in 'shakePose()' cleaner.

using Vec = CameraShakePose::Vec;

Vec operator+(const Vec& lhs, const Vec& rhs) { return { lhs.x + rhs.x, lhs.y + rhs.y,lhs.z + rhs.z }; }
Vec operator*(float scale, const Vec& rhs) { return { scale * rhs.x, scale * rhs.y, scale * rhs.z }; }
Vec operator*(const Vec& rhs, float scale) { return { scale * rhs.x, scale * rhs.y, scale * rhs.z }; }
Vec normalize(const Vec& v) { return std::sqrt(v.x * v.x + v.y * v.y + v.z * v.z) * v; }
Vec cross(const Vec& a, const Vec& b) { return {a.y* b.z - a.z * b.y, a.z* b.x - a.x * b.z, a.x* b.y - a.y * b.x}; }

void shakePose(const CameraShakeTrack& track, double secs, double strength, CameraShakePose* pose)
{
    CameraShakePoint shake = interpolateTrack(track, secs);

    // Setting this correctly doesn't really matter since the user can always attenuate shaking with 'strength'.
    const double FIELD_OF_VIEW = 90.0;

    float scale = 1.0 / double(track.hdr.frame_width) * (2. * 3.14159) * (FIELD_OF_VIEW / 360.);
    scale *= 0.25; // Tone down the shake a little.

    // Could use focal length and angular displacements here instead of just nudging the dir vector.
    // But this works OK too.

    Vec dirdelta = scale * (shake.x * pose->right + shake.y * pose->up);

    pose->dir = normalize(pose->dir + strength * dirdelta);

    // Tilt the 'up' vector to roll the camera.
    float roll = strength * shake.roll;
    pose->up = pose->right * -sin(roll) + pose->up * cos(roll);

    // Recompute 'right' now that 'dir' and 'pose' have been changed.
    pose->right = cross(pose->dir, pose->up);
}

Looping

Crossfaded looping is done automatically. The fade is already precomputed in the animation tracks.

Smooth crossfades. A plot of camera X and Y deltas of slowwalk.bin. The animation is repeated twice and the red vertical line marks the loop point. Notice that there's no discontinuity.

Smooth crossfades. A plot of camera X and Y deltas of slowwalk.bin. The animation is repeated twice and the red vertical line marks the loop point. Notice that there's no discontinuity.

CSV dumps

If you're not using C++ each animation track also has a CSV file. The CSV columns are these:

delta_x,delta_y,roll,roll_cosine,roll_sine

Frame rate is 30.003444 Hz, the pixel displacement are relative to a 1080p screen. So to convert the values into radians you'd need to do something like this:

WIDTH = 1920
HEIGHT = 1080
PROJECTION_PLANE_WIDTH = 4.9486 # in mm
FOCAL_LENGTH = 4 # in mm

mm_per_pixel = PROJECTION_PLANE_WIDTH / WIDTH
horizontal_radians = atan((delta_x * mm_per_pixel) / FOCAL_LENGTH)
vertical_radians = atan((delta_y * mm_per_pixel) / FOCAL_LENGTH)

However I was happy with just nudging a cam.direction vector with the x and y deltas as-is. Still, it's important to also use the roll column to tilt the camera! It adds some extra realism :)

License

All the code and data are placed in public domain. See LICENSE.