Source: scene/batching.js

Object.assign(pc, function () {

    // TODO: split by new layers

    /**
     * @constructor
     * @name pc.Batch
     * @classdesc Holds information about batched mesh instances. Created in {@link pc.BatchManager#create}.
     * @param {Array} meshInstances The mesh instances to be batched.
     * @param {Boolean} dynamic Whether this batch is dynamic (supports transforming mesh instances at runtime).
     * @param {Number} batchGroupId Link this batch to a specific batch group. This is done automatically with default batches.
     * @property {Array} origMeshInstances An array of original mesh instances, from which this batch was generated.
     * @property {pc.MeshInstance} meshInstance A single combined mesh instance, the result of batching.
     * @property {pc.Model} model A handy model object
     * @property {Boolean} dynamic Whether this batch is dynamic (supports transforming mesh instances at runtime).
     * @property {Number} [batchGroupId] Link this batch to a specific batch group. This is done automatically with default batches.
     */
    var Batch = function (meshInstances, dynamic, batchGroupId) {
        this.origMeshInstances = meshInstances;
        this._aabb = new pc.BoundingBox();
        this.meshInstance = null;
        this.model = null;
        this.dynamic = dynamic;
        this.batchGroupId = batchGroupId;
        this.refCounter = 0;
    };

    /**
     * @constructor
     * @name pc.BatchGroup
     * @classdesc Holds mesh batching settings and a unique id. Created via {@link pc.BatchManager#addGroup}.
     * @param {Number} id Unique id. Can be assigned to model and element components.
     * @param {String} name The name of the group.
     * @param {Boolean} dynamic Whether objects within this batch group should support transforming at runtime.
     * @param {Number} maxAabbSize Maximum size of any dimension of a bounding box around batched objects.
     * {@link pc.BatchManager#prepare} will split objects into local groups based on this size.
     * @param {Number[]} [layers] Layer ID array. Default is [pc.LAYERID_WORLD]. The whole batch group will belong
     * to these layers. Layers of source models will be ignored.
     * @property {Boolean} dynamic Whether objects within this batch group should support transforming at runtime.
     * @property {Number} maxAabbSize Maximum size of any dimension of a bounding box around batched objects.
     * {@link pc.BatchManager#prepare} will split objects into local groups based on this size.
     * @property {Number} id Unique id. Can be assigned to model and element components.
     * @property {String} name Name of the group.
     * @property {Number[]} [layers] Layer ID array. Default is [pc.LAYERID_WORLD]. The whole batch group will belong
     * to these layers. Layers of source models will be ignored.
     */
    var BatchGroup = function (id, name, dynamic, maxAabbSize, layers) {
        this.dynamic = dynamic;
        this.maxAabbSize = maxAabbSize;
        this.id = id;
        this.name = name;
        this.layers = layers === undefined ? [pc.LAYERID_WORLD] : layers;
        this._ui = false;
        this._sprite = false;
        this._obj = {
            model: [],
            element: [],
            sprite: []
        };
    };

    BatchGroup.MODEL = 'model';
    BatchGroup.ELEMENT = 'element';
    BatchGroup.SPRITE = 'sprite';

    // Modified SkinInstance for batching
    // Doesn't contain bind matrices, simplier
    var SkinBatchInstance = function (device, nodes, rootNode) {
        this.device = device;
        this.rootNode = rootNode;
        this._dirty = true;

        // Unique per clone
        this.bones = nodes;

        var numBones = nodes.length;

        if (device.supportsBoneTextures) {
            var size;
            if (numBones > 256)
                size = 64;
            else if (numBones > 64)
                size = 32;
            else if (numBones > 16)
                size = 16;
            else
                size = 8;

            this.boneTexture = new pc.Texture(device, {
                width: size,
                height: size,
                format: pc.PIXELFORMAT_RGBA32F,
                mipmaps: false,
                minFilter: pc.FILTER_NEAREST,
                magFilter: pc.FILTER_NEAREST
            });
            this.boneTexture.name = 'batching';
            this.matrixPalette = this.boneTexture.lock();
        } else {
            this.matrixPalette = new Float32Array(numBones * 16);
        }
    };

    Object.assign(SkinBatchInstance.prototype, {
        updateMatrices: function (rootNode) {
        },

        updateMatrixPalette: function () {
            var pe;
            var mp = this.matrixPalette;
            var base;

            for (var i = this.bones.length - 1; i >= 0; i--) {
                pe = this.bones[i].getWorldTransform().data;

                // Copy the matrix into the palette, ready to be sent to the vertex shader
                base = i * 16;
                mp[base] = pe[0];
                mp[base + 1] = pe[1];
                mp[base + 2] = pe[2];
                mp[base + 3] = pe[3];
                mp[base + 4] = pe[4];
                mp[base + 5] = pe[5];
                mp[base + 6] = pe[6];
                mp[base + 7] = pe[7];
                mp[base + 8] = pe[8];
                mp[base + 9] = pe[9];
                mp[base + 10] = pe[10];
                mp[base + 11] = pe[11];
                mp[base + 12] = pe[12];
                mp[base + 13] = pe[13];
                mp[base + 14] = pe[14];
                mp[base + 15] = pe[15];
            }

            if (this.device.supportsBoneTextures) {
                this.boneTexture.lock();
                this.boneTexture.unlock();
            }
        }
    });

    /**
     * @constructor
     * @name pc.BatchManager
     * @classdesc Glues many mesh instances into a single one for better performance.
     * @param {pc.GraphicsDevice} device The graphics device used by the batch manager.
     * @param {pc.Entity} root The entity under which batched models are added.
     * @param {pc.Scene} scene The scene that the batch manager affects.
     */
    var BatchManager = function (device, root, scene) {
        this.device = device;
        this.rootNode = root;
        this.scene = scene;
        this._init = false;

        this._batchGroups = {};
        this._batchGroupCounter = 0;
        this._batchList = [];
        this._dirtyGroups = [];

        // #ifdef PROFILER
        this._stats = {
            createTime: 0,
            updateLastFrameTime: 0
        };
        // #endif
    };

    // TODO: rename destroy() to something else and rename this to destroy
    BatchManager.prototype.destroyManager = function () {
        this.device = null;
        this.rootNode = null;
        this.scene = null;
        this._batchGroups = {};
        this._batchList = [];
        this._dirtyGroups = [];
    };

    /**
     * @function
     * @name pc.BatchManager#addGroup
     * @description Adds new global batch group.
     * @param {String} name Custom name
     * @param {Boolean} dynamic Is this batch group dynamic? Will these objects move/rotate/scale after being batched?
     * @param {Number} maxAabbSize Maximum size of any dimension of a bounding box around batched objects.
     * {@link pc.BatchManager#prepare} will split objects into local groups based on this size.
     * @param {Number} [id] Optional custom unique id for the group (will be generated automatically otherwise).
     * @param {Number[]} [layers] Optional layer ID array. Default is [pc.LAYERID_WORLD]. The whole batch group will
     * belong to these layers. Layers of source models will be ignored.
     * @returns {pc.BatchGroup} Group object.
     */
    BatchManager.prototype.addGroup = function (name, dynamic, maxAabbSize, id, layers) {
        if (id === undefined) {
            id = this._batchGroupCounter;
            this._batchGroupCounter++;
        }

        if (this._batchGroups[id]) {
            // #ifdef DEBUG
            console.error("batch group with id " + id + " already exists");
            // #endif
            return;
        }

        var group;
        this._batchGroups[id] = group = new pc.BatchGroup(id, name, dynamic, maxAabbSize, layers);

        return group;
    };

    /**
     * @function
     * @name pc.BatchManager#removeGroup
     * @description Remove global batch group by id.
     * Note, this traverses the entire scene graph and clears the batch group id from all components
     * @param {String} id Group id
     */
    BatchManager.prototype.removeGroup = function (id) {
        if (!this._batchGroups[id]) {
            // #ifdef DEBUG
            console.error("batch group with id " + id + " doesn't exist");
            // #endif
            return;
        }

        // delete batches with matching id
        var newBatchList = [];
        for (var i = 0; i < this._batchList.length; i++) {
            if (this._batchList[i].batchGroupId !== id) {
                newBatchList.push(this._batchList[i]);
                continue;
            }
            this.destroy(this._batchList[i]);
        }
        this._batchList = newBatchList;
        this._removeModelsFromBatchGroup(this.rootNode, id);

        delete this._batchGroups[id];
    };

    /**
     * @private
     * @function
     * @name pc.BatchManager.markGroupDirty
     * @description Mark a specific batch group as dirty. Dirty groups are re-batched before the next frame is rendered.
     * Note, re-batching a group is a potentially expensive operation
     * @param  {Number} id Batch Group ID to mark as dirty
     */
    BatchManager.prototype.markGroupDirty = function (id) {
        if (this._dirtyGroups.indexOf(id) < 0) {
            this._dirtyGroups.push(id);
        }
    };

    /**
     * @function
     * @name pc.BatchManager#getGroupByName
     * @description Retrieves a {@link pc.BatchGroup} object with a corresponding name, if it exists, or null otherwise.
     * @param {String} name Name
     * @returns {pc.BatchGroup} Group object.
     */
    BatchManager.prototype.getGroupByName = function (name) {
        var groups = this._batchGroups;
        for (var group in groups) {
            if (!groups.hasOwnProperty(group)) continue;
            if (groups[group].name === name) {
                return groups[group];
            }
        }
        return null;
    };

    /**
     * @private
     * @function
     * @name  pc.BatchManager#getBatches
     * @description  Return a list of all {@link pc.Batch} objects that belong to the Batch Group supplied
     * @param  {Number} batchGroupId The id of the batch group
     * @returns {pc.Batch[]} A list of batches that are used to render the batch group
     */
    BatchManager.prototype.getBatches = function (batchGroupId) {
        var results = [];
        var len = this._batchList.length;
        for (var i = 0; i < len; i++) {
            var batch = this._batchList[i];
            if (batch.batchGroupId === batchGroupId) {
                results.push(batch);
            }
        }

        return results;
    };

    // traverse full hierarchy and clear the batch group id from all model, element and sprite components
    BatchManager.prototype._removeModelsFromBatchGroup = function (node, id) {
        if (!node.enabled) return;

        if (node.model && node.model.batchGroupId === id) {
            node.model.batchGroupId = -1;
        }
        if (node.element && node.element.batchGroupId === id) {
            node.element.batchGroupId = -1;
        }
        if (node.sprite && node.sprite.batchGroupId === id) {
            node.sprite.batchGroupId = -1;
        }

        for (var i = 0; i < node._children.length; i++) {
            this._removeModelsFromBatchGroup(node._children[i], id);
        }
    };

    BatchManager.prototype.insert = function (type, groupId, node) {
        var group = this._batchGroups[groupId];
        if (group) {
            if (group._obj[type].indexOf(node) < 0) {
                group._obj[type].push(node);
                this.markGroupDirty(groupId);
            }
        } else {
            // #ifdef DEBUG
            console.warn('Invalid batch ' + groupId + ' insertion');
            // #endif
        }
    };

    BatchManager.prototype.remove = function (type, groupId, node) {
        var group = this._batchGroups[groupId];
        if (group) {
            var idx = group._obj[type].indexOf(node);
            if (idx >= 0) {
                group._obj[type].splice(idx, 1);
                this.markGroupDirty(groupId);
            }
        } else {
            // #ifdef DEBUG
            console.warn('Invalid batch ' + groupId + ' insertion');
            // #endif
        }
    };

    BatchManager.prototype._extractModel = function (node, arr, group, groupMeshInstances) {
        if (!node.model || !node.model.model) return arr;

        var i;
        if (node.model.isStatic) {
            // static mesh instances can be in both drawCall array with _staticSource linking to original
            // and in the original array as well, if no triangle splitting was done
            var drawCalls = this.scene.drawCalls;
            var nodeMeshInstances = node.model.meshInstances;
            for (i = 0; i < drawCalls.length; i++) {
                if (!drawCalls[i]._staticSource) continue;
                if (nodeMeshInstances.indexOf(drawCalls[i]._staticSource) < 0) continue;
                arr.push(drawCalls[i]);
            }
            for (i = 0; i < nodeMeshInstances.length; i++) {
                if (drawCalls.indexOf(nodeMeshInstances[i]) >= 0) {
                    arr.push(nodeMeshInstances[i]);
                }
            }
        } else {
            arr = groupMeshInstances[node.model.batchGroupId] = arr.concat(node.model.meshInstances);
        }

        node.model.removeModelFromLayers(node.model.model);

        // #ifdef DEBUG
        node.model._batchGroup = group;
        // #endif
        return arr;
    };

    BatchManager.prototype._extractElement = function (node, arr, group) {
        if (!node.element) return;
        var valid = false;
        if (node.element._text && node.element._text._model.meshInstances.length > 0) {
            arr.push(node.element._text._model.meshInstances[0]);
            node.element.removeModelFromLayers(node.element._text._model);

            valid = true;
        } else if (node.element._image) {
            arr.push(node.element._image._renderable.meshInstance);
            node.element.removeModelFromLayers(node.element._image._renderable.model);

            if (node.element._image._renderable.unmaskMeshInstance) {
                arr.push(node.element._image._renderable.unmaskMeshInstance);
                if (!node.element._image._renderable.unmaskMeshInstance.stencilFront ||
                    !node.element._image._renderable.unmaskMeshInstance.stencilBack) {
                    node.element._dirtifyMask();
                    node.element._onPrerender();
                }
            }

            valid = true;
        }

        if (valid) {
            group._ui = true;
            // #ifdef DEBUG
            node.element._batchGroup = group;
            // #endif
        }
    };

    // traverse scene hierarchy down from `node` and collect all components that are marked
    // with a batch group id. Remove from layers any models that these components contains.
    // Fill the `groupMeshInstances` with all the mesh instances to be included in the batch groups,
    // indexed by batch group id.
    BatchManager.prototype._collectAndRemoveModels = function (groupMeshInstances, groupIds) {
        var node, group, arr, id;
        for (var g = 0; g < groupIds.length; g++) {
            id = groupIds[g];
            group = this._batchGroups[id];
            if (!group) continue;
            arr = groupMeshInstances[id];
            if (!arr) arr = groupMeshInstances[id] = [];

            for (var m = 0; m < group._obj.model.length; m++) {
                arr = this._extractModel(group._obj.model[m], arr, group, groupMeshInstances);
            }

            for (var e = 0; e < group._obj.element.length; e++) {
                this._extractElement(group._obj.element[e], arr, group);
            }

            for (var s = 0; s < group._obj.sprite.length; s++) {
                node = group._obj.sprite[s];
                if (node.sprite && node.sprite._meshInstance &&
                    (group.dynamic || node.sprite.sprite._renderMode === pc.SPRITE_RENDERMODE_SIMPLE)) {
                    arr.push(node.sprite._meshInstance);
                    node.sprite.removeModelFromLayers();
                    group._sprite = true;
                    node.sprite._batchGroup = group;
                }
            }
        }
    };

    /**
     * @function
     * @name pc.BatchManager#generate
     * @description Destroys all batches and creates new based on scene models. Hides original models. Called by engine automatically on app start, and if batchGroupIds on models are changed.
     * @param {Array} [groupIds] Optional array of batch group IDs to update. Otherwise all groups are updated.
     */
    BatchManager.prototype.generate = function (groupIds) {
        var i, j;
        var groupMeshInstances = {};

        if (!groupIds) {
            // Full scene
            groupIds = Object.keys(this._batchGroups);
        }

        // delete old batches with matching batchGroupId
        var newBatchList = [];
        for (i = 0; i < this._batchList.length; i++) {
            if (groupIds.indexOf(this._batchList[i].batchGroupId) < 0) {
                newBatchList.push(this._batchList[i]);
                continue;
            }
            this.destroy(this._batchList[i]);
        }
        this._batchList = newBatchList;

        // collect
        this._collectAndRemoveModels(groupMeshInstances, groupIds);

        if (groupIds === this._dirtyGroups) {
            this._dirtyGroups.length = 0;
        } else {
            var newDirtyGroups = [];
            for (i = 0; i < this._dirtyGroups.length; i++) {
                if (groupIds.indexOf(this._dirtyGroups[i]) < 0) newDirtyGroups.push(this._dirtyGroups[i]);
            }
            this._dirtyGroups = newDirtyGroups;
        }

        var group, lists, groupData, batch;
        for (var groupId in groupMeshInstances) {
            if (!groupMeshInstances.hasOwnProperty(groupId)) continue;
            group = groupMeshInstances[groupId];

            groupData = this._batchGroups[groupId];
            if (!groupData) {
                // #ifdef DEBUG
                console.error("batch group " + groupId + " not found");
                // #endif
                continue;
            }

            lists = this.prepare(group, groupData.dynamic, groupData.maxAabbSize, groupData._ui || groupData._sprite);
            for (i = 0; i < lists.length; i++) {
                batch = this.create(lists[i], groupData.dynamic, parseInt(groupId, 10));
                if (!batch) continue;
                for (j = 0; j < groupData.layers.length; j++) {
                    var layer = this.scene.layers.getLayerById(groupData.layers[j]);
                    if (layer)
                        layer.addMeshInstances(batch.model.meshInstances);
                }
            }
        }
    };

    function paramsIdentical(a, b) {
        if (a && !b) return false;
        if (!a && b) return false;
        a = a.data;
        b = b.data;
        if (a === b) return true;
        if (a instanceof Float32Array && b instanceof Float32Array) {
            if (a.length !== b.length) return false;
            for (var i = 0; i < a.length; i++) {
                if (a[i] !== b[i]) return false;
            }
            return true;
        }
        return false;
    }

    function equalParamSets(params1, params2) {
        var param;
        for (param in params1) { // compare A -> B
            if (params1.hasOwnProperty(param) && !paramsIdentical(params1[param], params2[param]))
                return false;
        }
        for (param in params2) { // compare B -> A
            if (params2.hasOwnProperty(param) && !paramsIdentical(params2[param], params1[param]))
                return false;
        }
        return true;
    }

    function equalLightLists(lightList1, lightList2) {
        var k;
        for (k = 0; k < lightList1.length; k++) {
            if (lightList2.indexOf(lightList1[k]) < 0)
                return false;
        }
        for (k = 0; k < lightList2.length; k++) {
            if (lightList1.indexOf(lightList2[k]) < 0)
                return false;
        }
        return  true;
    }

    var worldMatX = new pc.Vec3();
    var worldMatY = new pc.Vec3();
    var worldMatZ = new pc.Vec3();
    function getScaleSign(mi) {
        var wt = mi.node.worldTransform;
        wt.getX(worldMatX);
        wt.getY(worldMatY);
        wt.getZ(worldMatZ);
        worldMatX.cross(worldMatX, worldMatY);
        return worldMatX.dot(worldMatZ) >= 0 ? 1 : -1;
    }

    /**
     * @function
     * @name pc.BatchManager#prepare
     * @description Takes a list of mesh instances to be batched and sorts them into lists one for each draw call.
     * The input list will be split, if:
     * <ul>
     *     <li>Mesh instances use different materials</li>
     *     <li>Mesh instances have different parameters (e.g. lightmaps or static lights)</li>
     *     <li>Mesh instances have different shader defines (shadow receiving, being aligned to screen space, etc)</li>
     *     <li>Too many vertices for a single batch (65535 is maximum)</li>
     *     <li>Too many instances for a single batch (hardware-dependent, expect 128 on low-end and 1024 on high-end)</li>
     *     <li>Bounding box of a batch is larger than maxAabbSize in any dimension</li>
     * </ul>
     * @param {Array} meshInstances Input list of mesh instances
     * @param {Boolean} dynamic Are we preparing for a dynamic batch? Instance count will matter then (otherwise not).
     * @param {Number} maxAabbSize Maximum size of any dimension of a bounding box around batched objects.
     * @param {Boolean} translucent Are we batching UI elements or sprites
     * This is useful to keep a balance between the number of draw calls and the number of drawn triangles, because smaller batches can be hidden when not visible in camera.
     * @returns {Array} An array of arrays of mesh instances, each valid to pass to {@link pc.BatchManager#create}.
     */
    BatchManager.prototype.prepare = function (meshInstances, dynamic, maxAabbSize, translucent) {
        if (meshInstances.length === 0) return [];
        if (maxAabbSize === undefined) maxAabbSize = Number.POSITIVE_INFINITY;
        var halfMaxAabbSize = maxAabbSize * 0.5;
        var maxInstanceCount = this.device.supportsBoneTextures ? 1024 : this.device.boneLimit;

        var i;
        var material, layer, vertCount, params, lightList, defs, stencil, staticLights, scaleSign, drawOrder;
        var aabb = new pc.BoundingBox();
        var testAabb = new pc.BoundingBox();
        var skipTranslucentAabb = null;

        var lists = [];
        var j = 0;
        if (translucent) {
            meshInstances.sort(function (a, b) {
                return a.drawOrder - b.drawOrder;
            });
        }
        var meshInstancesLeftA = meshInstances;
        var meshInstancesLeftB;

        var skipMesh = translucent ? function (mi) {
            if (skipTranslucentAabb) {
                skipTranslucentAabb.add(mi.aabb);
            } else {
                skipTranslucentAabb = mi.aabb.clone();
            }
            meshInstancesLeftB.push(mi);
        } : function (mi) {
            meshInstancesLeftB.push(mi);
        };

        var mi, sf;

        while (meshInstancesLeftA.length > 0) {
            lists[j] = [meshInstancesLeftA[0]];
            meshInstancesLeftB = [];
            material = meshInstancesLeftA[0].material;
            layer = meshInstancesLeftA[0].layer;
            defs = meshInstancesLeftA[0]._shaderDefs;
            params = meshInstancesLeftA[0].parameters;
            stencil = meshInstancesLeftA[0].stencilFront;
            lightList = meshInstancesLeftA[0]._staticLightList;
            vertCount = meshInstancesLeftA[0].mesh.vertexBuffer.getNumVertices();
            drawOrder = meshInstancesLeftA[0].drawOrder;
            aabb.copy(meshInstancesLeftA[0].aabb);
            scaleSign = getScaleSign(meshInstancesLeftA[0]);
            skipTranslucentAabb = null;

            for (i = 1; i < meshInstancesLeftA.length; i++) {
                mi = meshInstancesLeftA[i];

                // Split by instance number
                if (dynamic && lists[j].length >= maxInstanceCount) {
                    meshInstancesLeftB = meshInstancesLeftB.concat(meshInstancesLeftA.slice(i));
                    break;
                }

                // Split by material, layer (legacy), shader defines, static source, vert count, overlaping UI
                if ((material !== mi.material) ||
                    (layer !== mi.layer) ||
                    (defs !== mi._shaderDefs) ||
                    (vertCount + mi.mesh.vertexBuffer.getNumVertices() > 0xFFFF)) {
                    skipMesh(mi);
                    continue;
                }
                // Split by AABB
                testAabb.copy(aabb);
                testAabb.add(mi.aabb);
                if (testAabb.halfExtents.x > halfMaxAabbSize ||
                    testAabb.halfExtents.y > halfMaxAabbSize ||
                    testAabb.halfExtents.z > halfMaxAabbSize) {
                    skipMesh(mi);
                    continue;
                }
                // Split stencil mask (UI elements), both front and back expected to be the same
                if (stencil) {
                    if (!(sf = mi.stencilFront) || stencil.func != sf.func || stencil.zpass != sf.zpass) {
                        skipMesh(mi);
                        continue;
                    }
                }
                // Split by negavive scale
                if (scaleSign != getScaleSign(mi)) {
                    skipMesh(mi);
                    continue;
                }
                // Split by parameters
                if (!equalParamSets(params, mi.parameters)) {
                    skipMesh(mi);
                    continue;
                }
                // Split by static light list
                staticLights = mi._staticLightList;
                if (lightList && staticLights) {
                    if (!equalLightLists(lightList, staticLights)) {
                        skipMesh(mi);
                        continue;
                    }
                } else if (lightList || staticLights) { // Split by static/non static
                    skipMesh(mi);
                    continue;
                }

                if (translucent && skipTranslucentAabb && skipTranslucentAabb.intersects(mi.aabb) && mi.drawOrder !== drawOrder) {
                    skipMesh(mi);
                    continue;
                }

                aabb.add(mi.aabb);
                vertCount += mi.mesh.vertexBuffer.getNumVertices();
                lists[j].push(mi);
            }

            j++;
            meshInstancesLeftA = meshInstancesLeftB;
        }

        return lists;
    };

    /**
     * @function
     * @name pc.BatchManager#create
     * @description Takes a mesh instance list that has been prepared by {@link pc.BatchManager#prepare}, and returns a {@link pc.Batch} object. This method assumes that all mesh instances provided can be rendered in a single draw call.
     * @param {Array} meshInstances Input list of mesh instances
     * @param {Boolean} dynamic Is it a static or dynamic batch? Will objects be transformed after batching?
     * @param {Number} [batchGroupId] Link this batch to a specific batch group. This is done automatically with default batches.
     * @returns {pc.Batch} The resulting batch object.
     */
    BatchManager.prototype.create = function (meshInstances, dynamic, batchGroupId) {

        // #ifdef PROFILER
        var time = pc.now();
        // #endif

        if (!this._init) {
            var boneLimit = "#define BONE_LIMIT " + this.device.getBoneLimit() + "\n";
            this.transformVS = boneLimit + "#define DYNAMICBATCH\n" + pc.shaderChunks.transformVS;
            this.skinTexVS = pc.shaderChunks.skinBatchTexVS;
            this.skinConstVS = pc.shaderChunks.skinBatchConstVS;
            this.vertexFormats = {};
            this._init = true;
        }

        var i, j;

        // Check which vertex format and buffer size are needed, find out material
        var material = null;
        var mesh, elems, numVerts, vertSize;
        var hasPos, hasNormal, hasUv, hasUv2, hasTangent, hasColor;
        var batchNumVerts = 0;
        var batchNumIndices = 0;
        var visibleMeshInstanceCount = 0;
        for (i = 0; i < meshInstances.length; i++) {
            if (!meshInstances[i].visible)
                continue;

            visibleMeshInstanceCount++;

            if (!material) {
                material = meshInstances[i].material;
            } else {
                if (material !== meshInstances[i].material) {
                    // #ifdef DEBUG
                    console.error("BatchManager.create: multiple materials");
                    // #endif
                    return;
                }
            }
            mesh = meshInstances[i].mesh;
            elems = mesh.vertexBuffer.format.elements;
            numVerts = mesh.vertexBuffer.numVertices;
            batchNumVerts += numVerts;
            for (j = 0; j < elems.length; j++) {
                if (elems[j].name === pc.SEMANTIC_POSITION) {
                    hasPos = true;
                } else if (elems[j].name === pc.SEMANTIC_NORMAL) {
                    hasNormal = true;
                } else if (elems[j].name === pc.SEMANTIC_TEXCOORD0) {
                    hasUv = true;
                } else if (elems[j].name === pc.SEMANTIC_TEXCOORD1) {
                    hasUv2 = true;
                } else if (elems[j].name === pc.SEMANTIC_TANGENT) {
                    hasTangent = true;
                } else if (elems[j].name === pc.SEMANTIC_COLOR) {
                    hasColor = true;
                }
            }
            batchNumIndices += mesh.primitive[0].indexed ? mesh.primitive[0].count :
                (mesh.primitive[0].count === 4 ? 6 : 0);
        }

        if (!visibleMeshInstanceCount) {
            return;
        }

        if (!hasPos) {
            // #ifdef DEBUG
            console.error("BatchManager.create: no position");
            // #endif
            return;
        }

        var batch = new pc.Batch(meshInstances, dynamic, batchGroupId);
        this._batchList.push(batch);

        // Create buffers
        var entityIndexSizeF = dynamic ? 1 : 0;
        var batchVertSizeF = 3 + (hasNormal ? 3 : 0) + (hasUv ? 2 : 0) +  (hasUv2 ? 2 : 0) + (hasTangent ? 4 : 0) + (hasColor ? 1 : 0) + entityIndexSizeF;
        var batchOffsetNF = 3;
        var batchOffsetUF = hasNormal ? 3 * 2 : 3;
        var batchOffsetU2F = (hasNormal ? 3 * 2 : 3) + (hasUv ? 2 : 0);
        var batchOffsetTF = (hasNormal ? 3 * 2 : 3) + (hasUv ? 2 : 0) + (hasUv2 ? 2 : 0);
        var batchOffsetCF = (hasNormal ? 3 * 2 : 3) + (hasUv ? 2 : 0) + (hasUv2 ? 2 : 0) + (hasTangent ? 4 : 0);
        var batchOffsetEF = (hasNormal ? 3 * 2 : 3) + (hasUv ? 2 : 0) + (hasUv2 ? 2 : 0) + (hasTangent ? 4 : 0) + (hasColor ? 1 : 0);

        var arrayBuffer = new ArrayBuffer(batchNumVerts * batchVertSizeF * 4);
        var batchData = new Float32Array(arrayBuffer);
        var batchData8 = new Uint8Array(arrayBuffer);

        var indexBuffer = new pc.IndexBuffer(this.device, pc.INDEXFORMAT_UINT16, batchNumIndices, pc.BUFFER_STATIC);
        var batchIndexData = new Uint16Array(indexBuffer.lock());
        var vertSizeF;

        // Fill vertex/index/matrix buffers
        var data, data8, indexBase, numIndices, indexData;
        var verticesOffset = 0;
        var indexOffset = 0;
        var vbOffset = 0;
        var offsetPF, offsetNF, offsetUF, offsetU2F, offsetTF, offsetCF;
        var transform, vec =  new pc.Vec3();

        for (i = 0; i < meshInstances.length; i++) {
            if (!meshInstances[i].visible)
                continue;

            mesh = meshInstances[i].mesh;

            elems = mesh.vertexBuffer.format.elements;
            numVerts = mesh.vertexBuffer.numVertices;
            vertSize = mesh.vertexBuffer.format.size;
            vertSizeF = vertSize / 4;
            for (j = 0; j < elems.length; j++) {
                if (elems[j].name === pc.SEMANTIC_POSITION) {
                    offsetPF = elems[j].offset / 4;
                } else if (elems[j].name === pc.SEMANTIC_NORMAL) {
                    offsetNF = elems[j].offset / 4;
                } else if (elems[j].name === pc.SEMANTIC_TEXCOORD0) {
                    offsetUF = elems[j].offset / 4;
                } else if (elems[j].name === pc.SEMANTIC_TEXCOORD1) {
                    offsetU2F = elems[j].offset / 4;
                } else if (elems[j].name === pc.SEMANTIC_TANGENT) {
                    offsetTF = elems[j].offset / 4;
                } else if (elems[j].name === pc.SEMANTIC_COLOR) {
                    offsetCF = elems[j].offset / 4;
                }
            }
            data = new Float32Array(mesh.vertexBuffer.storage);
            data8 = new Uint8Array(mesh.vertexBuffer.storage);

            // Static: pre-transform vertices
            transform = meshInstances[i].node.getWorldTransform();

            for (j = 0; j < numVerts; j++) {
                vec.set(data[j * vertSizeF + offsetPF],
                        data[j * vertSizeF + offsetPF + 1],
                        data[j * vertSizeF + offsetPF + 2]);
                if (!dynamic)
                    transform.transformPoint(vec, vec);
                batchData[j * batchVertSizeF + vbOffset] =     vec.x;
                batchData[j * batchVertSizeF + vbOffset + 1] = vec.y;
                batchData[j * batchVertSizeF + vbOffset + 2] = vec.z;
                if (hasNormal) {
                    vec.set(data[j * vertSizeF + offsetNF],
                            data[j * vertSizeF + offsetNF + 1],
                            data[j * vertSizeF + offsetNF + 2]);
                    if (!dynamic)
                        transform.transformVector(vec, vec);
                    batchData[j * batchVertSizeF + vbOffset + batchOffsetNF] =    vec.x;
                    batchData[j * batchVertSizeF + vbOffset + batchOffsetNF + 1] = vec.y;
                    batchData[j * batchVertSizeF + vbOffset + batchOffsetNF + 2] = vec.z;
                }
                if (hasUv) {
                    batchData[j * batchVertSizeF + vbOffset + batchOffsetUF] =     data[j * vertSizeF + offsetUF];
                    batchData[j * batchVertSizeF + vbOffset + batchOffsetUF + 1] = data[j * vertSizeF + offsetUF + 1];
                }
                if (hasUv2) {
                    batchData[j * batchVertSizeF + vbOffset + batchOffsetU2F] =     data[j * vertSizeF + offsetU2F];
                    batchData[j * batchVertSizeF + vbOffset + batchOffsetU2F + 1] = data[j * vertSizeF + offsetU2F + 1];
                }
                if (hasTangent) {
                    vec.set(data[j * vertSizeF + offsetTF],
                            data[j * vertSizeF + offsetTF + 1],
                            data[j * vertSizeF + offsetTF + 2]);
                    if (!dynamic)
                        transform.transformVector(vec, vec);
                    batchData[j * batchVertSizeF + vbOffset + batchOffsetTF] =    vec.x;
                    batchData[j * batchVertSizeF + vbOffset + batchOffsetTF + 1] = vec.y;
                    batchData[j * batchVertSizeF + vbOffset + batchOffsetTF + 2] = vec.z;
                    batchData[j * batchVertSizeF + vbOffset + batchOffsetTF + 3] = data[j * vertSizeF + offsetTF + 3];
                }
                if (hasColor) {
                    batchData8[j * batchVertSizeF * 4 + vbOffset * 4 + batchOffsetCF * 4] =     data8[j * vertSizeF * 4 + offsetCF * 4];
                    batchData8[j * batchVertSizeF * 4 + vbOffset * 4 + batchOffsetCF * 4 + 1] = data8[j * vertSizeF * 4 + offsetCF * 4 + 1];
                    batchData8[j * batchVertSizeF * 4 + vbOffset * 4 + batchOffsetCF * 4 + 2] = data8[j * vertSizeF * 4 + offsetCF * 4 + 2];
                    batchData8[j * batchVertSizeF * 4 + vbOffset * 4 + batchOffsetCF * 4 + 3] = data8[j * vertSizeF * 4 + offsetCF * 4 + 3];
                }
                if (dynamic)
                    batchData[j * batchVertSizeF + batchOffsetEF + vbOffset] = i;
            }

            indexBase = mesh.primitive[0].base;
            numIndices = mesh.primitive[0].count;
            if (mesh.primitive[0].indexed) {
                indexData = new Uint16Array(mesh.indexBuffer[0].storage);
            } else if (numIndices === 4) {
                // Special case for UI image elements (pc.PRIMITIVE_TRIFAN)
                indexBase = 0;
                numIndices = 6;
                indexData = [0, 1, 3, 2, 3, 1];
            } else {
                numIndices = 0;
                continue;
            }
            for (j = 0; j < numIndices; j++) {
                batchIndexData[j + indexOffset] = indexData[indexBase + j] + verticesOffset;
            }
            indexOffset += numIndices;
            verticesOffset += numVerts;
            vbOffset = verticesOffset * batchVertSizeF;
        }

        // Create the vertex format
        var vertexFormatId = 0;
        if (hasNormal)  vertexFormatId |= 1 << 1;
        if (hasUv)      vertexFormatId |= 1 << 2;
        if (hasUv2)     vertexFormatId |= 1 << 3;
        if (hasTangent) vertexFormatId |= 1 << 4;
        if (hasColor)   vertexFormatId |= 1 << 5;
        if (dynamic)    vertexFormatId |= 1 << 6;
        var vertexFormat = this.vertexFormats[vertexFormatId];
        if (!vertexFormat) {
            var formatDesc = [];
            formatDesc.push({
                semantic: pc.SEMANTIC_POSITION,
                components: 3,
                type: pc.ELEMENTTYPE_FLOAT32,
                normalize: false
            });
            if (hasNormal) {
                formatDesc.push({
                    semantic: pc.SEMANTIC_NORMAL,
                    components: 3,
                    type: pc.ELEMENTTYPE_FLOAT32,
                    normalize: false
                });
            }
            if (hasUv) {
                formatDesc.push({
                    semantic: pc.SEMANTIC_TEXCOORD0,
                    components: 2,
                    type: pc.ELEMENTTYPE_FLOAT32,
                    normalize: false
                });
            }
            if (hasUv2) {
                formatDesc.push({
                    semantic: pc.SEMANTIC_TEXCOORD1,
                    components: 2,
                    type: pc.ELEMENTTYPE_FLOAT32,
                    normalize: false
                });
            }
            if (hasTangent) {
                formatDesc.push({
                    semantic: pc.SEMANTIC_TANGENT,
                    components: 4,
                    type: pc.ELEMENTTYPE_FLOAT32,
                    normalize: false
                });
            }
            if (hasColor) {
                formatDesc.push({
                    semantic: pc.SEMANTIC_COLOR,
                    components: 4,
                    type: pc.ELEMENTTYPE_UINT8,
                    normalize: true
                });
            }
            if (dynamic) {
                formatDesc.push({
                    semantic: pc.SEMANTIC_BLENDINDICES,
                    components: 1,
                    type: pc.ELEMENTTYPE_FLOAT32,
                    normalize: false
                });
            }
            vertexFormat = this.vertexFormats[vertexFormatId] = new pc.VertexFormat(this.device, formatDesc);
        }

        // Upload data to GPU
        var vertexBuffer = new pc.VertexBuffer(this.device, vertexFormat, batchNumVerts, pc.BUFFER_STATIC, batchData.buffer);
        indexBuffer.unlock();

        // Create mesh
        mesh = new pc.Mesh();
        mesh.vertexBuffer = vertexBuffer;
        mesh.indexBuffer[0] = indexBuffer;
        mesh.primitive[0].type = pc.PRIMITIVE_TRIANGLES; // Doesn't support any other primitive types batch.origMeshInstances[0].mesh.primitive[0].type;
        mesh.primitive[0].base = 0;
        mesh.primitive[0].count = batchNumIndices;
        mesh.primitive[0].indexed = true;
        mesh.cull = false;

        if (dynamic) {
            // Patch the material
            material = material.clone();
            material.chunks.transformVS = this.transformVS;
            material.chunks.skinTexVS = this.skinTexVS;
            material.chunks.skinConstVS = this.skinConstVS;
            material.update();
        }

        // Create meshInstance
        var meshInstance = new pc.MeshInstance(this.rootNode, mesh, material);
        meshInstance.castShadow = batch.origMeshInstances[0].castShadow;
        meshInstance.parameters = batch.origMeshInstances[0].parameters;
        meshInstance.isStatic = batch.origMeshInstances[0].isStatic;
        meshInstance.cull = batch.origMeshInstances[0].cull;
        meshInstance.layer = batch.origMeshInstances[0].layer;
        meshInstance._staticLightList = batch.origMeshInstances[0]._staticLightList;
        meshInstance._shaderDefs = batch.origMeshInstances[0]._shaderDefs;

        if (dynamic) {
            // Create skinInstance
            var nodes = [];
            for (i = 0; i < batch.origMeshInstances.length; i++) {
                nodes.push(batch.origMeshInstances[i].node);
            }
            meshInstance.skinInstance = new SkinBatchInstance(this.device, nodes, this.rootNode);
        }

        meshInstance._updateAabb = false;
        meshInstance.drawOrder = batch.origMeshInstances[0].drawOrder;
        meshInstance.stencilFront = batch.origMeshInstances[0].stencilFront;
        meshInstance.stencilBack = batch.origMeshInstances[0].stencilBack;
        meshInstance.flipFaces = getScaleSign(batch.origMeshInstances[0]) < 0;
        batch.meshInstance = meshInstance;
        this.update(batch);

        var newModel = new pc.Model();

        newModel.meshInstances = [batch.meshInstance];
        newModel.castShadows = batch.origMeshInstances[0].castShadows;
        batch.model = newModel;

        // #ifdef PROFILER
        this._stats.createTime += pc.now() - time;
        // #endif

        return batch;
    };

    /**
     * @private
     * @function
     * @name pc.BatchManager#update
     * @description Updates bounding box for a batch. Called automatically.
     * @param {pc.Batch} batch A batch object
     */
    BatchManager.prototype.update = function (batch) {
        batch._aabb.copy(batch.origMeshInstances[0].aabb);
        for (var i = 1; i < batch.origMeshInstances.length; i++) {
            batch._aabb.add(batch.origMeshInstances[i].aabb); // this is the slowest part
        }
        batch.meshInstance.aabb = batch._aabb;
        batch._aabb._radiusVer = -1;
        batch.meshInstance._aabbVer = 0;
    };

    /**
     * @private
     * @function
     * @name pc.BatchManager#updateAll
     * @description Updates bounding boxes for all dynamic batches. Called automatically.
     */
    BatchManager.prototype.updateAll = function () {
        // TODO: only call when needed. Applies to skinning matrices as well

        if (this._dirtyGroups.length > 0) {
            this.generate(this._dirtyGroups);
        }

        // #ifdef PROFILER
        var time = pc.now();
        // #endif

        for (var i = 0; i < this._batchList.length; i++) {
            if (!this._batchList[i].dynamic) continue;
            this.update(this._batchList[i]);
        }

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

    /**
     * @function
     * @name pc.BatchManager#clone
     * @description Clones a batch. This method doesn't rebuild batch geometry, but only creates a new model and batch objects, linked to different source mesh instances.
     * @param {pc.Batch} batch A batch object
     * @param {Array} clonedMeshInstances New mesh instances
     * @returns {pc.Batch} New batch object
     */
    BatchManager.prototype.clone = function (batch, clonedMeshInstances) {
        var batch2 = new pc.Batch(clonedMeshInstances, batch.dynamic, batch.batchGroupId);
        this._batchList.push(batch2);

        var nodes = [];
        for (var i = 0; i < clonedMeshInstances.length; i++) {
            nodes.push(clonedMeshInstances[i].node);
        }

        batch2.meshInstance = new pc.MeshInstance(batch.meshInstance.node, batch.meshInstance.mesh, batch.meshInstance.material);
        batch2.meshInstance._updateAabb = false;
        batch2.meshInstance.parameters = clonedMeshInstances[0].parameters;
        batch2.meshInstance.isStatic = clonedMeshInstances[0].isStatic;
        batch2.meshInstance.cull = clonedMeshInstances[0].cull;
        batch2.meshInstance.layer = clonedMeshInstances[0].layer;
        batch2.meshInstance._staticLightList = clonedMeshInstances[0]._staticLightList;

        if (batch.dynamic) {
            batch2.meshInstance.skinInstance = new SkinBatchInstance(this.device, nodes, this.rootNode);
        }

        batch2.meshInstance.castShadow = batch.meshInstance.castShadow;
        batch2.meshInstance._shader = batch.meshInstance._shader;

        var newModel = new pc.Model();

        newModel.meshInstances = [batch2.meshInstance];
        newModel.castShadows = batch.origMeshInstances[0].castShadows;
        batch2.model = newModel;

        return batch2;
    };

    /**
     * @private
     * @function
     * @name pc.BatchManager#destroy
     * @description Mark the batches ref counter to 0, remove the batch model out of all layers and destroy it
     * @param {pc.Batch} batch A batch object
     */
    BatchManager.prototype.destroy = function (batch) {
        batch.refCounter = 0;
        if (!batch.model)
            return;
        var layers = this._batchGroups[batch.batchGroupId].layers;
        for (var i = 0; i < layers.length; i++) {
            var layer = this.scene.layers.getLayerById(layers[i]);
            if (layer)
                layer.removeMeshInstances(batch.model.meshInstances);
        }
        batch.model.destroy();
    };

    /**
     * @private
     * @function
     * @name pc.BatchManager#decrement
     * @description Decrements reference counter on a batch. If it's zero, the batch is removed from scene, and its geometry is deleted from memory.
     * @param {pc.Batch} batch A batch object
     */
    BatchManager.prototype.decrement = function (batch) {
        batch.refCounter--;
        if (batch.refCounter === 0) {
            this.destroy(batch);
        }
    };

    return {
        Batch: Batch,
        BatchGroup: BatchGroup,
        BatchManager: BatchManager
    };
}());