buffered-interpolation-babylon8
Version:
A class for handling interpolation of networked Babylon JS objects.
214 lines (177 loc) • 6.41 kB
text/typescript
import { Vector3, Quaternion } from "@babylonjs/core/Maths/math.vector";
export enum BufferState {
INITIALIZING = 0,
BUFFERING = 1,
PLAYING = 2
}
export enum BufferMode {
MODE_LERP = 0,
MODE_HERMITE = 1
}
export interface frame {
position: Vector3;
velocity: Vector3;
scale: Vector3;
quaternion: Quaternion;
time: number;
}
// const vectorPool: Vector3[] = [];
// const quatPool: Quaternion[] = [];
const framePool: frame[] = [];
// const getPooledVector: () => Vector3 = () => vectorPool.shift() || new Vector3();
// const getPooledQuaternion: () => Quaternion = () => quatPool.shift() || new Quaternion();
const getPooledFrame: () => frame = () => {
let frame = framePool.pop();
if (!frame) {
frame = { position: new Vector3(), velocity: new Vector3(), scale: new Vector3(1, 1, 1), quaternion: new Quaternion(), time: 0 };
}
return frame;
};
const freeFrame: (f: frame) => void = f => framePool.push(f);
export class InterpolationBuffer {
state: BufferState;
mode: BufferMode;
buffer: frame[];
originFrame: frame;
position: Vector3;
scale: Vector3;
quaternion: Quaternion;
time: number;
bufferTime: number;
constructor(mode = BufferMode.MODE_LERP, bufferTime = 0.15) {
this.state = BufferState.INITIALIZING;
this.buffer = [];
this.bufferTime = bufferTime * 1000;
this.time = 0;
this.mode = mode;
this.originFrame = getPooledFrame();
this.position = new Vector3();
this.quaternion = new Quaternion();
this.scale = new Vector3(1, 1, 1);
}
hermite(target: Vector3, t: number, p1: Vector3, p2: Vector3, v1: Vector3, v2: Vector3) {
const t2 = t * t;
const t3 = t * t * t;
const a = 2 * t3 - 3 * t2 + 1;
const b = -2 * t3 + 3 * t2;
const c = t3 - 2 * t2 + t;
const d = t3 - t2;
target.copyFrom(p1.scaleInPlace(a));
target.add(p2.scaleInPlace(b));
target.add(v1.scaleInPlace(c));
target.add(v2.scaleInPlace(d));
}
lerp(target: Vector3, v1: Vector3, v2: Vector3, alpha: number) {
Vector3.LerpToRef(v1, v2, alpha, target);
}
slerp(target: Quaternion, r1: Quaternion, r2: Quaternion, alpha: number) {
Quaternion.SlerpToRef(r1, r2, alpha, target);
}
updateOriginFrameToBufferTail() {
freeFrame(this.originFrame);
this.originFrame = this.buffer.shift() || getPooledFrame();
}
appendBuffer(position?: Vector3, velocity?: Vector3, quaternion?: Quaternion, scale?: Vector3) {
const tail = this.buffer.length > 0 ? this.buffer[this.buffer.length - 1] : null;
// update the last entry in the buffer if this is the same frame
if (tail && tail.time === this.time) {
if (position) {
tail.position.copyFrom(position);
}
if (velocity) {
tail.velocity.copyFrom(velocity);
}
if (quaternion) {
tail.quaternion.copyFrom(quaternion);
}
if (scale) {
tail.scale.copyFrom(scale);
}
} else {
const priorFrame = tail || this.originFrame;
const newFrame = getPooledFrame();
newFrame.position.copyFrom(position || priorFrame.position);
newFrame.velocity.copyFrom(velocity || priorFrame.velocity);
newFrame.quaternion.copyFrom(quaternion || priorFrame.quaternion);
newFrame.scale.copyFrom(scale || priorFrame.scale);
newFrame.time = this.time;
this.buffer.push(newFrame);
}
}
setTarget(position: Vector3, velocity: Vector3, quaternion: Quaternion, scale: Vector3) {
this.appendBuffer(position, velocity, quaternion, scale);
}
setPosition(position: Vector3, velocity: Vector3) {
this.appendBuffer(position, velocity, undefined, undefined);
}
setQuaternion(quaternion: Quaternion) {
this.appendBuffer(undefined, undefined, quaternion, undefined);
}
setScale(scale: Vector3) {
this.appendBuffer(undefined, undefined, undefined, scale);
}
update(delta: number) {
if (this.state === BufferState.INITIALIZING) {
if (this.buffer.length > 0) {
this.updateOriginFrameToBufferTail();
this.position.copyFrom(this.originFrame.position);
this.quaternion.copyFrom(this.originFrame.quaternion);
this.scale.copyFrom(this.originFrame.scale);
this.state = BufferState.BUFFERING;
}
}
if (this.state === BufferState.BUFFERING) {
if (this.buffer.length > 0 && this.time > this.bufferTime) {
this.state = BufferState.PLAYING;
}
}
if (this.state === BufferState.PLAYING) {
const mark = this.time - this.bufferTime;
//Purge this.buffer of expired frames
while (this.buffer.length > 0 && mark > this.buffer[0].time) {
//if this is the last frame in the buffer, just update the time and reuse it
if (this.buffer.length > 1) {
this.updateOriginFrameToBufferTail();
} else {
this.originFrame.position.copyFrom(this.buffer[0].position);
this.originFrame.velocity.copyFrom(this.buffer[0].velocity);
this.originFrame.quaternion.copyFrom(this.buffer[0].quaternion);
this.originFrame.scale.copyFrom(this.buffer[0].scale);
this.originFrame.time = this.buffer[0].time;
this.buffer[0].time = this.time + delta;
}
}
if (this.buffer.length > 0 && this.buffer[0].time > 0) {
const targetFrame = this.buffer[0];
const delta_time = targetFrame.time - this.originFrame.time;
const alpha = (mark - this.originFrame.time) / delta_time;
if (this.mode === BufferMode.MODE_LERP) {
this.lerp(this.position, this.originFrame.position, targetFrame.position, alpha);
} else if (this.mode === BufferMode.MODE_HERMITE) {
this.hermite(
this.position,
alpha,
this.originFrame.position,
targetFrame.position,
this.originFrame.velocity.scaleInPlace(delta_time),
targetFrame.velocity.scaleInPlace(delta_time)
);
}
this.slerp(this.quaternion, this.originFrame.quaternion, targetFrame.quaternion, alpha);
this.lerp(this.scale, this.originFrame.scale, targetFrame.scale, alpha);
}
}
if (this.state !== BufferState.INITIALIZING) {
this.time += delta;
}
}
getPosition() {
return this.position;
}
getQuaternion() {
return this.quaternion;
}
getScale() {
return this.scale;
}
}