@c-frame/aframe-physics-system
Version:
Physics system for A-Frame VR, built on Cannon.js & Ammo.js
257 lines (220 loc) • 8.01 kB
JavaScript
/* global performance */
var webworkify = require('webworkify'),
webworkifyDebug = require('./webworkify-debug'),
Driver = require('./driver'),
Event = require('./event'),
worker = require('./worker'),
protocol = require('../utils/protocol');
var ID = protocol.ID;
/******************************************************************************
* Constructor
*/
function WorkerDriver (options) {
this.fps = options.fps;
this.engine = options.engine;
this.interpolate = options.interpolate;
// Approximate number of physics steps to 'pad' rendering.
this.interpBufferSize = options.interpolationBufferSize;
this.debug = options.debug;
this.bodies = {};
this.contacts = [];
// https://gafferongames.com/post/snapshot_interpolation/
this.frameDelay = this.interpBufferSize * 1000 / this.fps;
this.frameBuffer = [];
this.worker = this.debug
? webworkifyDebug(worker)
: webworkify(worker);
this.worker.addEventListener('message', this._onMessage.bind(this));
}
WorkerDriver.prototype = new Driver();
WorkerDriver.prototype.constructor = WorkerDriver;
module.exports = WorkerDriver;
/******************************************************************************
* Lifecycle
*/
/* @param {object} worldConfig */
WorkerDriver.prototype.init = function (worldConfig) {
this.worker.postMessage({
type: Event.INIT,
worldConfig: worldConfig,
fps: this.fps,
engine: this.engine
});
};
/**
* Increments the physics world forward one step, if interpolation is enabled.
* If disabled, increments are performed as messages arrive.
* @param {number} deltaMS
*/
WorkerDriver.prototype.step = function () {
if (!this.interpolate) return;
// Get the two oldest frames that haven't expired. Ideally we would use all
// available frames to keep things smooth, but lerping is easier and faster.
var prevFrame = this.frameBuffer[0];
var nextFrame = this.frameBuffer[1];
var timestamp = performance.now();
while (prevFrame && nextFrame && timestamp - prevFrame.timestamp > this.frameDelay) {
this.frameBuffer.shift();
prevFrame = this.frameBuffer[0];
nextFrame = this.frameBuffer[1];
}
if (!prevFrame || !nextFrame) return;
var mix = (timestamp - prevFrame.timestamp) / this.frameDelay;
mix = (mix - (1 - 1 / this.interpBufferSize)) * this.interpBufferSize;
for (var id in prevFrame.bodies) {
if (prevFrame.bodies.hasOwnProperty(id) && nextFrame.bodies.hasOwnProperty(id)) {
protocol.deserializeInterpBodyUpdate(
prevFrame.bodies[id],
nextFrame.bodies[id],
this.bodies[id],
mix
);
}
}
};
WorkerDriver.prototype.destroy = function () {
this.worker.terminate();
delete this.worker;
};
/** {Event} event */
WorkerDriver.prototype._onMessage = function (event) {
if (event.data.type === Event.STEP) {
var data = event.data,
bodies = data.bodies;
this.contacts = event.data.contacts;
// If interpolation is enabled, store the frame. If not, update all bodies
// immediately.
if (this.interpolate) {
this.frameBuffer.push({timestamp: performance.now(), bodies: bodies});
} else {
for (var id in bodies) {
if (bodies.hasOwnProperty(id)) {
protocol.deserializeBodyUpdate(bodies[id], this.bodies[id]);
}
}
}
} else if (event.data.type === Event.COLLIDE) {
var body = this.bodies[event.data.bodyID];
var target = this.bodies[event.data.targetID];
var contact = protocol.deserializeContact(event.data.contact, this.bodies);
if (!body._listeners || !body._listeners.collide) return;
for (var i = 0; i < body._listeners.collide.length; i++) {
body._listeners.collide[i]({target: target, body: body, contact: contact});
}
} else {
throw new Error('[WorkerDriver] Unexpected message type.');
}
};
/******************************************************************************
* Bodies
*/
/* @param {CANNON.Body} body */
WorkerDriver.prototype.addBody = function (body) {
protocol.assignID('body', body);
this.bodies[body[ID]] = body;
this.worker.postMessage({type: Event.ADD_BODY, body: protocol.serializeBody(body)});
};
/* @param {CANNON.Body} body */
WorkerDriver.prototype.removeBody = function (body) {
this.worker.postMessage({type: Event.REMOVE_BODY, bodyID: body[ID]});
delete this.bodies[body[ID]];
};
/**
* @param {CANNON.Body} body
* @param {string} methodName
* @param {Array} args
*/
WorkerDriver.prototype.applyBodyMethod = function (body, methodName, args) {
switch (methodName) {
case 'applyForce':
case 'applyImpulse':
this.worker.postMessage({
type: Event.APPLY_BODY_METHOD,
bodyID: body[ID],
methodName: methodName,
args: [args[0].toArray(), args[1].toArray()]
});
break;
default:
throw new Error('Unexpected methodName: %s', methodName);
}
};
/** @param {CANNON.Body} body */
WorkerDriver.prototype.updateBodyProperties = function (body) {
this.worker.postMessage({
type: Event.UPDATE_BODY_PROPERTIES,
body: protocol.serializeBody(body)
});
};
/******************************************************************************
* Materials
*/
/**
* @param {string} name
* @return {CANNON.Material}
*/
WorkerDriver.prototype.getMaterial = function (name) {
// No access to materials here. Eventually we might return the name or ID, if
// multiple materials were selected, but for now there's only one and it's safe
// to assume the worker is already using it.
return undefined;
};
/** @param {object} materialConfig */
WorkerDriver.prototype.addMaterial = function (materialConfig) {
this.worker.postMessage({type: Event.ADD_MATERIAL, materialConfig: materialConfig});
};
/**
* @param {string} matName1
* @param {string} matName2
* @param {object} contactMaterialConfig
*/
WorkerDriver.prototype.addContactMaterial = function (matName1, matName2, contactMaterialConfig) {
this.worker.postMessage({
type: Event.ADD_CONTACT_MATERIAL,
materialName1: matName1,
materialName2: matName2,
contactMaterialConfig: contactMaterialConfig
});
};
/******************************************************************************
* Constraints
*/
/* @param {CANNON.Constraint} constraint */
WorkerDriver.prototype.addConstraint = function (constraint) {
if (!constraint.type) {
if (constraint instanceof CANNON.LockConstraint) {
constraint.type = 'LockConstraint';
} else if (constraint instanceof CANNON.DistanceConstraint) {
constraint.type = 'DistanceConstraint';
} else if (constraint instanceof CANNON.HingeConstraint) {
constraint.type = 'HingeConstraint';
} else if (constraint instanceof CANNON.ConeTwistConstraint) {
constraint.type = 'ConeTwistConstraint';
} else if (constraint instanceof CANNON.PointToPointConstraint) {
constraint.type = 'PointToPointConstraint';
}
}
protocol.assignID('constraint', constraint);
this.worker.postMessage({
type: Event.ADD_CONSTRAINT,
constraint: protocol.serializeConstraint(constraint)
});
};
/* @param {CANNON.Constraint} constraint */
WorkerDriver.prototype.removeConstraint = function (constraint) {
this.worker.postMessage({
type: Event.REMOVE_CONSTRAINT,
constraintID: constraint[ID]
});
};
/******************************************************************************
* Contacts
*/
/** @return {Array<object>} */
WorkerDriver.prototype.getContacts = function () {
// TODO(donmccurdy): There's some wasted memory allocation here.
var bodies = this.bodies;
return this.contacts.map(function (message) {
return protocol.deserializeContact(message, bodies);
});
};