123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636 |
- import Cartesian2 from "../Core/Cartesian2.js";
- import Cartesian3 from "../Core/Cartesian3.js";
- import Cartesian4 from "../Core/Cartesian4.js";
- import combine from "../Core/combine.js";
- import ComponentDatatype from "../Core/ComponentDatatype.js";
- import defined from "../Core/defined.js";
- import destroyObject from "../Core/destroyObject.js";
- import DeveloperError from "../Core/DeveloperError.js";
- import PixelFormat from "../Core/PixelFormat.js";
- import ContextLimits from "../Renderer/ContextLimits.js";
- import PixelDatatype from "../Renderer/PixelDatatype.js";
- import Sampler from "../Renderer/Sampler.js";
- import Texture from "../Renderer/Texture.js";
- /**
- * 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 {Context} context The context in which the batch table is created.
- * @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
- * const attributes = [{
- * functionName : 'getShow',
- * componentDatatype : ComponentDatatype.UNSIGNED_BYTE,
- * componentsPerAttribute : 1
- * }, {
- * functionName : 'getPickColor',
- * componentDatatype : ComponentDatatype.UNSIGNED_BYTE,
- * componentsPerAttribute : 4,
- * normalize : true
- * }];
- * const batchTable = new BatchTable(context, attributes, 5);
- *
- * // when creating the draw commands, update the uniform map and the vertex shader
- * vertexShaderSource = batchTable.getVertexShaderCallback()(vertexShaderSource);
- * const 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(context, attributes, numberOfInstances) {
- //>>includeStart('debug', pragmas.debug);
- if (!defined(context)) {
- throw new DeveloperError("context is required");
- }
- 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;
- if (attributes.length === 0) {
- return;
- }
- // PERFORMANCE_IDEA: We may be able to arrange the attributes so they can be packing into fewer texels.
- // Right now, an attribute with one component uses an entire texel when 4 single component attributes can
- // be packed into a texel.
- //
- // Packing floats into unsigned byte textures makes the problem worse. A single component float attribute
- // will be packed into a single texel leaving 3 texels unused. 4 texels are reserved for each float attribute
- // regardless of how many components it has.
- const pixelDatatype = getDatatype(attributes);
- const textureFloatSupported = context.floatingPointTexture;
- const packFloats =
- pixelDatatype === PixelDatatype.FLOAT && !textureFloatSupported;
- const offsets = createOffsets(attributes, packFloats);
- const stride = getStride(offsets, attributes, packFloats);
- const maxNumberOfInstancesPerRow = Math.floor(
- ContextLimits.maximumTextureSize / stride
- );
- const instancesPerWidth = Math.min(
- numberOfInstances,
- maxNumberOfInstancesPerRow
- );
- const width = stride * instancesPerWidth;
- const height = Math.ceil(numberOfInstances / instancesPerWidth);
- const stepX = 1.0 / width;
- const centerX = stepX * 0.5;
- const stepY = 1.0 / height;
- const centerY = stepY * 0.5;
- this._textureDimensions = new Cartesian2(width, height);
- this._textureStep = new Cartesian4(stepX, centerX, stepY, centerY);
- this._pixelDatatype = !packFloats
- ? pixelDatatype
- : PixelDatatype.UNSIGNED_BYTE;
- this._packFloats = packFloats;
- this._offsets = offsets;
- this._stride = stride;
- this._texture = undefined;
- const batchLength = 4 * width * height;
- this._batchValues =
- pixelDatatype === PixelDatatype.FLOAT && !packFloats
- ? new Float32Array(batchLength)
- : new Uint8Array(batchLength);
- this._batchValuesDirty = false;
- }
- Object.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) {
- let foundFloatDatatype = false;
- const length = attributes.length;
- for (let 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) {
- const componentsPerAttribute =
- attributes[attributeIndex].componentsPerAttribute;
- if (componentsPerAttribute === 2) {
- return Cartesian2;
- } else if (componentsPerAttribute === 3) {
- return Cartesian3;
- } else if (componentsPerAttribute === 4) {
- return Cartesian4;
- }
- return Number;
- }
- function createOffsets(attributes, packFloats) {
- const offsets = new Array(attributes.length);
- let currentOffset = 0;
- const attributesLength = attributes.length;
- for (let i = 0; i < attributesLength; ++i) {
- const attribute = attributes[i];
- const componentDatatype = attribute.componentDatatype;
- offsets[i] = currentOffset;
- if (componentDatatype !== ComponentDatatype.UNSIGNED_BYTE && packFloats) {
- currentOffset += 4;
- } else {
- ++currentOffset;
- }
- }
- return offsets;
- }
- function getStride(offsets, attributes, packFloats) {
- const length = offsets.length;
- const lastOffset = offsets[length - 1];
- const lastAttribute = attributes[length - 1];
- const componentDatatype = lastAttribute.componentDatatype;
- if (componentDatatype !== ComponentDatatype.UNSIGNED_BYTE && packFloats) {
- return lastOffset + 4;
- }
- return lastOffset + 1;
- }
- const scratchPackedFloatCartesian4 = new Cartesian4();
- function getPackedFloat(array, index, result) {
- let packed = Cartesian4.unpack(array, index, scratchPackedFloatCartesian4);
- const x = Cartesian4.unpackFloat(packed);
- packed = Cartesian4.unpack(array, index + 4, scratchPackedFloatCartesian4);
- const y = Cartesian4.unpackFloat(packed);
- packed = Cartesian4.unpack(array, index + 8, scratchPackedFloatCartesian4);
- const z = Cartesian4.unpackFloat(packed);
- packed = Cartesian4.unpack(array, index + 12, scratchPackedFloatCartesian4);
- const w = Cartesian4.unpackFloat(packed);
- return Cartesian4.fromElements(x, y, z, w, result);
- }
- function setPackedAttribute(value, array, index) {
- let packed = Cartesian4.packFloat(value.x, scratchPackedFloatCartesian4);
- Cartesian4.pack(packed, array, index);
- packed = Cartesian4.packFloat(value.y, packed);
- Cartesian4.pack(packed, array, index + 4);
- packed = Cartesian4.packFloat(value.z, packed);
- Cartesian4.pack(packed, array, index + 8);
- packed = Cartesian4.packFloat(value.w, packed);
- Cartesian4.pack(packed, array, index + 12);
- }
- const 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');
- const attributes = this._attributes;
- const offset = this._offsets[attributeIndex];
- const stride = this._stride;
- const index = 4 * stride * instanceIndex + 4 * offset;
- let value;
- if (
- this._packFloats &&
- attributes[attributeIndex].componentDatatype !== PixelDatatype.UNSIGNED_BYTE
- ) {
- value = getPackedFloat(
- this._batchValues,
- index,
- scratchGetAttributeCartesian4
- );
- } else {
- value = Cartesian4.unpack(
- this._batchValues,
- index,
- scratchGetAttributeCartesian4
- );
- }
- const 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;
- };
- const setAttributeScratchValues = [
- undefined,
- undefined,
- new Cartesian2(),
- new Cartesian3(),
- new Cartesian4(),
- ];
- const 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');
- const attributes = this._attributes;
- const result =
- setAttributeScratchValues[
- attributes[attributeIndex].componentsPerAttribute
- ];
- const currentAttribute = this.getBatchedAttribute(
- instanceIndex,
- attributeIndex,
- result
- );
- const attributeType = getAttributeType(this._attributes, attributeIndex);
- const entriesEqual = defined(attributeType.equals)
- ? attributeType.equals(currentAttribute, value)
- : currentAttribute === value;
- if (entriesEqual) {
- return;
- }
- const 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;
- const offset = this._offsets[attributeIndex];
- const stride = this._stride;
- const index = 4 * stride * instanceIndex + 4 * offset;
- if (
- this._packFloats &&
- attributes[attributeIndex].componentDatatype !== PixelDatatype.UNSIGNED_BYTE
- ) {
- setPackedAttribute(attributeValue, this._batchValues, index);
- } else {
- Cartesian4.pack(attributeValue, this._batchValues, index);
- }
- this._batchValuesDirty = true;
- };
- function createTexture(batchTable, context) {
- const dimensions = batchTable._textureDimensions;
- batchTable._texture = new Texture({
- context: context,
- pixelFormat: PixelFormat.RGBA,
- pixelDatatype: batchTable._pixelDatatype,
- width: dimensions.x,
- height: dimensions.y,
- sampler: Sampler.NEAREST,
- flipY: false,
- });
- }
- function updateTexture(batchTable) {
- const dimensions = batchTable._textureDimensions;
- batchTable._texture.copyFrom({
- source: {
- 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) {
- if (
- (defined(this._texture) && !this._batchValuesDirty) ||
- this._attributes.length === 0
- ) {
- return;
- }
- this._batchValuesDirty = false;
- if (!defined(this._texture)) {
- createTexture(this, frameState.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 () {
- const that = this;
- return function (uniformMap) {
- if (that._attributes.length === 0) {
- return uniformMap;
- }
- const batchUniformMap = {
- batchTexture: function () {
- return that._texture;
- },
- batchTextureDimensions: function () {
- return that._textureDimensions;
- },
- batchTextureStep: function () {
- return that._textureStep;
- },
- };
- return combine(uniformMap, batchUniformMap);
- };
- };
- function getGlslComputeSt(batchTable) {
- const stride = batchTable._stride;
- // 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("
- }${stride}); \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("
- }${stride}); \n` +
- ` float xId = mod(batchId * numberOfAttributes, batchTextureDimensions.x); \n` +
- ` float yId = floor(batchId * numberOfAttributes / batchTextureDimensions.x); \n` +
- ` return vec2(centerX + (xId * stepX), 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) {
- const attributes = batchTable._attributes;
- const attribute = attributes[attributeIndex];
- const componentsPerAttribute = attribute.componentsPerAttribute;
- const functionName = attribute.functionName;
- const functionReturnType = getComponentType(componentsPerAttribute);
- const functionReturnValue = getComponentSwizzle(componentsPerAttribute);
- const offset = batchTable._offsets[attributeIndex];
- let glslFunction =
- `${functionReturnType} ${functionName}(float batchId) \n` +
- `{ \n` +
- ` vec2 st = computeSt(batchId); \n` +
- ` st.x += batchTextureStep.x * float(${offset}); \n`;
- if (
- batchTable._packFloats &&
- attribute.componentDatatype !== PixelDatatype.UNSIGNED_BYTE
- ) {
- glslFunction +=
- "vec4 textureValue; \n" +
- "textureValue.x = czm_unpackFloat(texture(batchTexture, st)); \n" +
- "textureValue.y = czm_unpackFloat(texture(batchTexture, st + vec2(batchTextureStep.x, 0.0))); \n" +
- "textureValue.z = czm_unpackFloat(texture(batchTexture, st + vec2(batchTextureStep.x * 2.0, 0.0))); \n" +
- "textureValue.w = czm_unpackFloat(texture(batchTexture, st + vec2(batchTextureStep.x * 3.0, 0.0))); \n";
- } else {
- glslFunction += " vec4 textureValue = texture(batchTexture, st); \n";
- }
- glslFunction += ` ${functionReturnType} value = textureValue${functionReturnValue}; \n`;
- if (
- batchTable._pixelDatatype === PixelDatatype.UNSIGNED_BYTE &&
- attribute.componentDatatype === ComponentDatatype.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 () {
- const attributes = this._attributes;
- if (attributes.length === 0) {
- return function (source) {
- return source;
- };
- }
- let batchTableShader = "uniform highp sampler2D batchTexture; \n";
- batchTableShader += `${getGlslComputeSt(this)}\n`;
- const length = attributes.length;
- for (let i = 0; i < length; ++i) {
- batchTableShader += getGlslAttributeFunction(this, i);
- }
- return function (source) {
- const mainIndex = source.indexOf("void main");
- const beforeMain = source.substring(0, mainIndex);
- const 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.
- *
- * @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.
- */
- export default BatchTable;
|