Skip to content

Commit

Permalink
Smooth camera animation (#394)
Browse files Browse the repository at this point in the history
  • Loading branch information
slimbuck authored Jan 30, 2025
1 parent 577de1b commit 3e9a7c9
Show file tree
Hide file tree
Showing 3 changed files with 163 additions and 16 deletions.
108 changes: 108 additions & 0 deletions src/anim/spline.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,108 @@
class CubicSpline {
// control times
times: number[];

// control data: in-tangent, point, out-tangent
knots: number[];

// dimension of the knot points
dim: number;

constructor(times: number[], knots: number[]) {
this.times = times;
this.knots = knots;
this.dim = knots.length / times.length / 3;
}

evaluate(time: number, result: number[]) {
const { times } = this;
const last = times.length - 1;

if (time <= times[0]) {
this.getKnot(0, result);
} else if (time >= times[last]) {
this.getKnot(last, result);
} else {
let seg = 0;
while (time >= times[seg + 1]) {
seg++;
}
return this.evaluateSegment(seg, (time - times[seg]) / (times[seg + 1] - times[seg]), result);
}
}

getKnot(index: number, result: number[]) {
const { knots, dim } = this;
const idx = index * 3 * dim;
for (let i = 0; i < dim; ++i) {
result[i] = knots[idx + i * 3 + 1];
}
}

// evaluate the spline segment at the given normalized time t
evaluateSegment(segment: number, t: number, result: number[]) {
const { knots, dim } = this;

const t2 = t * t;
const twot = t + t;
const omt = 1 - t;
const omt2 = omt * omt;

let idx = segment * 3 * dim;
for (let i = 0; i < dim; ++i) {
const p0 = knots[idx + 1];
const m0 = knots[idx + 2];
const m1 = knots[idx + 3 * dim];
const p1 = knots[idx + 3 * dim + 1];
idx += 3;

result[i] =
p0 * ((1 + twot) * omt2) +
m0 * (t * omt2) +
p1 * (t2 * (3 - twot)) +
m1 * (t2 * (t - 1));
}
}

// create cubic spline data from a set of control points to be interpolated
// times: time values for each control point
// points: control point values to be interpolated (n dimensional)
// tension: level of smoothness, 0 = smooth, 1 = linear interpolation
static fromPoints(times: number[], points: number[], tension = 0) {
const dim = points.length / times.length;
const knots = new Array<number>(times.length * dim * 3);

for (let i = 0; i < times.length; i++) {
const t = times[i];

for (let j = 0; j < dim; j++) {
const idx = i * dim + j;
const p = points[idx];

let tangent;
if (i === 0) {
tangent = (points[idx + dim] - p) / (times[i + 1] - t);
} else if (i === times.length - 1) {
tangent = (p - points[idx - dim]) / (t - times[i - 1]);
} else {
// finite difference tangents
tangent = 0.5 * ((points[idx + dim] - p) / (times[i + 1] - t) + (p - points[idx - dim]) / (t - times[i - 1]));

// cardinal spline tangents
// tangent = (points[idx + dim] - points[idx - dim]) / (times[i + 1] - times[i - 1]);
}

// apply tension
tangent *= (1.0 - tension);

knots[idx * 3] = tangent;
knots[idx * 3 + 1] = p;
knots[idx * 3 + 2] = tangent;
}
}

return new CubicSpline(times, knots);
}
}

export { CubicSpline };
3 changes: 3 additions & 0 deletions src/scene.ts
Original file line number Diff line number Diff line change
Expand Up @@ -298,6 +298,9 @@ class Scene {
// allow elements to update
this.forEachElement(e => e.onUpdate(deltaTime));

// fire global update
this.events.fire('update', deltaTime);

// fire a 'serialize' event which listers will use to store their state. we'll use
// this to decide if the view has changed and so requires rendering.
const i = this.app.frame % 2;
Expand Down
68 changes: 52 additions & 16 deletions src/ui/camera-panel.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,7 @@
import { Container, Label } from 'pcui';
import { Vec3 } from 'playcanvas';
import { EventHandle, Vec3 } from 'playcanvas';

import { CubicSpline } from 'src/anim/spline';

import { Events } from '../events';
import { localize } from './localization';
Expand Down Expand Up @@ -105,6 +107,16 @@ class CameraPanel extends Container {
const poses: { pose: Pose, row: Container }[] = [];
let currentPose = -1;

// animation support
let animHandle: EventHandle = null;

// stop the playing animation
const stop = () => {
posePlay.text = '\uE131';
animHandle.off();
animHandle = null;
};

const setPose = (index: number, speed = 1) => {
if (index === currentPose) {
return;
Expand All @@ -124,6 +136,11 @@ class CameraPanel extends Container {
});

currentPose = index;

// cancel animation playback if user selects a pose during animation
if (animHandle) {
stop();
}
};

const addPose = (pose: Pose) => {
Expand Down Expand Up @@ -182,26 +199,44 @@ class CameraPanel extends Container {
nextPose();
});

let timeout: number = null;
// start playing the current camera poses animation
const play = () => {
posePlay.text = '\uE135';

// construct the spline points to be interpolated
const times = poses.map((p, i) => i);
const points = [];
for (let i = 0; i < poses.length; ++i) {
const p = poses[i].pose;
points.push(p.position.x, p.position.y, p.position.z);
points.push(p.target.x, p.target.y, p.target.z);
}

const stop = () => {
posePlay.text = '\uE131';
clearTimeout(timeout);
timeout = null;
// interpolate camera positions and camera target positions
const spline = CubicSpline.fromPoints(times, points);
const result: number[] = [];
const pose = { position: new Vec3(), target: new Vec3() };
let time = 0;

// handle application update tick
animHandle = events.on('update', (dt: number) => {
time = (time + dt) % (poses.length - 1);

// evaluate the spline at current time
spline.evaluate(time, result);

// set camera pose
pose.position.set(result[0], result[1], result[2]);
pose.target.set(result[3], result[4], result[5]);
events.fire('camera.setPose', pose, 0);
});
};

posePlay.on('click', () => {
if (timeout) {

if (animHandle) {
stop();
} else if (poses.length > 0) {
const next = () => {
nextPose();
timeout = window.setTimeout(next, 250);
};

posePlay.text = '\uE135';
next();
play();
}
});

Expand All @@ -217,9 +252,10 @@ class CameraPanel extends Container {
removePose(index);
});

// cancel animation playback if user interacts with camera
events.on('camera.controller', (type: string) => {
if (type !== 'pointermove') {
if (timeout) {
if (animHandle) {
stop();
} else {
setPose(-1);
Expand Down

0 comments on commit 3e9a7c9

Please sign in to comment.