// @ts-check import BoundingSphere from "../Core/BoundingSphere.js"; import Cartesian3 from "../Core/Cartesian3.js"; import DeveloperError from "../Core/DeveloperError.js"; import Frozen from "../Core/Frozen.js"; import Matrix4 from "../Core/Matrix4.js"; import assert from "../Core/assert.js"; import ComponentDatatype from "../Core/ComponentDatatype.js"; import defined from "../Core/defined.js"; import Check from "../Core/Check.js"; import AttributeCompression from "../Core/AttributeCompression.js"; import SceneMode from "./SceneMode.js"; import AttributeType from "./AttributeType.js"; import oneTimeWarning from "../Core/oneTimeWarning.js"; import BlendOption from "../Scene/BlendOption.js"; /** @import { Destroyable, TypedArray, TypedArrayConstructor } from "../Core/globalTypes.js"; */ /** @import Context from "../Renderer/Context.js"; */ /** @import FrameState from "./FrameState.js"; */ /** @import BufferPrimitive from "./BufferPrimitive.js"; */ /** @import BufferPrimitiveMaterial from "./BufferPrimitiveMaterial.js"; */ /** @import PickId from "../Renderer/PickId.js"; */ /** * @typedef {object} BufferPrimitiveOptions * @property {Matrix4} [modelMatrix=Matrix4.IDENTITY] Transforms geometry from model to world coordinates. * @property {boolean} [show=true] * @property {BufferPrimitiveMaterial} [material] * @property {number} [featureId] * @property {object} [pickObject] * @experimental This feature is not final and is subject to change without Cesium's standard deprecation policy. */ /** * Collection of primitives held in ArrayBuffer storage for performance and memory optimization. * *

