@c-frame/aframe-physics-system
Version:
Physics system for A-Frame VR, built on Cannon.js & Ammo.js
545 lines (459 loc) • 18.3 kB
JavaScript
/* global Ammo,THREE */
const AmmoDebugDrawer = require("ammo-debug-drawer");
const threeToAmmo = require("three-to-ammo");
const CONSTANTS = require("../../constants"),
ACTIVATION_STATE = CONSTANTS.ACTIVATION_STATE,
COLLISION_FLAG = CONSTANTS.COLLISION_FLAG,
SHAPE = CONSTANTS.SHAPE,
TYPE = CONSTANTS.TYPE,
FIT = CONSTANTS.FIT;
const ACTIVATION_STATES = [
ACTIVATION_STATE.ACTIVE_TAG,
ACTIVATION_STATE.ISLAND_SLEEPING,
ACTIVATION_STATE.WANTS_DEACTIVATION,
ACTIVATION_STATE.DISABLE_DEACTIVATION,
ACTIVATION_STATE.DISABLE_SIMULATION
];
const RIGID_BODY_FLAGS = {
NONE: 0,
DISABLE_WORLD_GRAVITY: 1
};
function almostEqualsVector3(epsilon, u, v) {
return Math.abs(u.x - v.x) < epsilon && Math.abs(u.y - v.y) < epsilon && Math.abs(u.z - v.z) < epsilon;
}
function almostEqualsBtVector3(epsilon, u, v) {
return Math.abs(u.x() - v.x()) < epsilon && Math.abs(u.y() - v.y()) < epsilon && Math.abs(u.z() - v.z()) < epsilon;
}
function almostEqualsQuaternion(epsilon, u, v) {
return (
(Math.abs(u.x - v.x) < epsilon &&
Math.abs(u.y - v.y) < epsilon &&
Math.abs(u.z - v.z) < epsilon &&
Math.abs(u.w - v.w) < epsilon) ||
(Math.abs(u.x + v.x) < epsilon &&
Math.abs(u.y + v.y) < epsilon &&
Math.abs(u.z + v.z) < epsilon &&
Math.abs(u.w + v.w) < epsilon)
);
}
let AmmoBody = {
schema: {
loadedEvent: { default: "" },
mass: { default: 1 },
gravity: { type: "vec3", default: null },
linearDamping: { default: 0.01 },
angularDamping: { default: 0.01 },
linearSleepingThreshold: { default: 1.6 },
angularSleepingThreshold: { default: 2.5 },
angularFactor: { type: "vec3", default: { x: 1, y: 1, z: 1 } },
activationState: {
default: ACTIVATION_STATE.ACTIVE_TAG,
oneOf: ACTIVATION_STATES
},
type: { default: "dynamic", oneOf: [TYPE.STATIC, TYPE.DYNAMIC, TYPE.KINEMATIC] },
emitCollisionEvents: { default: false },
disableCollision: { default: false },
collisionFilterGroup: { default: 1 }, //32-bit mask,
collisionFilterMask: { default: 1 }, //32-bit mask
scaleAutoUpdate: { default: true },
restitution: {default: 0} // does not support updates
},
/**
* 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;
this.shapeComponents = [];
if (this.data.loadedEvent === "") {
this.loadedEventFired = true;
} else {
this.el.addEventListener(
this.data.loadedEvent,
() => {
this.loadedEventFired = true;
},
{ once: true }
);
}
if (this.system.initialized && this.loadedEventFired) {
this.initBody();
}
},
/**
* Parses an element's geometry and component metadata to create an Ammo body instance for the
* component.
*/
initBody: (function() {
const pos = new THREE.Vector3();
const quat = new THREE.Quaternion();
const boundingBox = new THREE.Box3();
return function() {
const el = this.el,
data = this.data;
const clamp = (num, min, max) => Math.min(Math.max(num, min), max)
this.localScaling = new Ammo.btVector3();
const obj = this.el.object3D;
obj.getWorldPosition(pos);
obj.getWorldQuaternion(quat);
this.prevScale = new THREE.Vector3(1, 1, 1);
this.prevNumChildShapes = 0;
this.msTransform = new Ammo.btTransform();
this.msTransform.setIdentity();
this.rotation = new Ammo.btQuaternion(quat.x, quat.y, quat.z, quat.w);
this.msTransform.getOrigin().setValue(pos.x, pos.y, pos.z);
this.msTransform.setRotation(this.rotation);
this.motionState = new Ammo.btDefaultMotionState(this.msTransform);
this.localInertia = new Ammo.btVector3(0, 0, 0);
this.compoundShape = new Ammo.btCompoundShape(true);
this.rbInfo = new Ammo.btRigidBodyConstructionInfo(
data.mass,
this.motionState,
this.compoundShape,
this.localInertia
);
this.rbInfo.m_restitution = clamp(this.data.restitution, 0, 1);
this.body = new Ammo.btRigidBody(this.rbInfo);
this.body.setActivationState(ACTIVATION_STATES.indexOf(data.activationState) + 1);
this.body.setSleepingThresholds(data.linearSleepingThreshold, data.angularSleepingThreshold);
this.body.setDamping(data.linearDamping, data.angularDamping);
const angularFactor = new Ammo.btVector3(data.angularFactor.x, data.angularFactor.y, data.angularFactor.z);
this.body.setAngularFactor(angularFactor);
Ammo.destroy(angularFactor);
this._updateBodyGravity(data.gravity)
this.updateCollisionFlags();
this.el.body = this.body;
this.body.el = el;
this.isLoaded = true;
this.el.emit("body-loaded", { body: this.el.body });
this._addToSystem();
};
})(),
tick: function() {
if (this.system.initialized && !this.isLoaded && this.loadedEventFired) {
this.initBody();
}
},
_updateBodyGravity(gravity) {
if (gravity.x !== undefined &&
gravity.y !== undefined &&
gravity.z !== undefined) {
const gravityBtVec = new Ammo.btVector3(gravity.x, gravity.y, gravity.z);
if (!almostEqualsBtVector3(0.001, gravityBtVec, this.system.driver.physicsWorld.getGravity())) {
this.body.setFlags(RIGID_BODY_FLAGS.DISABLE_WORLD_GRAVITY);
} else {
this.body.setFlags(RIGID_BODY_FLAGS.NONE);
}
this.body.setGravity(gravityBtVec);
Ammo.destroy(gravityBtVec);
}
else {
// no per-body gravity specified - just use world gravity
this.body.setFlags(RIGID_BODY_FLAGS.NONE);
}
},
_updateShapes: (function() {
const needsPolyhedralInitialization = [SHAPE.HULL, SHAPE.HACD, SHAPE.VHACD];
return function() {
let updated = false;
const obj = this.el.object3D;
if (this.data.scaleAutoUpdate && this.prevScale && !almostEqualsVector3(0.001, obj.scale, this.prevScale)) {
this.prevScale.copy(obj.scale);
updated = true;
this.localScaling.setValue(this.prevScale.x, this.prevScale.y, this.prevScale.z);
this.compoundShape.setLocalScaling(this.localScaling);
}
if (this.shapeComponentsChanged) {
this.shapeComponentsChanged = false;
updated = true;
for (let i = 0; i < this.shapeComponents.length; i++) {
const shapeComponent = this.shapeComponents[i];
if (shapeComponent.getShapes().length === 0) {
this._createCollisionShape(shapeComponent);
}
const collisionShapes = shapeComponent.getShapes();
for (let j = 0; j < collisionShapes.length; j++) {
const collisionShape = collisionShapes[j];
if (!collisionShape.added) {
this.compoundShape.addChildShape(collisionShape.localTransform, collisionShape);
collisionShape.added = true;
}
}
}
if (this.data.type === TYPE.DYNAMIC) {
this.updateMass();
}
this.system.driver.updateBody(this.body);
}
//call initializePolyhedralFeatures for hull shapes if debug is turned on and/or scale changes
if (this.system.debug && (updated || !this.polyHedralFeaturesInitialized)) {
for (let i = 0; i < this.shapeComponents.length; i++) {
const collisionShapes = this.shapeComponents[i].getShapes();
for (let j = 0; j < collisionShapes.length; j++) {
const collisionShape = collisionShapes[j];
if (needsPolyhedralInitialization.indexOf(collisionShape.type) !== -1) {
collisionShape.initializePolyhedralFeatures(0);
}
}
}
this.polyHedralFeaturesInitialized = true;
}
};
})(),
_createCollisionShape: function(shapeComponent) {
const data = shapeComponent.data;
const vertices = [];
const matrices = [];
const indexes = [];
const root = shapeComponent.el.object3D;
const matrixWorld = root.matrixWorld;
threeToAmmo.iterateGeometries(root, data, (vertexArray, matrixArray, indexArray) => {
vertices.push(vertexArray);
matrices.push(matrixArray);
indexes.push(indexArray);
});
const collisionShapes = threeToAmmo.createCollisionShapes(vertices, matrices, indexes, matrixWorld.elements, data);
shapeComponent.addShapes(collisionShapes);
return;
},
/**
* Registers the component with the physics system.
*/
play: function() {
if (this.isLoaded) {
this._addToSystem();
}
},
_addToSystem: function() {
if (!this.addedToSystem) {
this.system.addBody(this.body, this.data.collisionFilterGroup, this.data.collisionFilterMask);
if (this.data.emitCollisionEvents) {
this.system.driver.addEventListener(this.body);
}
this.system.addComponent(this);
this.addedToSystem = true;
}
},
/**
* Unregisters the component with the physics system.
*/
pause: function() {
if (this.addedToSystem) {
this.system.removeComponent(this);
this.system.removeBody(this.body);
this.addedToSystem = false;
}
},
/**
* Updates the rigid body instance, where possible.
*/
update: function(prevData) {
if (this.isLoaded) {
if (!this.hasUpdated) {
//skip the first update
this.hasUpdated = true;
return;
}
const data = this.data;
if (prevData.type !== data.type || prevData.disableCollision !== data.disableCollision) {
this.updateCollisionFlags();
}
if (prevData.activationState !== data.activationState) {
this.body.forceActivationState(ACTIVATION_STATES.indexOf(data.activationState) + 1);
if (data.activationState === ACTIVATION_STATE.ACTIVE_TAG) {
this.body.activate(true);
}
}
if (
prevData.collisionFilterGroup !== data.collisionFilterGroup ||
prevData.collisionFilterMask !== data.collisionFilterMask
) {
const broadphaseProxy = this.body.getBroadphaseProxy();
broadphaseProxy.set_m_collisionFilterGroup(data.collisionFilterGroup);
broadphaseProxy.set_m_collisionFilterMask(data.collisionFilterMask);
this.system.driver.broadphase
.getOverlappingPairCache()
.removeOverlappingPairsContainingProxy(broadphaseProxy, this.system.driver.dispatcher);
}
if (prevData.linearDamping != data.linearDamping || prevData.angularDamping != data.angularDamping) {
this.body.setDamping(data.linearDamping, data.angularDamping);
}
if (!almostEqualsVector3(0.001, prevData.gravity, data.gravity)) {
this._updateBodyGravity(data.gravity)
}
if (
prevData.linearSleepingThreshold != data.linearSleepingThreshold ||
prevData.angularSleepingThreshold != data.angularSleepingThreshold
) {
this.body.setSleepingThresholds(data.linearSleepingThreshold, data.angularSleepingThreshold);
}
if (!almostEqualsVector3(0.001, prevData.angularFactor, data.angularFactor)) {
const angularFactor = new Ammo.btVector3(data.angularFactor.x, data.angularFactor.y, data.angularFactor.z);
this.body.setAngularFactor(angularFactor);
Ammo.destroy(angularFactor);
}
if (prevData.restitution != data.restitution ) {
console.warn("ammo-body restitution cannot be updated from its initial value.")
}
//TODO: support dynamic update for other properties
}
},
/**
* Removes the component and all physics and scene side effects.
*/
remove: function() {
if (this.triMesh) Ammo.destroy(this.triMesh);
if (this.localScaling) Ammo.destroy(this.localScaling);
if (this.compoundShape) Ammo.destroy(this.compoundShape);
if (this.body) {
Ammo.destroy(this.body);
delete this.body;
}
Ammo.destroy(this.rbInfo);
Ammo.destroy(this.msTransform);
Ammo.destroy(this.motionState);
Ammo.destroy(this.localInertia);
Ammo.destroy(this.rotation);
},
beforeStep: function() {
this._updateShapes();
// Note that since static objects don't move,
// we don't sync them to physics on a routine basis.
if (this.data.type === TYPE.KINEMATIC) {
this.syncToPhysics();
}
},
step: function() {
if (this.data.type === TYPE.DYNAMIC) {
this.syncFromPhysics();
}
},
/**
* Updates the rigid body's position, velocity, and rotation, based on the scene.
*/
syncToPhysics: (function() {
const q = new THREE.Quaternion();
const v = new THREE.Vector3();
const q2 = new THREE.Vector3();
const v2 = new THREE.Vector3();
return function() {
const el = this.el,
parentEl = el.parentEl,
body = this.body;
if (!body) return;
this.motionState.getWorldTransform(this.msTransform);
if (parentEl.isScene) {
v.copy(el.object3D.position);
q.copy(el.object3D.quaternion);
} else {
el.object3D.getWorldPosition(v);
el.object3D.getWorldQuaternion(q);
}
const position = this.msTransform.getOrigin();
v2.set(position.x(), position.y(), position.z());
const quaternion = this.msTransform.getRotation();
q2.set(quaternion.x(), quaternion.y(), quaternion.z(), quaternion.w());
if (!almostEqualsVector3(0.001, v, v2) || !almostEqualsQuaternion(0.001, q, q2)) {
if (!this.body.isActive()) {
this.body.activate(true);
}
this.msTransform.getOrigin().setValue(v.x, v.y, v.z);
this.rotation.setValue(q.x, q.y, q.z, q.w);
this.msTransform.setRotation(this.rotation);
this.motionState.setWorldTransform(this.msTransform);
if (this.data.type !== TYPE.KINEMATIC) {
this.body.setCenterOfMassTransform(this.msTransform);
}
}
};
})(),
/**
* Updates the scene object's position and rotation, based on the physics simulation.
*/
syncFromPhysics: (function() {
const v = new THREE.Vector3(),
q1 = new THREE.Quaternion(),
q2 = new THREE.Quaternion();
return function() {
this.motionState.getWorldTransform(this.msTransform);
const position = this.msTransform.getOrigin();
const quaternion = this.msTransform.getRotation();
const el = this.el,
body = this.body;
// For the parent, prefer to use the THHREE.js scene graph parent (if it can be determined)
// and only use the HTML scene graph parent as a fallback.
// Usually these are the same, but there are various cases where it's useful to modify the THREE.js
// scene graph so that it deviates from the HTML.
// In these cases the THREE.js scene graph should be considered the definitive reference in terms
// of object positioning etc.
// For specific examples, and more discussion, see:
// https://github.com/c-frame/aframe-physics-system/pull/1#issuecomment-1264686433
const parentEl = el.object3D.parent.el ? el.object3D.parent.el : el.parentEl;
if (!body) return;
if (!parentEl) return;
if (parentEl.isScene) {
el.object3D.position.set(position.x(), position.y(), position.z());
el.object3D.quaternion.set(quaternion.x(), quaternion.y(), quaternion.z(), quaternion.w());
} else {
q1.set(quaternion.x(), quaternion.y(), quaternion.z(), quaternion.w());
parentEl.object3D.getWorldQuaternion(q2);
q1.multiply(q2.invert());
el.object3D.quaternion.copy(q1);
v.set(position.x(), position.y(), position.z());
parentEl.object3D.worldToLocal(v);
el.object3D.position.copy(v);
}
};
})(),
addShapeComponent: function(shapeComponent) {
if (shapeComponent.data.type === SHAPE.MESH && this.data.type !== TYPE.STATIC) {
console.warn("non-static mesh colliders not supported");
return;
}
this.shapeComponents.push(shapeComponent);
this.shapeComponentsChanged = true;
},
removeShapeComponent: function(shapeComponent) {
const index = this.shapeComponents.indexOf(shapeComponent);
if (this.compoundShape && index !== -1 && this.body) {
const shapes = shapeComponent.getShapes();
for (var i = 0; i < shapes.length; i++) {
this.compoundShape.removeChildShape(shapes[i]);
}
this.shapeComponentsChanged = true;
this.shapeComponents.splice(index, 1);
}
},
updateMass: function() {
const shape = this.body.getCollisionShape();
const mass = this.data.type === TYPE.DYNAMIC ? this.data.mass : 0;
shape.calculateLocalInertia(mass, this.localInertia);
this.body.setMassProps(mass, this.localInertia);
this.body.updateInertiaTensor();
},
updateCollisionFlags: function() {
let flags = this.data.disableCollision ? 4 : 0;
switch (this.data.type) {
case TYPE.STATIC:
flags |= COLLISION_FLAG.STATIC_OBJECT;
break;
case TYPE.KINEMATIC:
flags |= COLLISION_FLAG.KINEMATIC_OBJECT;
break;
default:
this.body.applyGravity();
break;
}
this.body.setCollisionFlags(flags);
this.updateMass();
// TODO: enable CCD if dynamic?
// this.body.setCcdMotionThreshold(0.001);
// this.body.setCcdSweptSphereRadius(0.001);
this.system.driver.updateBody(this.body);
},
getVelocity: function() {
return this.body.getLinearVelocity();
}
};
module.exports.definition = AmmoBody;
module.exports.Component = AFRAME.registerComponent("ammo-body", AmmoBody);