UNPKG

@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
/* 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);