aframe-physics-system
Version:
Physics system for A-Frame VR, built on Cannon.js
275 lines (232 loc) • 9.22 kB
JavaScript
/* global THREE */
var CANNON = require('cannon-es'),
CONSTANTS = require('./constants'),
C_GRAV = CONSTANTS.GRAVITY,
C_MAT = CONSTANTS.CONTACT_MATERIAL;
var LocalDriver = require('./drivers/local-driver'),
WorkerDriver = require('./drivers/worker-driver'),
NetworkDriver = require('./drivers/network-driver'),
AmmoDriver = require('./drivers/ammo-driver');
/**
* Physics system.
*/
module.exports = AFRAME.registerSystem('physics', {
schema: {
// CANNON.js driver type
driver: { default: 'local', oneOf: ['local', 'worker', 'network', 'ammo'] },
networkUrl: { default: '', if: {driver: 'network'} },
workerFps: { default: 60, if: {driver: 'worker'} },
workerInterpolate: { default: true, if: {driver: 'worker'} },
workerInterpBufferSize: { default: 2, if: {driver: 'worker'} },
workerEngine: { default: 'cannon', if: {driver: 'worker'}, oneOf: ['cannon'] },
workerDebug: { default: false, if: {driver: 'worker'} },
gravity: { default: C_GRAV },
iterations: { default: CONSTANTS.ITERATIONS },
friction: { default: C_MAT.friction },
restitution: { default: C_MAT.restitution },
contactEquationStiffness: { default: C_MAT.contactEquationStiffness },
contactEquationRelaxation: { default: C_MAT.contactEquationRelaxation },
frictionEquationStiffness: { default: C_MAT.frictionEquationStiffness },
frictionEquationRegularization: { default: C_MAT.frictionEquationRegularization },
// Never step more than four frames at once. Effectively pauses the scene
// when out of focus, and prevents weird "jumps" when focus returns.
maxInterval: { default: 4 / 60 },
// If true, show wireframes around physics bodies.
debug: { default: false },
// If using ammo, set the default rendering mode for debug
debugDrawMode: { default: THREE.AmmoDebugConstants.NoDebug },
// If using ammo, set the max number of steps per frame
maxSubSteps: { default: 4 },
// If using ammo, set the framerate of the simulation
fixedTimeStep: { default: 1 / 60 }
},
/**
* Initializes the physics system.
*/
async init() {
var data = this.data;
// If true, show wireframes around physics bodies.
this.debug = data.debug;
this.callbacks = {beforeStep: [], step: [], afterStep: []};
this.listeners = {};
this.driver = null;
switch (data.driver) {
case 'local':
this.driver = new LocalDriver();
break;
case 'ammo':
this.driver = new AmmoDriver();
break;
case 'network':
this.driver = new NetworkDriver(data.networkUrl);
break;
case 'worker':
this.driver = new WorkerDriver({
fps: data.workerFps,
engine: data.workerEngine,
interpolate: data.workerInterpolate,
interpolationBufferSize: data.workerInterpBufferSize,
debug: data.workerDebug
});
break;
default:
throw new Error('[physics] Driver not recognized: "%s".', data.driver);
}
if (data.driver !== 'ammo') {
await this.driver.init({
quatNormalizeSkip: 0,
quatNormalizeFast: false,
solverIterations: data.iterations,
gravity: data.gravity,
});
this.driver.addMaterial({name: 'defaultMaterial'});
this.driver.addMaterial({name: 'staticMaterial'});
this.driver.addContactMaterial('defaultMaterial', 'defaultMaterial', {
friction: data.friction,
restitution: data.restitution,
contactEquationStiffness: data.contactEquationStiffness,
contactEquationRelaxation: data.contactEquationRelaxation,
frictionEquationStiffness: data.frictionEquationStiffness,
frictionEquationRegularization: data.frictionEquationRegularization
});
this.driver.addContactMaterial('staticMaterial', 'defaultMaterial', {
friction: 1.0,
restitution: 0.0,
contactEquationStiffness: data.contactEquationStiffness,
contactEquationRelaxation: data.contactEquationRelaxation,
frictionEquationStiffness: data.frictionEquationStiffness,
frictionEquationRegularization: data.frictionEquationRegularization
});
} else {
await this.driver.init({
gravity: data.gravity,
debugDrawMode: data.debugDrawMode,
solverIterations: data.iterations,
maxSubSteps: data.maxSubSteps,
fixedTimeStep: data.fixedTimeStep
});
}
this.initialized = true;
if (this.debug) {
this.setDebug(true);
}
},
/**
* Updates the physics world on each tick of the A-Frame scene. It would be
* entirely possible to separate the two – updating physics more or less
* frequently than the scene – if greater precision or performance were
* necessary.
* @param {number} t
* @param {number} dt
*/
tick: function (t, dt) {
if (!this.initialized || !dt) return;
var i;
var callbacks = this.callbacks;
for (i = 0; i < this.callbacks.beforeStep.length; i++) {
this.callbacks.beforeStep[i].beforeStep(t, dt);
}
this.driver.step(Math.min(dt / 1000, this.data.maxInterval));
for (i = 0; i < callbacks.step.length; i++) {
callbacks.step[i].step(t, dt);
}
for (i = 0; i < callbacks.afterStep.length; i++) {
callbacks.afterStep[i].afterStep(t, dt);
}
},
setDebug: function(debug) {
this.debug = debug;
if (this.data.driver === 'ammo' && this.initialized) {
if (debug && !this.debugDrawer) {
this.debugDrawer = this.driver.getDebugDrawer(this.el.object3D);
this.debugDrawer.enable();
} else if (this.debugDrawer) {
this.debugDrawer.disable();
this.debugDrawer = null;
}
}
},
/**
* Adds a body to the scene, and binds proxied methods to the driver.
* @param {CANNON.Body} body
*/
addBody: function (body, group, mask) {
var driver = this.driver;
if (this.data.driver === 'local') {
body.__applyImpulse = body.applyImpulse;
body.applyImpulse = function () {
driver.applyBodyMethod(body, 'applyImpulse', arguments);
};
body.__applyForce = body.applyForce;
body.applyForce = function () {
driver.applyBodyMethod(body, 'applyForce', arguments);
};
body.updateProperties = function () {
driver.updateBodyProperties(body);
};
this.listeners[body.id] = function (e) { body.el.emit('collide', e); };
body.addEventListener('collide', this.listeners[body.id]);
}
this.driver.addBody(body, group, mask);
},
/**
* Removes a body and its proxied methods.
* @param {CANNON.Body} body
*/
removeBody: function (body) {
this.driver.removeBody(body);
if (this.data.driver === 'local' || this.data.driver === 'worker') {
body.removeEventListener('collide', this.listeners[body.id]);
delete this.listeners[body.id];
body.applyImpulse = body.__applyImpulse;
delete body.__applyImpulse;
body.applyForce = body.__applyForce;
delete body.__applyForce;
delete body.updateProperties;
}
},
/** @param {CANNON.Constraint or Ammo.btTypedConstraint} constraint */
addConstraint: function (constraint) {
this.driver.addConstraint(constraint);
},
/** @param {CANNON.Constraint or Ammo.btTypedConstraint} constraint */
removeConstraint: function (constraint) {
this.driver.removeConstraint(constraint);
},
/**
* Adds a component instance to the system and schedules its update methods to be called
* the given phase.
* @param {Component} component
* @param {string} phase
*/
addComponent: function (component) {
var callbacks = this.callbacks;
if (component.beforeStep) callbacks.beforeStep.push(component);
if (component.step) callbacks.step.push(component);
if (component.afterStep) callbacks.afterStep.push(component);
},
/**
* Removes a component instance from the system.
* @param {Component} component
* @param {string} phase
*/
removeComponent: function (component) {
var callbacks = this.callbacks;
if (component.beforeStep) {
callbacks.beforeStep.splice(callbacks.beforeStep.indexOf(component), 1);
}
if (component.step) {
callbacks.step.splice(callbacks.step.indexOf(component), 1);
}
if (component.afterStep) {
callbacks.afterStep.splice(callbacks.afterStep.indexOf(component), 1);
}
},
/** @return {Array<object>} */
getContacts: function () {
return this.driver.getContacts();
},
getMaterial: function (name) {
return this.driver.getMaterial(name);
}
});