Source: framework/components/rigid-body/system.js

Object.assign(pc, function () {
    var ammoRayStart, ammoRayEnd;

    var collisions = {};
    var frameCollisions = {};


    // DEPRECATED WARNINGS
    var WARNED_RAYCAST_CALLBACK = false;

    /**
     * @constructor
     * @name pc.RaycastResult
     * @classdesc Object holding the result of a successful raycast hit
     * @description Create a new RaycastResult
     * @param {pc.Entity} entity The entity that was hit
     * @param {pc.Vec3} point The point at which the ray hit the entity in world space
     * @param {pc.Vec3} normal The normal vector of the surface where the ray hit in world space.
     * @property {pc.Entity} entity The entity that was hit
     * @property {pc.Vec3} point The point at which the ray hit the entity in world space
     * @property {pc.Vec3} normal The normal vector of the surface where the ray hit in world space.
     */
    var RaycastResult = function RaycastResult(entity, point, normal) {
        this.entity = entity;
        this.point = point;
        this.normal = normal;
    };

    /**
     * @constructor
     * @name pc.SingleContactResult
     * @classdesc Object holding the result of a contact between two rigid bodies
     * @description Create a new SingleContactResult
     * @param {pc.Entity} a The first entity involved in the contact
     * @param {pc.Entity} b The second entity involved in the contact
     * @param {pc.ContactPoint} contactPoint The contact point between the two entities
     * @property {pc.Entity} a The first entity involved in the contact
     * @property {pc.Entity} b The second entity involved in the contact
     * @property {pc.Vec3} localPointA The point on Entity A where the contact occurred, relative to A
     * @property {pc.Vec3} localPointB The point on Entity B where the contact occurred, relative to B
     * @property {pc.Vec3} pointA The point on Entity A where the contact occurred, in world space
     * @property {pc.Vec3} pointB The point on Entity B where the contact occurred, in world space
     * @property {pc.Vec3} normal The normal vector of the contact on Entity B, in world space
     */
    var SingleContactResult = function SingleContactResult(a, b, contactPoint) {
        if (arguments.length === 0) {
            this.a = null;
            this.b = null;
            this.localPointA = new pc.Vec3();
            this.localPointB = new pc.Vec3();
            this.pointA = new pc.Vec3();
            this.pointB = new pc.Vec3();
            this.normal = new pc.Vec3();
        } else {
            this.a = a;
            this.b = b;
            this.localPointA = contactPoint.localPoint;
            this.localPointB = contactPoint.localPointOther;
            this.pointA = contactPoint.point;
            this.pointB = contactPoint.pointOther;
            this.normal = contactPoint.normal;
        }
    };

    /**
     * @constructor
     * @name pc.ContactPoint
     * @classdesc Object holding the result of a contact between two Entities.
     * @description Create a new ContactPoint
     * @param {pc.Vec3} localPoint The point on the entity where the contact occurred, relative to the entity
     * @param {pc.Vec3} localPointOther The point on the other entity where the contact occurred, relative to the other entity
     * @param {pc.Vec3} point The point on the entity where the contact occurred, in world space
     * @param {pc.Vec3} pointOther The point on the other entity where the contact occurred, in world space
     * @param {pc.Vec3} normal The normal vector of the contact on the other entity, in world space
     * @property {pc.Vec3} localPoint The point on the entity where the contact occurred, relative to the entity
     * @property {pc.Vec3} localPointOther The point on the other entity where the contact occurred, relative to the other entity
     * @property {pc.Vec3} point The point on the entity where the contact occurred, in world space
     * @property {pc.Vec3} pointOther The point on the other entity where the contact occurred, in world space
     * @property {pc.Vec3} normal The normal vector of the contact on the other entity, in world space
     */
    var ContactPoint = function ContactPoint(localPoint, localPointOther, point, pointOther, normal) {
        if (arguments.length === 0) {
            this.localPoint = new pc.Vec3();
            this.localPointOther = new pc.Vec3();
            this.point = new pc.Vec3();
            this.pointOther = new pc.Vec3();
            this.normal = new pc.Vec3();
        } else {
            this.localPoint = localPoint;
            this.localPointOther = localPointOther;
            this.point = point;
            this.pointOther = pointOther;
            this.normal = normal;
        }
    };

    /**
     * @constructor
     * @name pc.ContactResult
     * @classdesc Object holding the result of a contact between two Entities
     * @description Create a new ContactResult
     * @param {pc.Entity} other The entity that was involved in the contact with this entity
     * @param {pc.ContactPoint[]} contacts An array of ContactPoints with the other entity
     * @property {pc.Entity} other The entity that was involved in the contact with this entity
     * @property {pc.ContactPoint[]} contacts An array of ContactPoints with the other entity
     */
    var ContactResult = function ContactResult(other, contacts) {
        this.other = other;
        this.contacts = contacts;
    };

    // Events Documentation
    /**
     * @event
     * @name pc.RigidBodyComponentSystem#contact
     * @description Fired when a contact occurs between two rigid bodies
     * @param {pc.SingleContactResult} result Details of the contact between the two bodies
     */

    var _schema = [
        'enabled',
        'type',
        'mass',
        'linearDamping',
        'angularDamping',
        'linearFactor',
        'angularFactor',
        'friction',
        'restitution',
        'group',
        'mask',
        'body'
    ];

    /**
     * @constructor
     * @name pc.RigidBodyComponentSystem
     * @classdesc The RigidBodyComponentSystem maintains the dynamics world for simulating rigid bodies,
     * it also controls global values for the world such as gravity. Note: The RigidBodyComponentSystem
     * is only valid if 3D Physics is enabled in your application. You can enable this in the application
     * settings for your project.
     * @description Create a new RigidBodyComponentSystem
     * @param {pc.Application} app The Application
     * @extends pc.ComponentSystem
     * @property {pc.Vec3} gravity The world space vector representing global gravity in the physics simulation.
     * Defaults to [0, -9.81, 0] which is an approximation of the gravitational force on Earth.
     */
    var RigidBodyComponentSystem = function RigidBodyComponentSystem(app) {
        pc.ComponentSystem.call(this, app);

        this.id = 'rigidbody';
        this.description = "Adds the entity to the scene's physical simulation.";
        this._stats = app.stats.frame;

        this.ComponentType = pc.RigidBodyComponent;
        this.DataType = pc.RigidBodyComponentData;

        this.contactPointPool = new pc.AllocatePool(ContactPoint, 1);
        this.contactResultPool = new pc.AllocatePool(ContactResult, 1);
        this.singleContactResultPool = new pc.AllocatePool(SingleContactResult, 1);

        this.schema = _schema;

        this.maxSubSteps = 10;
        this.fixedTimeStep = 1 / 60;
        this.gravity = new pc.Vec3(0, -9.81, 0);

        this.on('remove', this.onRemove, this);
    };
    RigidBodyComponentSystem.prototype = Object.create(pc.ComponentSystem.prototype);
    RigidBodyComponentSystem.prototype.constructor = RigidBodyComponentSystem;

    pc.Component._buildAccessors(pc.RigidBodyComponent.prototype, _schema);

    Object.assign(RigidBodyComponentSystem.prototype, {
        onLibraryLoaded: function () {
            // Create the Ammo physics world
            if (typeof Ammo !== 'undefined') {
                var collisionConfiguration = new Ammo.btDefaultCollisionConfiguration();
                var dispatcher = new Ammo.btCollisionDispatcher(collisionConfiguration);
                var overlappingPairCache = new Ammo.btDbvtBroadphase();
                var solver = new Ammo.btSequentialImpulseConstraintSolver();
                this.dynamicsWorld = new Ammo.btDiscreteDynamicsWorld(dispatcher, overlappingPairCache, solver, collisionConfiguration);

                // Lazily create temp vars
                ammoRayStart = new Ammo.btVector3();
                ammoRayEnd = new Ammo.btVector3();
                pc.ComponentSystem.bind('update', this.onUpdate, this);
            } else {
                // Unbind the update function if we haven't loaded Ammo by now
                pc.ComponentSystem.unbind('update', this.onUpdate, this);
            }
        },

        initializeComponentData: function (component, _data, properties) {
            properties = ['enabled', 'mass', 'linearDamping', 'angularDamping', 'linearFactor', 'angularFactor', 'friction', 'restitution', 'type', 'group', 'mask'];

            // duplicate the input data because we are modifying it
            var data = {};
            for (var i = 0, len = properties.length; i < len; i++) {
                var property = properties[i];
                data[property] = _data[property];
            }

            // backwards compatibility
            if (_data.bodyType) {
                data.type = _data.bodyType;
                console.warn("WARNING: rigidbody.bodyType: Property is deprecated. Use type instead.");
            }

            if (data.linearFactor && pc.type(data.linearFactor) === 'array') {
                data.linearFactor = new pc.Vec3(data.linearFactor[0], data.linearFactor[1], data.linearFactor[2]);
            }
            if (data.angularFactor && pc.type(data.angularFactor) === 'array') {
                data.angularFactor = new pc.Vec3(data.angularFactor[0], data.angularFactor[1], data.angularFactor[2]);
            }

            pc.ComponentSystem.prototype.initializeComponentData.call(this, component, data, properties);
        },

        cloneComponent: function (entity, clone) {
            // create new data block for clone
            var data = {
                enabled: entity.rigidbody.enabled,
                mass: entity.rigidbody.mass,
                linearDamping: entity.rigidbody.linearDamping,
                angularDamping: entity.rigidbody.angularDamping,
                linearFactor: [entity.rigidbody.linearFactor.x, entity.rigidbody.linearFactor.y, entity.rigidbody.linearFactor.z],
                angularFactor: [entity.rigidbody.angularFactor.x, entity.rigidbody.angularFactor.y, entity.rigidbody.angularFactor.z],
                friction: entity.rigidbody.friction,
                restitution: entity.rigidbody.restitution,
                type: entity.rigidbody.type,
                group: entity.rigidbody.group,
                mask: entity.rigidbody.mask
            };

            this.addComponent(clone, data);
        },

        onRemove: function (entity, data) {
            if (data.body) {
                this.removeBody(data.body);
                Ammo.destroy(data.body);
            }

            data.body = null;
        },

        addBody: function (body, group, mask) {
            if (group !== undefined && mask !== undefined) {
                this.dynamicsWorld.addRigidBody(body, group, mask);
            } else {
                this.dynamicsWorld.addRigidBody(body);
            }

            return body;
        },

        removeBody: function (body) {
            this.dynamicsWorld.removeRigidBody(body);
        },

        addConstraint: function (constraint) {
            this.dynamicsWorld.addConstraint(constraint);
            return constraint;
        },

        removeConstraint: function (constraint) {
            this.dynamicsWorld.removeConstraint(constraint);
        },

        /**
         * @function
         * @name pc.RigidBodyComponentSystem#raycastFirst
         * @description Raycast the world and return the first entity the ray hits. Fire a ray into the world from start to end,
         * if the ray hits an entity with a collision component, it returns a {@link pc.RaycastResult}, otherwise returns null.
         * @param {pc.Vec3} start The world space point where the ray starts
         * @param {pc.Vec3} end The world space point where the ray ends
         * @returns {pc.RaycastResult} The result of the raycasting or null if there was no hit.
         */
        raycastFirst: function (start, end) {
            var result = null;

            ammoRayStart.setValue(start.x, start.y, start.z);
            ammoRayEnd.setValue(end.x, end.y, end.z);
            var rayCallback = new Ammo.ClosestRayResultCallback(ammoRayStart, ammoRayEnd);

            this.dynamicsWorld.rayTest(ammoRayStart, ammoRayEnd, rayCallback);
            if (rayCallback.hasHit()) {
                var collisionObj = rayCallback.get_m_collisionObject();
                var body = Ammo.castObject(collisionObj, Ammo.btRigidBody);
                if (body) {
                    var point = rayCallback.get_m_hitPointWorld();
                    var normal = rayCallback.get_m_hitNormalWorld();

                    result = new RaycastResult(
                        body.entity,
                        new pc.Vec3(point.x(), point.y(), point.z()),
                        new pc.Vec3(normal.x(), normal.y(), normal.z())
                    );

                    // keeping for backwards compatibility
                    if (arguments.length > 2) {
                        var callback = arguments[2];
                        callback(result);

                        if (!WARNED_RAYCAST_CALLBACK) {
                            console.warn('[DEPRECATED]: pc.RigidBodyComponentSystem#rayCastFirst no longer requires a callback. The result of the raycast is returned by the function instead.');
                            WARNED_RAYCAST_CALLBACK = true;
                        }
                    }
                }
            }

            Ammo.destroy(rayCallback);

            return result;
        },

        /**
         * @private
         * @function
         * @name pc.RigidBodyComponentSystem#_storeCollision
         * @description Stores a collision between the entity and other in the contacts map and returns true if it is a new collision
         * @param {pc.Entity} entity The entity
         * @param {pc.Entity} other The entity that collides with the first entity
         * @returns {Boolean} true if this is a new collision, false otherwise.
         */
        _storeCollision: function (entity, other) {
            var isNewCollision = false;
            var guid = entity.getGuid();

            collisions[guid] = collisions[guid] || { others: [], entity: entity };

            if (collisions[guid].others.indexOf(other) < 0) {
                collisions[guid].others.push(other);
                isNewCollision = true;
            }

            frameCollisions[guid] = frameCollisions[guid] || { others: [], entity: entity };
            frameCollisions[guid].others.push(other);

            return isNewCollision;
        },

        _createContactPointFromAmmo: function (contactPoint) {
            var contact = this.contactPointPool.allocate();

            contact.localPoint.set(contactPoint.get_m_localPointA().x(), contactPoint.get_m_localPointA().y(), contactPoint.get_m_localPointA().z());
            contact.localPointOther.set(contactPoint.get_m_localPointB().x(), contactPoint.get_m_localPointB().y(), contactPoint.get_m_localPointB().z());
            contact.point.set(contactPoint.getPositionWorldOnA().x(), contactPoint.getPositionWorldOnA().y(), contactPoint.getPositionWorldOnA().z());
            contact.pointOther.set(contactPoint.getPositionWorldOnB().x(), contactPoint.getPositionWorldOnB().y(), contactPoint.getPositionWorldOnB().z());
            contact.normal.set(contactPoint.get_m_normalWorldOnB().x(), contactPoint.get_m_normalWorldOnB().y(), contactPoint.get_m_normalWorldOnB().z());

            return contact;
        },

        _createReverseContactPointFromAmmo: function (contactPoint) {
            var contact = this.contactPointPool.allocate();

            contact.localPointOther.set(contactPoint.get_m_localPointA().x(), contactPoint.get_m_localPointA().y(), contactPoint.get_m_localPointA().z());
            contact.localPoint.set(contactPoint.get_m_localPointB().x(), contactPoint.get_m_localPointB().y(), contactPoint.get_m_localPointB().z());
            contact.pointOther.set(contactPoint.getPositionWorldOnA().x(), contactPoint.getPositionWorldOnA().y(), contactPoint.getPositionWorldOnA().z());
            contact.point.set(contactPoint.getPositionWorldOnB().x(), contactPoint.getPositionWorldOnB().y(), contactPoint.getPositionWorldOnB().z());
            contact.normal.set(contactPoint.get_m_normalWorldOnB().x(), contactPoint.get_m_normalWorldOnB().y(), contactPoint.get_m_normalWorldOnB().z());
            return contact;
        },

        _createSingleContactResult: function (a, b, contactPoint) {
            var result = this.singleContactResultPool.allocate();

            result.a = a;
            result.b = b;
            result.localPointA = contactPoint.localPoint;
            result.localPointB = contactPoint.localPointOther;
            result.pointA = contactPoint.point;
            result.pointB = contactPoint.pointOther;
            result.normal = contactPoint.normal;

            return result;
        },

        _createContactResult: function (other, contacts) {
            var result = this.contactResultPool.allocate();
            result.other = other;
            result.contacts = contacts;
            return result;
        },

        /**
         * @private
         * @function
         * @name pc.RigidBodyComponentSystem#_cleanOldCollisions
         * @description Removes collisions that no longer exist from the collisions list and fires collisionend events to the
         * related entities.
         */
        _cleanOldCollisions: function () {
            for (var guid in collisions) {
                if (collisions.hasOwnProperty(guid)) {
                    var entity = collisions[guid].entity;
                    var entityCollision = entity.collision;
                    var others = collisions[guid].others;
                    var length = others.length;
                    var i = length;
                    while (i--) {
                        var other = others[i];
                        // if the contact does not exist in the current frame collisions then fire event
                        if (!frameCollisions[guid] || frameCollisions[guid].others.indexOf(other) < 0) {
                            // remove from others list
                            others.splice(i, 1);

                            if (entityCollision && other.collision) {
                                if (entity.rigidbody && other.rigidbody) {
                                    // if both are rigidbodies fire collision end
                                    entityCollision.fire("collisionend", other);
                                } else if (entity.trigger) {
                                    // if entity is a trigger
                                    entityCollision.fire("triggerleave", other);
                                }
                            }
                        }
                    }

                    if (others.length === 0) {
                        delete collisions[guid];
                    }
                }
            }
        },

        onUpdate: function (dt) {
            // #ifdef PROFILER
            this._stats.physicsStart = pc.now();
            // #endif

            // Check to see whether we need to update gravity on the dynamics world
            var gravity = this.dynamicsWorld.getGravity();
            if (gravity.x() !== this.gravity.x || gravity.y() !== this.gravity.y || gravity.z() !== this.gravity.z) {
                gravity.setValue(this.gravity.x, this.gravity.y, this.gravity.z);
                this.dynamicsWorld.setGravity(gravity);
            }

            // Update the transforms of all bodies
            this.dynamicsWorld.stepSimulation(dt, this.maxSubSteps, this.fixedTimeStep);

            // Update the transforms of all entities referencing a body
            var components = this.store;
            for (var id in components) {
                if (components.hasOwnProperty(id)) {
                    var entity = components[id].entity;
                    var componentData = components[id].data;
                    if (componentData.body && componentData.body.isActive() && componentData.enabled && entity.enabled) {
                        if (componentData.type === pc.BODYTYPE_DYNAMIC) {
                            entity.rigidbody.syncBodyToEntity();
                        } else if (componentData.type === pc.BODYTYPE_KINEMATIC) {
                            entity.rigidbody._updateKinematic(dt);
                        }
                    }

                }
            }

            // Check for collisions and fire callbacks
            var dispatcher = this.dynamicsWorld.getDispatcher();
            var numManifolds = dispatcher.getNumManifolds();
            var i, j;

            frameCollisions = {};

            // loop through the all contacts and fire events
            for (i = 0; i < numManifolds; i++) {
                var manifold = dispatcher.getManifoldByIndexInternal(i);
                var body0 = manifold.getBody0();
                var body1 = manifold.getBody1();
                var wb0 = Ammo.castObject(body0, Ammo.btRigidBody);
                var wb1 = Ammo.castObject(body1, Ammo.btRigidBody);
                var e0 = wb0.entity;
                var e1 = wb1.entity;

                // check if entity is null - TODO: investigate when this happens
                if (!e0 || !e1) {
                    continue;
                }

                var flags0 = body0.getCollisionFlags();
                var flags1 = body1.getCollisionFlags();

                var numContacts = manifold.getNumContacts();
                var forwardContacts = [];
                var reverseContacts = [];
                var newCollision, e0Events, e1Events;

                if (numContacts > 0) {
                    // don't fire contact events for triggers
                    if ((flags0 & pc.BODYFLAG_NORESPONSE_OBJECT) ||
                        (flags1 & pc.BODYFLAG_NORESPONSE_OBJECT)) {

                        e0Events = e0.collision ? e0.collision.hasEvent("triggerenter") || e0.collision.hasEvent("triggerleave") : false;
                        e1Events = e1.collision ? e1.collision.hasEvent("triggerenter") || e1.collision.hasEvent("triggerleave") : false;

                        if (e0Events) {
                            // fire triggerenter events
                            newCollision = this._storeCollision(e0, e1);
                            if (newCollision) {
                                if (e0.collision && !(flags1 & pc.BODYFLAG_NORESPONSE_OBJECT)) {
                                    e0.collision.fire("triggerenter", e1);
                                }
                            }
                        }

                        if (e1Events) {
                            newCollision = this._storeCollision(e1, e0);
                            if (newCollision) {
                                if (e1.collision && !(flags0 & pc.BODYFLAG_NORESPONSE_OBJECT)) {
                                    e1.collision.fire("triggerenter", e0);
                                }
                            }
                        }
                    } else {
                        e0Events = e0.collision ? e0.collision.hasEvent("collisionstart")  || e0.collision.hasEvent("collisionend") || e0.collision.hasEvent("contact") : false;
                        e1Events = e1.collision ? e1.collision.hasEvent("collisionstart") || e1.collision.hasEvent("collisionend") || e1.collision.hasEvent("contact") : false;
                        var globalEvents = this.hasEvent("contact");

                        if (globalEvents || e0Events || e1Events) {
                            for (j = 0; j < numContacts; j++) {
                                var btContactPoint = manifold.getContactPoint(j);

                                var contactPoint = this._createContactPointFromAmmo(btContactPoint);
                                var reverseContactPoint = null;
                                if (e0Events || e1Events) {
                                    reverseContactPoint = this._createReverseContactPointFromAmmo(btContactPoint);
                                    forwardContacts.push(contactPoint);
                                    reverseContacts.push(reverseContactPoint);
                                }

                                if (globalEvents) {
                                    // fire global contact event for every contact
                                    var result = this._createSingleContactResult(e0, e1, contactPoint);
                                    this.fire("contact", result);
                                }
                            }

                            if (e0Events) {
                                var forwardResult = this._createContactResult(e1, forwardContacts);

                                // fire contact events on collision volume
                                if (e0.collision) {
                                    e0.collision.fire("contact", forwardResult);
                                }

                                // fire collisionstart events
                                newCollision = this._storeCollision(e0, e1);
                                if (newCollision && e0.collision) {
                                    e0.collision.fire("collisionstart", forwardResult);
                                }
                            }

                            if (e1Events) {
                                var reverseResult = this._createContactResult(e0, reverseContacts);

                                if (e1.collision) {
                                    e1.collision.fire("contact", reverseResult);
                                }

                                newCollision = this._storeCollision(e1, e0);
                                if (newCollision && e1.collision) {
                                    e1.collision.fire("collisionstart", reverseResult);
                                }
                            }
                        }
                    }

                }
            }

            // check for collisions that no longer exist and fire events
            this._cleanOldCollisions();

            // Reset contact pools
            this.contactPointPool.freeAll();
            this.contactResultPool.freeAll();
            this.singleContactResultPool.freeAll();

            // #ifdef PROFILER
            this._stats.physicsTime = pc.now() - this._stats.physicsStart;
            // #endif
        }


    });

    return {
        // DEPRECATED ENUMS - see rigidbody_constants.js
        RIGIDBODY_TYPE_STATIC: 'static',
        RIGIDBODY_TYPE_DYNAMIC: 'dynamic',
        RIGIDBODY_TYPE_KINEMATIC: 'kinematic',
        RIGIDBODY_CF_STATIC_OBJECT: 1,
        RIGIDBODY_CF_KINEMATIC_OBJECT: 2,
        RIGIDBODY_CF_NORESPONSE_OBJECT: 4,
        RIGIDBODY_ACTIVE_TAG: 1,
        RIGIDBODY_ISLAND_SLEEPING: 2,
        RIGIDBODY_WANTS_DEACTIVATION: 3,
        RIGIDBODY_DISABLE_DEACTIVATION: 4,
        RIGIDBODY_DISABLE_SIMULATION: 5,

        RigidBodyComponentSystem: RigidBodyComponentSystem
    };
}());