@c-frame/aframe-physics-system
Version:
Physics system for A-Frame VR, built on Cannon.js & Ammo.js
369 lines (301 loc) • 10.5 kB
JavaScript
var CANNON = require('cannon-es');
const { threeToCannon, ShapeType } = require('three-to-cannon');
const identityQuaternion = new THREE.Quaternion()
function mesh2shape (object, options) {
const result = threeToCannon(object, options);
return result;
}
require('../../../lib/CANNON-shape2mesh');
var Body = {
dependencies: ['velocity'],
schema: {
mass: {default: 5, if: {type: 'dynamic'}},
linearDamping: { default: 0.01, if: {type: 'dynamic'}},
angularDamping: { default: 0.01, if: {type: 'dynamic'}},
shape: {default: 'auto', oneOf: ['auto', 'box', 'cylinder', 'sphere', 'hull', 'mesh', 'none']},
cylinderAxis: {default: 'y', oneOf: ['x', 'y', 'z']},
sphereRadius: {default: NaN},
type: {default: 'dynamic', oneOf: ['static', 'dynamic']}
},
/**
* Initializes a body component, assigning it to the physics system and binding listeners for
* parsing the elements geometry.
*/
init: function () {
this.system = this.el.sceneEl.systems.physics;
if (this.el.sceneEl.hasLoaded) {
this.initBody();
} else {
this.el.sceneEl.addEventListener('loaded', this.initBody.bind(this));
}
},
/**
* Parses an element's geometry and component metadata to create a CANNON.Body instance for the
* component.
*/
initBody: function () {
var el = this.el,
data = this.data;
var obj = this.el.object3D;
var pos = obj.position;
var quat = obj.quaternion;
this.body = new CANNON.Body({
mass: data.type === 'static' ? 0 : data.mass || 0,
material: this.system.getMaterial('defaultMaterial'),
position: new CANNON.Vec3(pos.x, pos.y, pos.z),
quaternion: new CANNON.Quaternion(quat.x, quat.y, quat.z, quat.w),
linearDamping: data.linearDamping,
angularDamping: data.angularDamping,
type: data.type === 'dynamic' ? CANNON.Body.DYNAMIC : CANNON.Body.STATIC,
});
// Matrix World must be updated at root level, if scale is to be applied – updateMatrixWorld()
// only checks an object's parent, not the rest of the ancestors. Hence, a wrapping entity with
// scale="0.5 0.5 0.5" will be ignored.
// Reference: https://github.com/mrdoob/three.js/blob/master/src/core/Object3D.js#L511-L541
// Potential fix: https://github.com/mrdoob/three.js/pull/7019
this.el.object3D.updateMatrixWorld(true);
if(data.shape !== 'none') {
var options = data.shape === 'auto' ? undefined : AFRAME.utils.extend({}, this.data, {
type: ShapeType[data.shape.toUpperCase()]
});
const shapeInfo = mesh2shape(this.el.object3D, options);
let shape, offset, orientation;
if (shapeInfo) {
({ shape, offset, orientation } = shapeInfo);
}
if (!shape) {
el.addEventListener('object3dset', this.initBody.bind(this));
return;
}
this.body.addShape(shape, offset, orientation);
// Show wireframe
if (this.system.debug) {
this.shouldUpdateWireframe = true;
}
this.hasShape = true;
}
this.el.body = this.body;
this.body.el = el;
// If component wasn't initialized when play() was called, finish up.
if (this.isPlaying) {
this._play();
}
if (this.hasShape) {
this.el.emit('body-loaded', {body: this.el.body});
}
},
addShape: function(shape, offset, orientation) {
if (this.data.shape !== 'none') {
console.warn('shape can only be added if shape property is none');
return;
}
if (!shape) {
console.warn('shape cannot be null');
return;
}
if (!this.body) {
console.warn('shape cannot be added before body is loaded');
return;
}
this.body.addShape(shape, offset, orientation);
if (this.system.debug) {
this.shouldUpdateWireframe = true;
}
this.shouldUpdateBody = true;
},
tick: function () {
if (this.shouldUpdateBody) {
// Calling play will result in the object being re-added to the
// physics system with the updated body / shape data.
// But we mustn't add it twice, so any previously loaded body should be paused first.
this._pause();
this.hasShape = true;
this._play()
this.el.emit('body-loaded', {body: this.el.body});
this.shouldUpdateBody = false;
}
if (this.shouldUpdateWireframe) {
this.createWireframe(this.body);
this.shouldUpdateWireframe = false;
}
},
/**
* Registers the component with the physics system, if ready.
*/
play: function () {
this._play();
},
/**
* Internal helper to register component with physics system.
*/
_play: function () {
if (!this.hasShape) return;
this.syncToPhysics();
this.system.addComponent(this);
this.system.addBody(this.body);
if (this.wireframe) this.el.sceneEl.object3D.add(this.wireframe);
},
/**
* Unregisters the component with the physics system.
*/
pause: function () {
this._pause();
},
_pause: function () {
if (!this.hasShape) return;
this.system.removeComponent(this);
if (this.body) this.system.removeBody(this.body);
if (this.wireframe) this.el.sceneEl.object3D.remove(this.wireframe);
},
/**
* Updates the CANNON.Body instance, where possible.
*/
update: function (prevData) {
if (!this.body) return;
var data = this.data;
if (prevData.type != undefined && data.type != prevData.type) {
this.body.type = data.type === 'dynamic' ? CANNON.Body.DYNAMIC : CANNON.Body.STATIC;
}
this.body.mass = data.mass || 0;
if (data.type === 'dynamic') {
this.body.linearDamping = data.linearDamping;
this.body.angularDamping = data.angularDamping;
}
if (data.mass !== prevData.mass) {
this.body.updateMassProperties();
}
if (this.body.updateProperties) this.body.updateProperties();
},
/**
* Removes the component and all physics and scene side effects.
*/
remove: function () {
if (this.body) {
delete this.body.el;
delete this.body;
}
delete this.el.body;
delete this.wireframe;
},
beforeStep: function () {
if (this.body.mass === 0) {
this.syncToPhysics();
}
},
step: function () {
if (this.body.mass !== 0) {
this.syncFromPhysics();
}
},
/**
* Creates a wireframe for the body, for debugging.
* TODO(donmccurdy) – Refactor this into a standalone utility or component.
* @param {CANNON.Body} body
* @param {CANNON.Shape} shape
*/
createWireframe: function (body) {
if (this.wireframe) {
this.el.sceneEl.object3D.remove(this.wireframe);
delete this.wireframe;
}
this.wireframe = new THREE.Object3D();
this.el.sceneEl.object3D.add(this.wireframe);
var offset, mesh;
var orientation = new THREE.Quaternion();
for (var i = 0; i < this.body.shapes.length; i++)
{
offset = this.body.shapeOffsets[i],
orientation.copy(this.body.shapeOrientations[i]),
mesh = CANNON.shape2mesh(this.body).children[i];
var wireframe = new THREE.LineSegments(
new THREE.EdgesGeometry(mesh.geometry),
new THREE.LineBasicMaterial({color: 0xff0000})
);
if (offset) {
wireframe.position.copy(offset);
}
if (orientation) {
wireframe.quaternion.copy(orientation);
}
this.wireframe.add(wireframe);
}
this.syncWireframe();
},
/**
* Updates the debugging wireframe's position and rotation.
*/
syncWireframe: function () {
var offset,
wireframe = this.wireframe;
if (!this.wireframe) return;
// Apply rotation. If the shape required custom orientation, also apply
// that on the wireframe.
wireframe.quaternion.copy(this.body.quaternion);
if (wireframe.orientation) {
wireframe.quaternion.multiply(wireframe.orientation);
}
// Apply position. If the shape required custom offset, also apply that on
// the wireframe.
wireframe.position.copy(this.body.position);
if (wireframe.offset) {
offset = wireframe.offset.clone().applyQuaternion(wireframe.quaternion);
wireframe.position.add(offset);
}
wireframe.updateMatrix();
},
/**
* Updates the CANNON.Body instance's position, velocity, and rotation, based on the scene.
*/
syncToPhysics: (function () {
var q = new THREE.Quaternion(),
v = new THREE.Vector3();
return function () {
var el = this.el,
parentEl = el.parentEl,
body = this.body;
if (!body) return;
if (el.components.velocity) body.velocity.copy(el.getAttribute('velocity'));
if (parentEl.isScene) {
body.quaternion.copy(el.object3D.quaternion);
body.position.copy(el.object3D.position);
} else {
el.object3D.getWorldQuaternion(q);
body.quaternion.copy(q);
el.object3D.getWorldPosition(v);
body.position.copy(v);
}
if (this.body.updateProperties) this.body.updateProperties();
if (this.wireframe) this.syncWireframe();
};
}()),
/**
* Updates the scene object's position and rotation, based on the physics simulation.
*/
syncFromPhysics: (function () {
var v = new THREE.Vector3(),
q1 = new THREE.Quaternion(),
q2 = new THREE.Quaternion();
return function () {
var el = this.el,
parentEl = el.parentEl,
body = this.body;
if (!body) return;
if (!parentEl) return;
if (parentEl.isScene) {
el.object3D.quaternion.copy(body.quaternion);
el.object3D.position.copy(body.position);
} else {
q1.copy(body.quaternion);
parentEl.object3D.getWorldQuaternion(q2);
q1.premultiply(q2.invert());
el.object3D.quaternion.copy(q1);
v.copy(body.position);
parentEl.object3D.worldToLocal(v);
el.object3D.position.copy(v);
}
if (this.wireframe) this.syncWireframe();
};
}())
};
module.exports.definition = Body;
module.exports.Component = AFRAME.registerComponent('body', Body);