Source: Scene/BatchTable.js

/*global define*/
define([
        '../Core/Cartesian2',
        '../Core/Cartesian3',
        '../Core/Cartesian4',
        '../Core/combine',
        '../Core/ComponentDatatype',
        '../Core/defined',
        '../Core/defineProperties',
        '../Core/destroyObject',
        '../Core/DeveloperError',
        '../Core/PixelFormat',
        '../Core/RuntimeError',
        '../Renderer/ContextLimits',
        '../Renderer/PixelDatatype',
        '../Renderer/Sampler',
        '../Renderer/Texture',
        '../Renderer/TextureMagnificationFilter',
        '../Renderer/TextureMinificationFilter'
    ], function(
        Cartesian2,
        Cartesian3,
        Cartesian4,
        combine,
        ComponentDatatype,
        defined,
        defineProperties,
        destroyObject,
        DeveloperError,
        PixelFormat,
        RuntimeError,
        ContextLimits,
        PixelDatatype,
        Sampler,
        Texture,
        TextureMagnificationFilter,
        TextureMinificationFilter) {
    'use strict';

    /**
     * Creates a texture to look up per instance attributes for batched primitives. For example, store each primitive's pick color in the texture.
     *
     * @alias BatchTable
     * @constructor
     * @private
     *
     * @param {Object[]} attributes An array of objects describing a per instance attribute. Each object contains a datatype, components per attributes, whether it is normalized and a function name
     *     to retrieve the value in the vertex shader.
     * @param {Number} numberOfInstances The number of instances in a batch table.
     *
     * @example
     * // create the batch table
     * var attributes = [{
     *     functionName : 'getShow',
     *     componentDatatype : ComponentDatatype.UNSIGNED_BYTE,
     *     componentsPerAttribute : 1
     * }, {
     *     functionName : 'getPickColor',
     *     componentDatatype : ComponentDatatype.UNSIGNED_BYTE,
     *     componentsPerAttribute : 4,
     *     normalize : true
     * }];
     * var batchTable = new BatchTable(attributes, 5);
     *
     * // when creating the draw commands, update the uniform map and the vertex shader
     * vertexShaderSource = batchTable.getVertexShaderCallback()(vertexShaderSource);
     * var shaderProgram = ShaderProgram.fromCache({
     *    // ...
     *    vertexShaderSource : vertexShaderSource,
     * });
     *
     * drawCommand.shaderProgram = shaderProgram;
     * drawCommand.uniformMap = batchTable.getUniformMapCallback()(uniformMap);
     *
     * // use the attribute function names in the shader to retrieve the instance values
     * // ...
     * attribute float batchId;
     *
     * void main() {
     *     // ...
     *     float show = getShow(batchId);
     *     vec3 pickColor = getPickColor(batchId);
     *     // ...
     * }
     */
    function BatchTable(attributes, numberOfInstances) {
        //>>includeStart('debug', pragmas.debug);
        if (!defined(attributes)) {
            throw new DeveloperError('attributes is required');
        }
        if (!defined(numberOfInstances)) {
            throw new DeveloperError('numberOfInstances is required');
        }
        //>>includeEnd('debug');

        this._attributes = attributes;
        this._numberOfInstances = numberOfInstances;

        var pixelDatatype = getDatatype(attributes);

        var numberOfAttributes = attributes.length;
        var maxNumberOfInstancesPerRow = Math.floor(ContextLimits.maximumTextureSize / numberOfAttributes);

        var instancesPerWidth = Math.min(numberOfInstances, maxNumberOfInstancesPerRow);
        var width = numberOfAttributes * instancesPerWidth;
        var height = Math.ceil(numberOfInstances / instancesPerWidth);

        var stepX = 1.0 / width;
        var centerX = stepX * 0.5;
        var stepY = 1.0 / height;
        var centerY = stepY * 0.5;

        this._textureDimensions = new Cartesian2(width, height);
        this._textureStep = new Cartesian4(stepX, centerX, stepY, centerY);
        this._pixelDatatype = pixelDatatype;
        this._texture = undefined;

        var batchLength = width * height * 4;
        this._batchValues = pixelDatatype === PixelDatatype.FLOAT ? new Float32Array(batchLength) : new Uint8Array(batchLength);
        this._batchValuesDirty = false;
    }

    defineProperties(BatchTable.prototype, {
        /**
         * The attribute descriptions.
         * @memberOf BatchTable.prototype
         * @type {Object[]}
         * @readonly
         */
        attributes : {
            get : function() {
                return this._attributes;
            }
        },
        /**
         * The number of instances.
         * @memberOf BatchTable.prototype
         * @type {Number}
         * @readonly
         */
        numberOfInstances : {
            get : function () {
                return this._numberOfInstances;
            }
        }
    });

    function getDatatype(attributes) {
        var foundFloatDatatype = false;
        var length = attributes.length;
        for (var i = 0; i < length; ++i) {
            if (attributes[i].componentDatatype !== ComponentDatatype.UNSIGNED_BYTE) {
                foundFloatDatatype = true;
                break;
            }
        }
        return foundFloatDatatype ? PixelDatatype.FLOAT : PixelDatatype.UNSIGNED_BYTE;
    }

    function getAttributeType(attributes, attributeIndex) {
        var componentsPerAttribute = attributes[attributeIndex].componentsPerAttribute;
        if (componentsPerAttribute === 2) {
            return Cartesian2;
        } else if (componentsPerAttribute === 3) {
            return Cartesian3;
        } else if (componentsPerAttribute === 4) {
            return Cartesian4;
        }
        return Number;
    }

    var scratchGetAttributeCartesian4 = new Cartesian4();

    /**
     * Gets the value of an attribute in the table.
     *
     * @param {Number} instanceIndex The index of the instance.
     * @param {Number} attributeIndex The index of the attribute.
     * @param {undefined|Cartesian2|Cartesian3|Cartesian4} [result] The object onto which to store the result. The type is dependent on the attribute's number of components.
     * @returns {Number|Cartesian2|Cartesian3|Cartesian4} The attribute value stored for the instance.
     *
     * @exception {DeveloperError} instanceIndex is out of range.
     * @exception {DeveloperError} attributeIndex is out of range.
     */
    BatchTable.prototype.getBatchedAttribute = function(instanceIndex, attributeIndex, result) {
        //>>includeStart('debug', pragmas.debug);
        if (instanceIndex < 0 || instanceIndex >= this._numberOfInstances) {
            throw new DeveloperError('instanceIndex is out of range.');
        }
        if (attributeIndex < 0 || attributeIndex >= this._attributes.length) {
            throw new DeveloperError('attributeIndex is out of range');
        }
        //>>includeEnd('debug');

        var attributes = this._attributes;
        var index = 4 * attributes.length * instanceIndex + 4 * attributeIndex;
        var value = Cartesian4.unpack(this._batchValues, index, scratchGetAttributeCartesian4);

        var attributeType = getAttributeType(attributes, attributeIndex);
        if (defined(attributeType.fromCartesian4)) {
            return attributeType.fromCartesian4(value, result);
        } else if (defined(attributeType.clone)) {
            return attributeType.clone(value, result);
        }

        return value.x;
    };

    var setAttributeScratchValues = [undefined, undefined, new Cartesian2(), new Cartesian3(), new Cartesian4()];
    var setAttributeScratchCartesian4 = new Cartesian4();

    /**
     * Sets the value of an attribute in the table.
     *
     * @param {Number} instanceIndex The index of the instance.
     * @param {Number} attributeIndex The index of the attribute.
     * @param {Number|Cartesian2|Cartesian3|Cartesian4} value The value to be stored in the table. The type of value will depend on the number of components of the attribute.
     *
     * @exception {DeveloperError} instanceIndex is out of range.
     * @exception {DeveloperError} attributeIndex is out of range.
     */
    BatchTable.prototype.setBatchedAttribute = function(instanceIndex, attributeIndex, value) {
        //>>includeStart('debug', pragmas.debug);
        if (instanceIndex < 0 || instanceIndex >= this._numberOfInstances) {
            throw new DeveloperError('instanceIndex is out of range.');
        }
        if (attributeIndex < 0 || attributeIndex >= this._attributes.length) {
            throw new DeveloperError('attributeIndex is out of range');
        }
        if (!defined(value)) {
            throw new DeveloperError('value is required.');
        }
        //>>includeEnd('debug');

        var attributes = this._attributes;
        var result = setAttributeScratchValues[attributes[attributeIndex].componentsPerAttribute];
        var currentAttribute = this.getBatchedAttribute(instanceIndex, attributeIndex, result);
        var attributeType = getAttributeType(this._attributes, attributeIndex);
        var entriesEqual = defined(attributeType.equals) ? attributeType.equals(currentAttribute, value) : currentAttribute === value;
        if (entriesEqual) {
            return;
        }

        var attributeValue = setAttributeScratchCartesian4;
        attributeValue.x = defined(value.x) ? value.x : value;
        attributeValue.y = defined(value.y) ? value.y : 0.0;
        attributeValue.z = defined(value.z) ? value.z : 0.0;
        attributeValue.w = defined(value.w) ? value.w : 0.0;

        var index = 4 * attributes.length * instanceIndex + 4 * attributeIndex;
        Cartesian4.pack(attributeValue, this._batchValues, index);

        this._batchValuesDirty = true;
    };

    function createTexture(batchTable, context) {
        var dimensions = batchTable._textureDimensions;
        batchTable._texture = new Texture({
            context : context,
            pixelFormat : PixelFormat.RGBA,
            pixelDatatype : batchTable._pixelDatatype,
            width : dimensions.x,
            height : dimensions.y,
            sampler : new Sampler({
                minificationFilter : TextureMinificationFilter.NEAREST,
                magnificationFilter : TextureMagnificationFilter.NEAREST
            })
        });
    }

    function updateTexture(batchTable) {
        var dimensions = batchTable._textureDimensions;
        batchTable._texture.copyFrom({
            width : dimensions.x,
            height : dimensions.y,
            arrayBufferView : batchTable._batchValues
        });
    }

    /**
     * Creates/updates the batch table texture.
     * @param {FrameState} frameState The frame state.
     *
     * @exception {RuntimeError} The floating point texture extension is required but not supported.
     */
    BatchTable.prototype.update = function(frameState) {
        var context = frameState.context;
        if (this._pixelDatatype === PixelDatatype.FLOAT && !context.floatingPointTexture) {
            // We could probably pack the floats to RGBA unsigned bytes but that would add a lot CPU and memory overhead.
            throw new RuntimeError('The floating point texture extension is required but not supported.');
        }

        if (defined(this._texture) && !this._batchValuesDirty) {
            return;
        }

        this._batchValuesDirty = false;

        if (!defined(this._texture)) {
            createTexture(this, context);
        }
        updateTexture(this);
    };

    /**
     * Gets a function that will update a uniform map to contain values for looking up values in the batch table.
     *
     * @returns {BatchTable~updateUniformMapCallback} A callback for updating uniform maps.
     */
    BatchTable.prototype.getUniformMapCallback = function() {
        var that = this;
        return function(uniformMap) {
            var batchUniformMap = {
                batchTexture : function() {
                    return that._texture;
                },
                batchTextureDimensions : function() {
                    return that._textureDimensions;
                },
                batchTextureStep : function() {
                    return that._textureStep;
                }
            };

            return combine(uniformMap, batchUniformMap);
        };
    };

    function getGlslComputeSt(batchTable) {
        var numberOfAttributes = batchTable._attributes.length;

        // GLSL batchId is zero-based: [0, numberOfInstances - 1]
        if (batchTable._textureDimensions.y === 1) {
            return 'uniform vec4 batchTextureStep; \n' +
                   'vec2 computeSt(float batchId) \n' +
                   '{ \n' +
                   '    float stepX = batchTextureStep.x; \n' +
                   '    float centerX = batchTextureStep.y; \n' +
                   '    float numberOfAttributes = float('+ numberOfAttributes + '); \n' +
                   '    return vec2(centerX + (batchId * numberOfAttributes * stepX), 0.5); \n' +
                   '} \n';
        }

        return 'uniform vec4 batchTextureStep; \n' +
               'uniform vec2 batchTextureDimensions; \n' +
               'vec2 computeSt(float batchId) \n' +
               '{ \n' +
               '    float stepX = batchTextureStep.x; \n' +
               '    float centerX = batchTextureStep.y; \n' +
               '    float stepY = batchTextureStep.z; \n' +
               '    float centerY = batchTextureStep.w; \n' +
               '    float numberOfAttributes = float('+ numberOfAttributes + '); \n' +
               '    float xId = mod(batchId * numberOfAttributes, batchTextureDimensions.x); \n' +
               '    float yId = floor(batchId * numberOfAttributes / batchTextureDimensions.x); \n' +
               '    return vec2(centerX + (xId * stepX), 1.0 - (centerY + (yId * stepY))); \n' +
               '} \n';
    }

    function getComponentType(componentsPerAttribute) {
        if (componentsPerAttribute === 1) {
            return 'float';
        }
        return 'vec' + componentsPerAttribute;
    }

    function getComponentSwizzle(componentsPerAttribute) {
        if (componentsPerAttribute === 1) {
            return '.x';
        } else if (componentsPerAttribute === 2) {
            return '.xy';
        } else if (componentsPerAttribute === 3) {
            return '.xyz';
        }
        return '';
    }

    function getGlslAttributeFunction(batchTable, attributeIndex) {
        var attributes = batchTable._attributes;
        var attribute = attributes[attributeIndex];
        var componentsPerAttribute = attribute.componentsPerAttribute;
        var functionName = attribute.functionName;
        var functionReturnType = getComponentType(componentsPerAttribute);
        var functionReturnValue = getComponentSwizzle(componentsPerAttribute);

        var glslFunction =
            functionReturnType + ' ' + functionName + '(float batchId) \n' +
            '{ \n' +
            '    vec2 st = computeSt(batchId); \n' +
            '    st.x += batchTextureStep.x * float(' + attributeIndex + '); \n' +
            '    vec4 textureValue = texture2D(batchTexture, st); \n' +
            '    ' + functionReturnType + ' value = textureValue' + functionReturnValue + '; \n';

        if (batchTable._pixelDatatype === PixelDatatype.UNSIGNED_BYTE && !attribute.normalize) {
            glslFunction += 'value *= 255.0; \n';
        } else if (batchTable._pixelDatatype === PixelDatatype.FLOAT && attribute.componentDatatype === ComponentDatatype.UNSIGNED_BYTE && attribute.normalize) {
            glslFunction += 'value /= 255.0; \n';
        }

        glslFunction +=
            '    return value; \n' +
            '} \n';
        return glslFunction;
    }

    /**
     * Gets a function that will update a vertex shader to contain functions for looking up values in the batch table.
     *
     * @returns {BatchTable~updateVertexShaderSourceCallback} A callback for updating a vertex shader source.
     */
    BatchTable.prototype.getVertexShaderCallback = function() {
        var batchTableShader = 'uniform sampler2D batchTexture; \n';
        batchTableShader += getGlslComputeSt(this) + '\n';

        var attributes = this._attributes;
        var length = attributes.length;
        for (var i = 0; i < length; ++i) {
            batchTableShader += getGlslAttributeFunction(this, i);
        }

        return function(source) {
            var mainIndex = source.indexOf('void main');
            var beforeMain = source.substring(0, mainIndex);
            var afterMain = source.substring(mainIndex);
            return beforeMain + '\n' + batchTableShader + '\n' + afterMain;
        };
    };

    /**
     * Returns true if this object was destroyed; otherwise, false.
     * <br /><br />
     * If this object was destroyed, it should not be used; calling any function other than
     * <code>isDestroyed</code> will result in a {@link DeveloperError} exception.
     *
     * @returns {Boolean} <code>true</code> if this object was destroyed; otherwise, <code>false</code>.
     *
     * @see BatchTable#destroy
     */
    BatchTable.prototype.isDestroyed = function() {
        return false;
    };

    /**
     * Destroys the WebGL resources held by this object.  Destroying an object allows for deterministic
     * release of WebGL resources, instead of relying on the garbage collector to destroy this object.
     * <br /><br />
     * Once an object is destroyed, it should not be used; calling any function other than
     * <code>isDestroyed</code> will result in a {@link DeveloperError} exception.  Therefore,
     * assign the return value (<code>undefined</code>) to the object as done in the example.
     *
     * @returns {undefined}
     *
     * @exception {DeveloperError} This object was destroyed, i.e., destroy() was called.
     *
     * @see BatchTable#isDestroyed
     */
    BatchTable.prototype.destroy = function() {
        this._texture = this._texture && this._texture.destroy();
        return destroyObject(this);
    };

    /**
     * A callback for updating uniform maps.
     * @callback BatchTable~updateUniformMapCallback
     *
     * @param {Object} uniformMap The uniform map.
     * @returns {Object} The new uniform map with properties for retrieving values from the batch table.
     */

    /**
     * A callback for updating a vertex shader source.
     * @callback BatchTable~updateVertexShaderSourceCallback
     *
     * @param {String} vertexShaderSource The vertex shader source.
     * @returns {String} The new vertex shader source with the functions for retrieving batch table values injected.
     */

    return BatchTable;
});