To get the full performance benefit of using a BufferPrimitiveCollection containing "N" primitives, * be careful to avoid allocating "N" instances of any related JavaScript object. {@link BufferPrimitive}, * {@link Color}, {@link Cartesian3}, and other objects can all be reused when working with large collections, * using the {@link https://en.wikipedia.org/wiki/Flyweight_pattern|flyweight pattern}.

* * @abstract * @template T extends BufferPrimitive * @experimental This feature is not final and is subject to change without Cesium's standard deprecation policy. * * @see BufferPrimitive * @see BufferPrimitiveMaterial * @see BufferPointCollection * @see BufferPolylineCollection * @see BufferPolygonCollection */ class BufferPrimitiveCollection { /** @ignore */ static Error = { ERR_RESIZE: "BufferPrimitive range cannot be resized after initialization.", ERR_CAPACITY: "BufferPrimitiveCollection capacity exceeded.", ERR_MULTIPLE_OF_FOUR: "BufferPrimitive byte length must be a multiple of 4.", ERR_OUT_OF_RANGE: "BufferPrimitive buffer access out of range.", }; /** * Resources managed by the collection's renderer. Collections may have multiple renderer * implementations, so the collection should be ignorant of the renderer's implementation * and context data. A collection only has one renderer active at a time. * * @type {Destroyable|null} * @ignore */ _renderContext = null; /** * @param {object} options * @param {Matrix4} [options.modelMatrix=Matrix4.IDENTITY] Transforms geometry from model to world coordinates. * @param {number} [options.primitiveCountMax=BufferPrimitiveCollection.DEFAULT_CAPACITY] * @param {number} [options.vertexCountMax=BufferPrimitiveCollection.DEFAULT_CAPACITY] * @param {boolean} [options.show=true] * @param {ComponentDatatype} [options.positionDatatype=ComponentDatatype.DOUBLE] * @param {boolean} [options.positionNormalized=false] When true, integer position values are treated as normalized, * where the full integer range maps to [-1, 1] (signed) or [0, 1] (unsigned). Only relevant for integer position datatypes * (BYTE, UNSIGNED_BYTE, SHORT, UNSIGNED_SHORT). * @param {boolean} [options.allowPicking=false] When true, primitives are pickable with {@link Scene#pick}. When false, memory and initialization cost are lower. * @param {BoundingSphere} [options.boundingVolume] Bounding volume, in world space, for the collection. When * unspecified, a bounding volume is computed automatically and updated when primitive positions change. When * specified, users are responsible for updating bounding volume as needed. Pre-computing the bounding volume * manually, and updating it only as needed, will improve performance for larger dynamic collections. * @param {boolean} [options.debugShowBoundingVolume=false] * @param {BlendOption} [options.blendOption=BlendOption.TRANSLUCENT] */ constructor(options = Frozen.EMPTY_OBJECT) { /** * Determines if primitives in this collection will be shown. * @type {boolean} * @default true */ this.show = options.show ?? true; /** * Collection blend option; must be OPAQUE or TRANSLUCENT. * @type {BlendOption} * @readonly * @ignore */ this._blendOption = options.blendOption ?? BlendOption.TRANSLUCENT; /** * Transforms geometry from model to world coordinates. * @type {Matrix4} * @default Matrix4.IDENTITY * @readonly * @protected */ this._modelMatrix = Matrix4.clone(options.modelMatrix ?? Matrix4.IDENTITY); /** * @type {BoundingSphere} * @readonly * @protected */ this._boundingVolume = BoundingSphere.clone( options.boundingVolume ?? new BoundingSphere(), new BoundingSphere(), ); /** * @type {boolean} * @readonly * @protected */ this._boundingVolumeAutoUpdate = !defined(options.boundingVolume); /** * When true, primitives are pickable with {@link Scene#pick}. * When false, memory and initialization cost are lower. * @type {boolean} * @readonly * @ignore * @default false */ this._allowPicking = options.allowPicking ?? false; /** * @type {Map} * @readonly * @ignore */ this._pickIds = new Map(); /** * @type {object[]} * @readonly * @ignore */ this._pickObjects = []; /** * This property is for debugging only; it is not for production use nor is it optimized. *

* Draws the bounding sphere for each draw command in the primitive. *

* * @type {boolean} * @default false */ this.debugShowBoundingVolume = options.debugShowBoundingVolume ?? false; /** * @type {number} * @protected * @ignore */ this._primitiveCount = 0; /** * @type {number} * @protected * @ignore */ this._primitiveCountMax = options.primitiveCountMax ?? BufferPrimitiveCollection.DEFAULT_CAPACITY; /** * @type {DataView} * @ignore */ this._primitiveView = null; /** * @type {number} * @ignore */ this._positionCount = 0; /** * @type {number} * @protected * @ignore */ this._positionCountMax = options.vertexCountMax ?? BufferPrimitiveCollection.DEFAULT_CAPACITY; /** * @type {TypedArray} * @ignore */ this._positionView = null; /** * @type {ComponentDatatype} * @ignore */ this._positionDatatype = options.positionDatatype ?? ComponentDatatype.DOUBLE; /** * When true, integer position values represent normalized floats * in [-1, 1] (signed) or [0, 1] (unsigned). Only applicable to integer datatypes. * @type {boolean} * @ignore */ this._positionNormalized = options.positionNormalized ?? false; /** * @type {DataView} * @ignore */ this._materialView = null; // Potentially-dirty primitives are tracked as a contiguous range, with // 'clean' primitives potentially within the range. Individual primitive // 'dirty' flags are source-of-truth. /** * @type {number} * @ignore */ this._dirtyOffset = 0; /** * @type {number} * @ignore */ this._dirtyCount = 0; /** * @type {boolean} * @ignore */ this._dirtyBoundingVolume = false; this._allocatePrimitiveBuffer(); this._allocatePositionBuffer(); this._allocateMaterialBuffer(); } /** * Accessing `this.constructor` can cause JSDoc builds to fail, so use this * protected getter function instead. * @protected * @return {*} * @ignore */ _getCollectionClass() { DeveloperError.throwInstantiationError(); } /** * @protected * @return {*} * @ignore */ _getPrimitiveClass() { DeveloperError.throwInstantiationError(); } /** * @return {*} * @ignore */ _getMaterialClass() { DeveloperError.throwInstantiationError(); } ///////////////////////////////////////////////////////////////////////////// // COLLECTION LIFECYCLE /** * @private * @ignore */ _allocatePrimitiveBuffer() { const layout = this._getPrimitiveClass().Layout; //>>includeStart('debug', pragmas.debug); const { ERR_MULTIPLE_OF_FOUR } = BufferPrimitiveCollection.Error; assert(layout.__BYTE_LENGTH % 4 === 0, ERR_MULTIPLE_OF_FOUR); //>>includeEnd('debug'); this._primitiveView = new DataView( new ArrayBuffer(this._primitiveCountMax * layout.__BYTE_LENGTH), ); } /** * @private * @ignore */ _allocatePositionBuffer() { // @ts-expect-error https://github.com/CesiumGS/cesium/issues/13420 this._positionView = ComponentDatatype.createTypedArray( this._positionDatatype, this._positionCountMax * 3, ); } /** * @private * @ignore */ _allocateMaterialBuffer() { const MaterialClass = this._getMaterialClass(); this._materialView = new DataView( new ArrayBuffer(this._primitiveCountMax * MaterialClass.packedLength), ); } /** * Returns true if this object was destroyed; otherwise, false. * * @returns {boolean} True if this object was destroyed; otherwise, false. */ isDestroyed() { return false; } /** Destroys collection and its GPU resources. */ destroy() { this._pickObjects.length = 0; for (const contextPickIds of this._pickIds.values()) { for (const pickId of contextPickIds) { pickId.destroy(); } } if (defined(this._renderContext)) { this._renderContext.destroy(); this._renderContext = undefined; this._dirtyOffset = 0; this._dirtyCount = this.primitiveCount; } } /** * Sorts primitives of the collection. * * Because sorting changes the indices (but not the feature IDs) of primitives * in the collection, the function also returns an array mapping from previous * index to new index. When sorting repeatedly, the array can be reused and * passed as the 'result' argument for each call. * * @param {Function} sortFn * @param {Uint32Array} result * @returns {Uint32Array} Mapping from previous index to new index. */ sort(sortFn, result = new Uint32Array(this.primitiveCount)) { const PrimitiveClass = this._getPrimitiveClass(); const CollectionClass = this._getCollectionClass(); const { primitiveCount } = this; const a = new PrimitiveClass(); const b = new PrimitiveClass(); // Mapping from NEW index to PREVIOUS index. const dstSrcMap = new Uint32Array(primitiveCount); for (let i = 0; i < primitiveCount; i++) { dstSrcMap[i] = i; } dstSrcMap.sort((indexA, indexB) => sortFn(this.get(indexA, a), this.get(indexB, b)), ); // Mapping from PREVIOUS index to NEW index. for (let i = 0; i < primitiveCount; i++) { result[dstSrcMap[i]] = i; } // Copy primitives to temporary collection, in sort order. const tmp = CollectionClass._cloneEmpty(this); for (let i = 0; i < primitiveCount; i++) { const src = this.get(dstSrcMap[i], a); const dst = tmp.add({}, b); PrimitiveClass.clone(src, dst); } // Assign buffers from temporary collection onto this one. CollectionClass._replaceBuffers(tmp, this); this._dirtyOffset = 0; this._dirtyCount = primitiveCount; return result; } /** * Duplicates the contents of this collection into the result collection. * Result collection is not resized, and must contain enough space for all * primitives in the source collection. Existing primitives in the result * collection will be overwritten. * *

Useful when allocating more space for a collection that has reached its * capacity, and efficiently transferring features to the new collection.

* * @example * const result = new BufferPrimitiveCollection({ ... }); // allocate larger 'result' collection * BufferPrimitiveCollection.clone(collection, result); // copy primitives from 'collection' into 'result' * * @param {BufferPrimitiveCollection} collection * @param {BufferPrimitiveCollection} result * @template T extends BufferPrimitive */ static clone(collection, result) { //>>includeStart('debug', pragmas.debug); const { ERR_CAPACITY } = BufferPrimitiveCollection.Error; assert(collection.primitiveCount <= result.primitiveCountMax, ERR_CAPACITY); assert(collection.vertexCount <= result.vertexCountMax, ERR_CAPACITY); //>>includeEnd('debug'); const layout = collection._getPrimitiveClass().Layout; const MaterialClass = collection._getMaterialClass(); const PrimitiveClass = collection._getPrimitiveClass(); this._copySubDataView( collection._primitiveView, result._primitiveView, collection.primitiveCount * layout.__BYTE_LENGTH, ); this._copySubArray( collection._positionView, result._positionView, collection.vertexCount * 3, ); this._copySubDataView( collection._materialView, result._materialView, collection.primitiveCount * MaterialClass.packedLength, ); result.show = collection.show; result.debugShowBoundingVolume = collection.debugShowBoundingVolume; result._primitiveCount = collection._primitiveCount; result._positionCount = collection._positionCount; // Unset PickIds. const primitive = new PrimitiveClass(); for (let i = 0, il = result.primitiveCount; i < il; i++) { result.get(i, primitive)._pickId = 0; } result._dirtyOffset = 0; result._dirtyCount = result.primitiveCount; collection.boundingVolume.clone(result.boundingVolume); return result; } /** * Returns an empty collection with the same buffer sizes as this collection. * Internal utility for operations requiring a working copy of memory. * * @param {BufferPrimitiveCollection} collection * @returns {BufferPrimitiveCollection} * @template T extends BufferPrimitive * @protected * @abstract * @ignore */ static _cloneEmpty(collection) { DeveloperError.throwInstantiationError(); } /** * Assigns buffers from source collection to target collection, without * validation or side effects. Callers must handle any validation, dirty * flag updates, etc. * * @param {BufferPrimitiveCollection} src * @param {BufferPrimitiveCollection} dst * @template T extends BufferPrimitive * @protected * @ignore */ static _replaceBuffers(src, dst) { dst._primitiveView = src._primitiveView; dst._positionView = src._positionView; dst._materialView = src._materialView; } /** * Rebuilds collection bounding volume. * @protected * @ignore */ _updateBoundingVolume() { // Exclude unused space in the position buffer. let vertices = this._positionView.subarray(0, this._positionCount * 3); if (this._positionNormalized) { vertices = AttributeCompression.dequantize( /** @type {Int8Array|Uint8Array|Int16Array|Uint16Array|Int32Array|Uint32Array} */ ( vertices ), this._positionDatatype, AttributeType.VEC3, this._positionCount, ); } BoundingSphere.fromVertices( vertices, Cartesian3.ZERO, 3, this.boundingVolume, ); BoundingSphere.transform( this.boundingVolume, this.modelMatrix, this.boundingVolume, ); this._dirtyBoundingVolume = false; } /** * Updates PickIds for the given context. * @param {Context} context * @protected * @ignore */ _updatePickIds(context) { let pickIds = this._pickIds.get(context); if (pickIds && pickIds.length === this._primitiveCount) { return; } if (!pickIds) { pickIds = []; this._pickIds.set(context, pickIds); } const collection = this; const PrimitiveClass = this._getPrimitiveClass(); const primitive = new PrimitiveClass(); // Fill in missing PickIDs for recently-added primitives. for (let i = pickIds.length, il = this._primitiveCount; i < il; i++) { this.get(i, primitive); const pickObject = this._pickObjects[i] || { collection: this, index: i, get primitive() { // Cannot reuse primitives; scene.drillPick() appends to a list. return collection.get(i, new PrimitiveClass()); }, }; const pickId = context.createPickId(pickObject); primitive._pickId = pickId.key; pickIds.push(pickId); } } ///////////////////////////////////////////////////////////////////////////// // PRIMITIVE LIFECYCLE /** * Makes the given {@link BufferPrimitive} a view onto this collection's * primitive at the given index, for use when reading/writing primitive * properties. When iterating over a large collection, prefer to reuse * the same BufferPrimitive instance throughout the loop — rebinding * an existing instance to a different primitive is cheap, and avoids * allocating in-memory objects for every object. * * @example * const primitive = new BufferPrimitive(); * for (let i = 0; i < collection.primitiveCount; i++) { * collection.get(i, primitive); * primitive.setColor(Color.RED); * } * * @param {number} index * @param {BufferPrimitive} result * @returns {BufferPrimitive} The BufferPrimitive instance passed as the * 'result' argument, now bound to the specified primitive index. */ get(index, result) { //>>includeStart('debug', pragmas.debug); Check.typeOf.number.greaterThanOrEquals("index", index, 0); Check.typeOf.number.lessThan("index", index, this._primitiveCount); //>>includeEnd('debug'); result._collection = this; result._index = index; result._byteOffset = index * this._getPrimitiveClass().Layout.__BYTE_LENGTH; return result; } /** * Adds a new primitive to the collection, with the specified options. A * {@link BufferPrimitive} instance is linked to the new primitive, using * the 'result' argument if given, or a new instance if not. For repeated * calls, prefer to reuse a single BufferPrimitive instance rather than * allocating a new instance on each call. * * @param {BufferPrimitiveOptions} options * @param {BufferPrimitive} result * @returns {BufferPrimitive} */ add(options = Frozen.EMPTY_OBJECT, result) { //>>includeStart('debug', pragmas.debug); const { ERR_CAPACITY } = BufferPrimitiveCollection.Error; assert(this.primitiveCount < this.primitiveCountMax, ERR_CAPACITY); //>>includeEnd('debug'); const MaterialClass = this._getMaterialClass(); const index = this._primitiveCount++; result = this.get(index, result); result.featureId = options.featureId ?? index; result.show = options.show ?? true; result.setMaterial(options.material ?? MaterialClass.DEFAULT_MATERIAL); result._pickId = 0; // unset result._dirty = true; if (defined(options.pickObject)) { this._pickObjects[index] = options.pickObject; } return result; } /** * Marks primitive at given index as 'dirty', to be updated on next render. * @param {number} index * @ignore */ _makeDirty(index) { if (this._dirtyCount === 0) { this._dirtyCount = 1; this._dirtyOffset = index; } else if (index < this._dirtyOffset) { this._dirtyCount += this._dirtyOffset - index; this._dirtyOffset = index; } else if (index + 1 > this._dirtyOffset + this._dirtyCount) { this._dirtyCount = index + 1 - this._dirtyOffset; } } /** * Marks collection bounding volume as 'dirty', to be updated on next render, * if automatic bounding volume updates are enabled. * @ignore */ _makeDirtyBoundingVolume() { if (this._boundingVolumeAutoUpdate) { this._dirtyBoundingVolume = true; } } ///////////////////////////////////////////////////////////////////////////// // RENDER /** @param {object} frameState */ update(frameState) { if (/** @type {FrameState} */ (frameState).mode !== SceneMode.SCENE3D) { oneTimeWarning( "bufferprim-scenemode", "BufferPrimitiveCollection requires SceneMode.SCENE3D.", ); } if (this._dirtyBoundingVolume) { this._updateBoundingVolume(); } if (this._allowPicking && this._dirtyCount > 0) { this._updatePickIds(/** @type {FrameState} */ (frameState).context); } } ///////////////////////////////////////////////////////////////////////////// // ACCESSORS /** * Number of primitives in collection. Must be <= {@link primitiveCountMax}. * * @type {number} * @readonly */ get primitiveCount() { return this._primitiveCount; } /** * Maximum number of primitives this collection can contain. Must be >= * {@link primitiveCount}. * * @type {number} * @readonly * @default {@link BufferPrimitiveCollection.DEFAULT_CAPACITY} */ get primitiveCountMax() { return this._primitiveCountMax; } /** * Total byte length of buffers owned by this collection. Includes any unused * space allocated by {@link primitiveCountMax}, even if no primitives have * yet been added in that space. * * @type {number} * @readonly */ get byteLength() { return ( this._primitiveView.byteLength + this._positionView.byteLength + this._materialView.byteLength ); } /** * Number of vertices in collection. Must be <= {@link vertexCountMax}. * * @type {number} * @readonly */ get vertexCount() { return this._positionCount; } /** * Maximum number of vertices this collection can contain. Must be >= * {@link vertexCount}. * * @type {number} * @readonly * @default {@link BufferPrimitiveCollection.DEFAULT_CAPACITY} */ get vertexCountMax() { return this._positionCountMax; } /** * Transforms geometry from model to world coordinates. * @type {Matrix4} * @default Matrix4.IDENTITY * @readonly */ get modelMatrix() { return this._modelMatrix; } /** * World-space bounding volume for all primitives in the collection, including both * shown and hidden primitives. * @type {BoundingSphere} * @readonly */ get boundingVolume() { return this._boundingVolume; } /** * The component datatype used to store position values. * @type {ComponentDatatype} * @readonly */ get positionDatatype() { return this._positionDatatype; } /** * When true, integer position values are treated as normalized * values, where the full integer range maps to [-1, 1] (signed) or [0, 1] * (unsigned). * @type {boolean} * @readonly */ get positionNormalized() { return this._positionNormalized; } ///////////////////////////////////////////////////////////////////////////// // UTILS /** * @param {TypedArray} src * @param {TypedArray} dst * @param {number} count * @protected * @ignore */ static _copySubArray(src, dst, count) { for (let i = 0; i < count; i++) { dst[i] = src[i]; } } /** * @param {DataView} src * @param {DataView} dst * @param {number} byteLength * @protected * @ignore */ static _copySubDataView(src, dst, byteLength) { // No need to match the original array type, just copy in 4-byte chunks. this._copySubArray( new Uint32Array(src.buffer, src.byteOffset, src.byteLength / 4), new Uint32Array(dst.buffer, dst.byteOffset, dst.byteLength / 4), byteLength / 4, ); } ///////////////////////////////////////////////////////////////////////////// // DEBUG /** * Returns a JSON-serializable array representing the collection. This encoding * is not memory-efficient, and should generally be used for debugging and * testing. * * @example * console.table(collection.toJSON()); * * @returns {Array} List of JSON-serializable objects, one for each * primitive in the collection. */ toJSON() { const PrimitiveClass = this._getPrimitiveClass(); const primitive = new PrimitiveClass(); const results = []; for (let i = 0, il = this.primitiveCount; i < il; i++) { results.push(this.get(i, primitive).toJSON()); } return results; } } /** * Default capacity of buffers on new collections. A quantity of elements: * number of vertices in the vertex buffer, primitives in the primitive * buffer, etc. This value is arbitrary, and collections cannot be resized, * so specific per-buffer capacities should be provided in the collection * constructor when available. * * @type {number} * @static * @constant */ BufferPrimitiveCollection.DEFAULT_CAPACITY = 1024; export default BufferPrimitiveCollection;