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:
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;
}
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);
}
Below are listings of the two files that make up the libshakez animation library.
#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);
#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);
}
Crossfaded looping is done automatically. The fade is already precomputed in the animation tracks.
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 :)
All the code and data are placed in public domain. See LICENSE.