Source: framework/components/collision/system.js

Object.assign(pc, function () {
    var _schema = [
        'enabled',
        'type',
        'halfExtents',
        'radius',
        'axis',
        'height',
        'asset',
        'shape',
        'model'
    ];

    // Collision system implementations
    var CollisionSystemImpl = function (system) {
        this.system = system;
    };

    Object.assign(CollisionSystemImpl.prototype, {
        // Called before the call to system.super.initializeComponentData is made
        beforeInitialize: function (component, data) {
            data.shape = this.createPhysicalShape(component.entity, data);

            data.model = new pc.Model();
            data.model.graph = new pc.GraphNode();
        },

        // Called after the call to system.super.initializeComponentData is made
        afterInitialize: function (component, data) {
            this.recreatePhysicalShapes(component);
            component.data.initialized = true;
        },

        // Called when a collision component changes type in order to recreate debug and physical shapes
        reset: function (component, data) {
            this.beforeInitialize(component, data);
            this.afterInitialize(component, data);
        },

        // Re-creates rigid bodies / triggers
        recreatePhysicalShapes: function (component) {
            var entity = component.entity;
            var data = component.data;

            if (typeof Ammo !== 'undefined') {
                data.shape = this.createPhysicalShape(component.entity, data);
                if (entity.rigidbody) {
                    entity.rigidbody.disableSimulation();
                    entity.rigidbody.createBody();
                } else {
                    if (!entity.trigger) {
                        entity.trigger = new pc.Trigger(this.system.app, component, data);
                    } else {
                        entity.trigger.initialize(data);
                    }
                }
            }
        },

        // Creates a physical shape for the collision. This consists
        // of the actual shape that will be used for the rigid bodies / triggers of
        // the collision.
        createPhysicalShape: function (entity, data) {
            return undefined;
        },

        updateTransform: function (component, position, rotation, scale) {
            if (component.entity.trigger) {
                component.entity.trigger.syncEntityToBody();
            }
        },

        // Called when the collision is removed
        remove: function (entity, data) {
            var app = this.system.app;
            if (entity.rigidbody && entity.rigidbody.body) {
                app.systems.rigidbody.removeBody(entity.rigidbody.body);
                entity.rigidbody.disableSimulation();
            }

            if (entity.trigger) {
                entity.trigger.destroy();
                delete entity.trigger;
            }

            if (app.scene.containsModel(data.model)) {
                app.root.removeChild(data.model.graph);
                app.scene.removeModel(data.model);
            }
        },

        // Called when the collision is cloned to another entity
        clone: function (entity, clone) {
            var src = this.system.store[entity.getGuid()];

            var data = {
                enabled: src.data.enabled,
                type: src.data.type,
                halfExtents: [src.data.halfExtents.x, src.data.halfExtents.y, src.data.halfExtents.z],
                radius: src.data.radius,
                axis: src.data.axis,
                height: src.data.height,
                asset: src.data.asset,
                model: src.data.model
            };

            return this.system.addComponent(clone, data);
        }
    });

    // Box Collision System
    var CollisionBoxSystemImpl = function (system) {
        CollisionSystemImpl.call(this, system);
    };
    CollisionBoxSystemImpl.prototype = Object.create(CollisionSystemImpl.prototype);
    CollisionBoxSystemImpl.prototype.constructor = CollisionBoxSystemImpl;

    Object.assign(CollisionBoxSystemImpl.prototype, {
        createPhysicalShape: function (entity, data) {
            if (typeof Ammo !== 'undefined') {
                var he = data.halfExtents;
                var ammoHe = new Ammo.btVector3(he ? he.x : 0.5, he ? he.y : 0.5, he ? he.z : 0.5);
                return new Ammo.btBoxShape(ammoHe);
            }
            return undefined;
        }
    });

    // Sphere Collision System
    var CollisionSphereSystemImpl = function (system) {
        CollisionSystemImpl.call(this, system);
    };
    CollisionSphereSystemImpl.prototype = Object.create(CollisionSystemImpl.prototype);
    CollisionSphereSystemImpl.prototype.constructor = CollisionSphereSystemImpl;

    Object.assign(CollisionSphereSystemImpl.prototype, {
        createPhysicalShape: function (entity, data) {
            if (typeof Ammo !== 'undefined') {
                return new Ammo.btSphereShape(data.radius);
            }
            return undefined;
        }
    });

    // Capsule Collision System
    var CollisionCapsuleSystemImpl = function (system) {
        CollisionSystemImpl.call(this, system);
    };
    CollisionCapsuleSystemImpl.prototype = Object.create(CollisionSystemImpl.prototype);
    CollisionCapsuleSystemImpl.prototype.constructor = CollisionCapsuleSystemImpl;

    Object.assign(CollisionCapsuleSystemImpl.prototype, {
        createPhysicalShape: function (entity, data) {
            var shape = null;
            var axis = (data.axis !== undefined) ? data.axis : 1;
            var radius = data.radius || 0.5;
            var height = Math.max((data.height || 2) - 2 * radius, 0);

            if (typeof Ammo !== 'undefined') {
                switch (axis) {
                    case 0:
                        shape = new Ammo.btCapsuleShapeX(radius, height);
                        break;
                    case 1:
                        shape = new Ammo.btCapsuleShape(radius, height);
                        break;
                    case 2:
                        shape = new Ammo.btCapsuleShapeZ(radius, height);
                        break;
                }
            }
            return shape;
        }
    });

    // Cylinder Collision System
    var CollisionCylinderSystemImpl = function (system) {
        CollisionSystemImpl.call(this, system);
    };
    CollisionCylinderSystemImpl.prototype = Object.create(CollisionSystemImpl.prototype);
    CollisionCylinderSystemImpl.prototype.constructor = CollisionCylinderSystemImpl;

    Object.assign(CollisionCylinderSystemImpl.prototype, {
        createPhysicalShape: function (entity, data) {
            var halfExtents = null;
            var shape = null;
            var axis = (data.axis !== undefined) ? data.axis : 1;
            var radius = (data.radius !== undefined) ? data.radius : 0.5;
            var height = (data.height !== undefined) ? data.height : 1;

            if (typeof Ammo !== 'undefined') {
                switch (axis) {
                    case 0:
                        halfExtents = new Ammo.btVector3(height * 0.5, radius, radius);
                        shape = new Ammo.btCylinderShapeX(halfExtents);
                        break;
                    case 1:
                        halfExtents = new Ammo.btVector3(radius, height * 0.5, radius);
                        shape = new Ammo.btCylinderShape(halfExtents);
                        break;
                    case 2:
                        halfExtents = new Ammo.btVector3(radius, radius, height * 0.5);
                        shape = new Ammo.btCylinderShapeZ(halfExtents);
                        break;
                }
            }
            return shape;
        }
    });

    // Mesh Collision System
    var CollisionMeshSystemImpl = function (system) {
        CollisionSystemImpl.call(this, system);
    };
    CollisionMeshSystemImpl.prototype = Object.create(CollisionSystemImpl.prototype);
    CollisionMeshSystemImpl.prototype.constructor = CollisionMeshSystemImpl;

    Object.assign(CollisionMeshSystemImpl.prototype, {
        // override for the mesh implementation because the asset model needs
        // special handling
        beforeInitialize: function (component, data) {},

        createPhysicalShape: function (entity, data) {
            if (typeof Ammo !== 'undefined' && data.model) {
                var model = data.model;
                var shape = new Ammo.btCompoundShape();

                var i, j;
                for (i = 0; i < model.meshInstances.length; i++) {
                    var meshInstance = model.meshInstances[i];
                    var mesh = meshInstance.mesh;
                    var ib = mesh.indexBuffer[pc.RENDERSTYLE_SOLID];
                    var vb = mesh.vertexBuffer;

                    var format = vb.getFormat();
                    var stride = format.size / 4;
                    var positions;
                    for (j = 0; j < format.elements.length; j++) {
                        var element = format.elements[j];
                        if (element.name === pc.SEMANTIC_POSITION) {
                            positions = new Float32Array(vb.lock(), element.offset);
                        }
                    }

                    var indices = new Uint16Array(ib.lock());
                    var numTriangles = mesh.primitive[0].count / 3;

                    var v1 = new Ammo.btVector3();
                    var v2 = new Ammo.btVector3();
                    var v3 = new Ammo.btVector3();
                    var i1, i2, i3;

                    var base = mesh.primitive[0].base;
                    var triMesh = new Ammo.btTriangleMesh();
                    for (j = 0; j < numTriangles; j++) {
                        i1 = indices[base + j * 3] * stride;
                        i2 = indices[base + j * 3 + 1] * stride;
                        i3 = indices[base + j * 3 + 2] * stride;
                        v1.setValue(positions[i1], positions[i1 + 1], positions[i1 + 2]);
                        v2.setValue(positions[i2], positions[i2 + 1], positions[i2 + 2]);
                        v3.setValue(positions[i3], positions[i3 + 1], positions[i3 + 2]);
                        triMesh.addTriangle(v1, v2, v3, true);
                    }

                    var useQuantizedAabbCompression = true;
                    var triMeshShape = new Ammo.btBvhTriangleMeshShape(triMesh, useQuantizedAabbCompression);

                    var wtm = meshInstance.node.getWorldTransform();
                    var scl = wtm.getScale();
                    triMeshShape.setLocalScaling(new Ammo.btVector3(scl.x, scl.y, scl.z));

                    var pos = meshInstance.node.getPosition();
                    var rot = meshInstance.node.getRotation();

                    var transform = new Ammo.btTransform();
                    transform.setIdentity();
                    transform.getOrigin().setValue(pos.x, pos.y, pos.z);

                    var ammoQuat = new Ammo.btQuaternion();
                    ammoQuat.setValue(rot.x, rot.y, rot.z, rot.w);
                    transform.setRotation(ammoQuat);

                    shape.addChildShape(transform, triMeshShape);
                }

                var entityTransform = entity.getWorldTransform();
                var scale = entityTransform.getScale();
                var vec = new Ammo.btVector3();
                vec.setValue(scale.x, scale.y, scale.z);
                shape.setLocalScaling(vec);

                return shape;
            }
            return undefined;
        },

        recreatePhysicalShapes: function (component) {
            var data = component.data;

            if (data.asset !== null && component.enabled && component.entity.enabled) {
                this.loadModelAsset(component);
            } else {
                this.doRecreatePhysicalShape(component);
            }
        },

        loadModelAsset: function (component) {
            var self = this;
            var id = component.data.asset;
            var data = component.data;
            var assets = this.system.app.assets;

            var asset = assets.get(id);
            if (asset) {
                asset.ready(function (asset) {
                    data.model = asset.resource;
                    self.doRecreatePhysicalShape(component);
                });
                assets.load(asset);
            } else {
                assets.once("add:" + id, function (asset) {
                    asset.ready(function (asset) {
                        data.model = asset.resource;
                        self.doRecreatePhysicalShape(component);
                    });
                    assets.load(asset);
                });
            }
        },

        doRecreatePhysicalShape: function (component) {
            var entity = component.entity;
            var data = component.data;

            if (data.model) {
                if (data.shape) {
                    Ammo.destroy(data.shape);
                }

                data.shape = this.createPhysicalShape(entity, data);

                if (entity.rigidbody) {
                    entity.rigidbody.createBody();
                } else {
                    if (!entity.trigger) {
                        entity.trigger = new pc.Trigger(this.system.app, component, data);
                    } else {
                        entity.trigger.initialize(data);
                    }

                }
            } else {
                this.remove(entity, data);
            }
        },

        updateTransform: function (component, position, rotation, scale) {
            if (component.shape) {
                var entityTransform = component.entity.getWorldTransform();
                var worldScale = entityTransform.getScale();

                // if the scale changed then recreate the shape
                var previousScale = component.shape.getLocalScaling();
                if (worldScale.x !== previousScale.x() ||
                    worldScale.y !== previousScale.y() ||
                    worldScale.z !== previousScale.z() ) {
                    this.doRecreatePhysicalShape(component);
                }
            }

            pc.CollisionSystemImpl.prototype.updateTransform.call(this, component, position, rotation, scale);
        }
    });

    /**
     * @constructor
     * @name pc.CollisionComponentSystem
     * @classdesc Manages creation of {@link pc.CollisionComponent}s.
     * @description Creates a new CollisionComponentSystem.
     * @param {pc.Application} app The running {pc.Application}
     * @extends pc.ComponentSystem
     */
    var CollisionComponentSystem = function CollisionComponentSystem(app) {
        pc.ComponentSystem.call(this, app);

        this.id = "collision";
        this.description = "Specifies a collision volume.";

        this.ComponentType = pc.CollisionComponent;
        this.DataType = pc.CollisionComponentData;

        this.schema = _schema;

        this.implementations = { };

        this.on('remove', this.onRemove, this);

        pc.ComponentSystem.bind('update', this.onUpdate, this);
    };
    CollisionComponentSystem.prototype = Object.create(pc.ComponentSystem.prototype);
    CollisionComponentSystem.prototype.constructor = CollisionComponentSystem;

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

    Object.assign(CollisionComponentSystem.prototype, {
        onLibraryLoaded: function () {
            if (typeof Ammo !== 'undefined') {
                //
            } 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 = ['type', 'halfExtents', 'radius', 'axis', 'height', 'shape', 'model', 'asset', 'enabled'];

            // 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];
            }

            // asset takes priority over model
            // but they are both trying to change the mesh
            // so remove one of them to avoid conflicts
            var idx;
            if (_data.hasOwnProperty('asset')) {
                idx = properties.indexOf('model');
                if (idx !== -1) {
                    properties.splice(idx, 1);
                }
            } else if (_data.hasOwnProperty('model')) {
                idx = properties.indexOf('asset');
                if (idx !== -1) {
                    properties.splice(idx, 1);
                }
            }

            if (!data.type) {
                data.type = component.data.type;
            }
            component.data.type = data.type;

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

            var impl = this._createImplementation(data.type);
            impl.beforeInitialize(component, data);

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

            impl.afterInitialize(component, data);
        },

        // Creates an implementation based on the collision type and caches it
        // in an internal implementations structure, before returning it.
        _createImplementation: function (type) {
            if (this.implementations[type] === undefined) {
                var impl;
                switch (type) {
                    case 'box':
                        impl = new CollisionBoxSystemImpl(this);
                        break;
                    case 'sphere':
                        impl = new CollisionSphereSystemImpl(this);
                        break;
                    case 'capsule':
                        impl = new CollisionCapsuleSystemImpl(this);
                        break;
                    case 'cylinder':
                        impl = new CollisionCylinderSystemImpl(this);
                        break;
                    case 'mesh':
                        impl = new CollisionMeshSystemImpl(this);
                        break;
                    default:
                        // #ifdef DEBUG
                        console.error("_createImplementation: Invalid collision system type: " + type);
                        // #endif
                }
                this.implementations[type] = impl;
            }

            return this.implementations[type];
        },

        // Gets an existing implementation for the specified entity
        _getImplementation: function (entity) {
            return this.implementations[entity.collision.data.type];
        },

        cloneComponent: function (entity, clone) {
            return this._getImplementation(entity).clone(entity, clone);
        },

        onRemove: function (entity, data) {
            this.implementations[data.type].remove(entity, data);
        },

        onUpdate: function (dt) {
            var id, entity, data;
            var components = this.store;

            for (id in components) {
                entity = components[id].entity;
                data = components[id].data;

                if (data.enabled && entity.enabled) {
                    if (!entity.rigidbody && entity.trigger) {
                        entity.trigger.syncEntityToBody();
                    }
                }
            }
        },

        onTransformChanged: function (component, position, rotation, scale) {
            this.implementations[component.data.type].updateTransform(component, position, rotation, scale);
        },

        // Destroys the previous collision type and creates a new one based on the new type provided
        changeType: function (component, previousType, newType) {
            this.implementations[previousType].remove( component.entity, component.data);
            this._createImplementation(newType).reset(component, component.data);
        },

        // Recreates rigid bodies or triggers for the specified component
        recreatePhysicalShapes: function (component) {
            this.implementations[component.data.type].recreatePhysicalShapes(component);
        }
    });

    return {
        CollisionComponentSystem: CollisionComponentSystem
    };
}());