UNPKG

terriajs-cesium

Version:

CesiumJS is a JavaScript library for creating 3D globes and 2D maps in a web browser without a plugin.

1,486 lines (1,333 loc) 78 kB
import AttributeCompression from "../Core/AttributeCompression.js"; import BoundingRectangle from "../Core/BoundingRectangle.js"; import BoundingSphere from "../Core/BoundingSphere.js"; import Cartesian2 from "../Core/Cartesian2.js"; import Cartesian3 from "../Core/Cartesian3.js"; import Check from "../Core/Check.js"; import Color from "../Core/Color.js"; import ComponentDatatype from "../Core/ComponentDatatype.js"; import Frozen from "../Core/Frozen.js"; import defined from "../Core/defined.js"; import destroyObject from "../Core/destroyObject.js"; import EncodedCartesian3 from "../Core/EncodedCartesian3.js"; import IndexDatatype from "../Core/IndexDatatype.js"; import CesiumMath from "../Core/Math.js"; import Matrix4 from "../Core/Matrix4.js"; import Buffer from "../Renderer/Buffer.js"; import BufferUsage from "../Renderer/BufferUsage.js"; import ContextLimits from "../Renderer/ContextLimits.js"; import DrawCommand from "../Renderer/DrawCommand.js"; import Pass from "../Renderer/Pass.js"; import RenderState from "../Renderer/RenderState.js"; import ShaderProgram from "../Renderer/ShaderProgram.js"; import ShaderSource from "../Renderer/ShaderSource.js"; import VertexArrayFacade from "../Renderer/VertexArrayFacade.js"; import BillboardCollectionFS from "../Shaders/BillboardCollectionFS.js"; import BillboardCollectionVS from "../Shaders/BillboardCollectionVS.js"; import Billboard from "./Billboard.js"; import BlendingState from "./BlendingState.js"; import BlendOption from "./BlendOption.js"; import HeightReference, { isHeightReferenceClamp } from "./HeightReference.js"; import HorizontalOrigin from "./HorizontalOrigin.js"; import SceneMode from "./SceneMode.js"; import SDFSettings from "./SDFSettings.js"; import TextureAtlas from "../Renderer/TextureAtlas.js"; import VerticalOrigin from "./VerticalOrigin.js"; import Ellipsoid from "../Core/Ellipsoid.js"; import WebGLConstants from "../Core/WebGLConstants.js"; import deprecationWarning from "../Core/deprecationWarning.js"; const SHOW_INDEX = Billboard.SHOW_INDEX; const POSITION_INDEX = Billboard.POSITION_INDEX; const PIXEL_OFFSET_INDEX = Billboard.PIXEL_OFFSET_INDEX; const EYE_OFFSET_INDEX = Billboard.EYE_OFFSET_INDEX; const HORIZONTAL_ORIGIN_INDEX = Billboard.HORIZONTAL_ORIGIN_INDEX; const VERTICAL_ORIGIN_INDEX = Billboard.VERTICAL_ORIGIN_INDEX; const SCALE_INDEX = Billboard.SCALE_INDEX; const IMAGE_INDEX_INDEX = Billboard.IMAGE_INDEX_INDEX; const COLOR_INDEX = Billboard.COLOR_INDEX; const ROTATION_INDEX = Billboard.ROTATION_INDEX; const ALIGNED_AXIS_INDEX = Billboard.ALIGNED_AXIS_INDEX; const SCALE_BY_DISTANCE_INDEX = Billboard.SCALE_BY_DISTANCE_INDEX; const TRANSLUCENCY_BY_DISTANCE_INDEX = Billboard.TRANSLUCENCY_BY_DISTANCE_INDEX; const PIXEL_OFFSET_SCALE_BY_DISTANCE_INDEX = Billboard.PIXEL_OFFSET_SCALE_BY_DISTANCE_INDEX; const DISTANCE_DISPLAY_CONDITION_INDEX = Billboard.DISTANCE_DISPLAY_CONDITION; const DISABLE_DEPTH_DISTANCE = Billboard.DISABLE_DEPTH_DISTANCE; const TEXTURE_COORDINATE_BOUNDS = Billboard.TEXTURE_COORDINATE_BOUNDS; const SDF_INDEX = Billboard.SDF_INDEX; const SPLIT_DIRECTION_INDEX = Billboard.SPLIT_DIRECTION_INDEX; const NUMBER_OF_PROPERTIES = Billboard.NUMBER_OF_PROPERTIES; let attributeLocations; const attributeLocationsBatched = { positionHighAndScale: 0, positionLowAndRotation: 1, compressedAttribute0: 2, // pixel offset, translate, horizontal origin, vertical origin, show, direction, texture coordinates compressedAttribute1: 3, // aligned axis, translucency by distance, image width compressedAttribute2: 4, // image height, color, pick color, size in meters, valid aligned axis, 13 bits free eyeOffset: 5, // 4 bytes free scaleByDistance: 6, pixelOffsetScaleByDistance: 7, compressedAttribute3: 8, textureCoordinateBoundsOrLabelTranslate: 9, a_batchId: 10, sdf: 11, splitDirection: 12, }; const attributeLocationsInstanced = { direction: 0, positionHighAndScale: 1, positionLowAndRotation: 2, // texture offset in w compressedAttribute0: 3, compressedAttribute1: 4, compressedAttribute2: 5, eyeOffset: 6, // texture range in w scaleByDistance: 7, pixelOffsetScaleByDistance: 8, compressedAttribute3: 9, textureCoordinateBoundsOrLabelTranslate: 10, a_batchId: 11, sdf: 12, splitDirection: 13, }; /** * A renderable collection of billboards. Billboards are viewport-aligned * images positioned in the 3D scene. * <br /><br /> * <div align='center'> * <img src='Images/Billboard.png' width='400' height='300' /><br /> * Example billboards * </div> * <br /><br /> * Billboards are added and removed from the collection using {@link BillboardCollection#add} * and {@link BillboardCollection#remove}. Billboards in a collection automatically share textures * for images with the same identifier. * * @alias BillboardCollection * @constructor * * @param {object} [options] Object with the following properties: * @param {Matrix4} [options.modelMatrix=Matrix4.IDENTITY] The 4x4 transformation matrix that transforms each billboard from model to world coordinates. * @param {boolean} [options.debugShowBoundingVolume=false] For debugging only. Determines if this primitive's commands' bounding spheres are shown. * @param {Scene} [options.scene] Must be passed in for billboards that use the height reference property or will be depth tested against the globe. * @param {BlendOption} [options.blendOption=BlendOption.OPAQUE_AND_TRANSLUCENT] The billboard blending option. The default * is used for rendering both opaque and translucent billboards. However, if either all of the billboards are completely opaque or all are completely translucent, * setting the technique to BlendOption.OPAQUE or BlendOption.TRANSLUCENT can improve performance by up to 2x. * @param {boolean} [options.show=true] Determines if the billboards in the collection will be shown. * @param {number} [options.coarseDepthTestDistance] The distance from the camera, beyond which, billboards are depth-tested against an approximation of the globe ellipsoid rather than against the full globe depth buffer. If unspecified, the default value is determined relative to the value of {@link Ellipsoid.default}. * @param {number} [options.threePointDepthTestDistance] The distance from the camera, within which, billboards with a {@link Billboard#heightReference} value of {@link HeightReference.CLAMP_TO_GROUND} or {@link HeightReference.CLAMP_TO_TERRAIN} are depth tested against three key points. This ensures that if any key point of the billboard is visible, the whole billboard will be visible. If unspecified, the default value is determined relative to the value of {@link Ellipsoid.default}. * @performance For best performance, prefer a few collections, each with many billboards, to * many collections with only a few billboards each. Organize collections so that billboards * with the same update frequency are in the same collection, i.e., billboards that do not * change should be in one collection; billboards that change every frame should be in another * collection; and so on. * * @see BillboardCollection#add * @see BillboardCollection#remove * @see Billboard * @see LabelCollection * * @demo {@link https://sandcastle.cesium.com/index.html?id=billboards|Cesium Sandcastle Billboard Demo} * * @example * // Create a billboard collection with two billboards * const billboards = scene.primitives.add(new Cesium.BillboardCollection()); * billboards.add({ * position : new Cesium.Cartesian3(1.0, 2.0, 3.0), * image : 'url/to/image' * }); * billboards.add({ * position : new Cesium.Cartesian3(4.0, 5.0, 6.0), * image : 'url/to/another/image' * }); */ function BillboardCollection(options) { options = options ?? Frozen.EMPTY_OBJECT; this._scene = options.scene; this._batchTable = options.batchTable; let textureAtlas = options.textureAtlas; // Hidden option for internal use if (!defined(textureAtlas)) { textureAtlas = new TextureAtlas(); } this._textureAtlas = textureAtlas; this._textureAtlasGUID = textureAtlas.guid; this._destroyTextureAtlas = true; this._billboardTextureCache = new Map(); this._sp = undefined; this._spTranslucent = undefined; this._rsOpaque = undefined; this._rsTranslucent = undefined; this._vaf = undefined; this._billboards = []; this._billboardsToUpdate = []; this._billboardsToUpdateIndex = 0; this._billboardsRemoved = false; this._createVertexArray = false; this._shaderRotation = false; this._compiledShaderRotation = false; this._shaderAlignedAxis = false; this._compiledShaderAlignedAxis = false; this._shaderScaleByDistance = false; this._compiledShaderScaleByDistance = false; this._shaderTranslucencyByDistance = false; this._compiledShaderTranslucencyByDistance = false; this._shaderPixelOffsetScaleByDistance = false; this._compiledShaderPixelOffsetScaleByDistance = false; this._shaderDistanceDisplayCondition = false; this._compiledShaderDistanceDisplayCondition = false; this._shaderDisableDepthDistance = false; this._compiledShaderDisableDepthDistance = false; this._shaderClampToGround = false; this._compiledShaderClampToGround = false; this._propertiesChanged = new Uint32Array(NUMBER_OF_PROPERTIES); this._maxSize = 0.0; this._maxEyeOffset = 0.0; this._maxScale = 1.0; this._maxPixelOffset = 0.0; this._allHorizontalCenter = true; this._allVerticalCenter = true; this._allSizedInMeters = true; this._baseVolume = new BoundingSphere(); this._baseVolumeWC = new BoundingSphere(); this._baseVolume2D = new BoundingSphere(); this._boundingVolume = new BoundingSphere(); this._boundingVolumeDirty = false; this._colorCommands = []; this._allBillboardsReady = false; /** * Determines if billboards in this collection will be shown. * * @type {boolean} * @default true */ this.show = options.show ?? true; /** * The 4x4 transformation matrix that transforms each billboard in this collection from model to world coordinates. * When this is the identity matrix, the billboards are drawn in world coordinates, i.e., Earth's WGS84 coordinates. * Local reference frames can be used by providing a different transformation matrix, like that returned * by {@link Transforms.eastNorthUpToFixedFrame}. * * @type {Matrix4} * @default {@link Matrix4.IDENTITY} * * * @example * const center = Cesium.Cartesian3.fromDegrees(-75.59777, 40.03883); * billboards.modelMatrix = Cesium.Transforms.eastNorthUpToFixedFrame(center); * billboards.add({ * image : 'url/to/image', * position : new Cesium.Cartesian3(0.0, 0.0, 0.0) // center * }); * billboards.add({ * image : 'url/to/image', * position : new Cesium.Cartesian3(1000000.0, 0.0, 0.0) // east * }); * billboards.add({ * image : 'url/to/image', * position : new Cesium.Cartesian3(0.0, 1000000.0, 0.0) // north * }); * billboards.add({ * image : 'url/to/image', * position : new Cesium.Cartesian3(0.0, 0.0, 1000000.0) // up * }); * * @see Transforms.eastNorthUpToFixedFrame */ this.modelMatrix = Matrix4.clone(options.modelMatrix ?? Matrix4.IDENTITY); this._modelMatrix = Matrix4.clone(Matrix4.IDENTITY); /** * This property is for debugging only; it is not for production use nor is it optimized. * <p> * Draws the bounding sphere for each draw command in the primitive. * </p> * * @type {boolean} * * @default false */ this.debugShowBoundingVolume = options.debugShowBoundingVolume ?? false; /** * This property is for debugging only; it is not for production use nor is it optimized. * <p> * Draws the texture atlas for this BillboardCollection as a fullscreen quad. * </p> * * @type {boolean} * * @default false */ this.debugShowTextureAtlas = options.debugShowTextureAtlas ?? false; /** * The billboard blending option. The default is used for rendering both opaque and translucent billboards. * However, if either all of the billboards are completely opaque or all are completely translucent, * setting the technique to BlendOption.OPAQUE or BlendOption.TRANSLUCENT can improve * performance by up to 2x. * @type {BlendOption} * @default BlendOption.OPAQUE_AND_TRANSLUCENT */ this.blendOption = options.blendOption ?? BlendOption.OPAQUE_AND_TRANSLUCENT; this._blendOption = undefined; this._mode = SceneMode.SCENE3D; // The buffer usage for each attribute is determined based on the usage of the attribute over time. this._buffersUsage = [ BufferUsage.STATIC_DRAW, // SHOW_INDEX BufferUsage.STATIC_DRAW, // POSITION_INDEX BufferUsage.STATIC_DRAW, // PIXEL_OFFSET_INDEX BufferUsage.STATIC_DRAW, // EYE_OFFSET_INDEX BufferUsage.STATIC_DRAW, // HORIZONTAL_ORIGIN_INDEX BufferUsage.STATIC_DRAW, // VERTICAL_ORIGIN_INDEX BufferUsage.STATIC_DRAW, // SCALE_INDEX BufferUsage.STATIC_DRAW, // IMAGE_INDEX_INDEX BufferUsage.STATIC_DRAW, // COLOR_INDEX BufferUsage.STATIC_DRAW, // ROTATION_INDEX BufferUsage.STATIC_DRAW, // ALIGNED_AXIS_INDEX BufferUsage.STATIC_DRAW, // SCALE_BY_DISTANCE_INDEX BufferUsage.STATIC_DRAW, // TRANSLUCENCY_BY_DISTANCE_INDEX BufferUsage.STATIC_DRAW, // PIXEL_OFFSET_SCALE_BY_DISTANCE_INDEX BufferUsage.STATIC_DRAW, // DISTANCE_DISPLAY_CONDITION_INDEX BufferUsage.STATIC_DRAW, // TEXTURE_COORDINATE_BOUNDS BufferUsage.STATIC_DRAW, // SPLIT_DIRECTION_INDEX ]; this._highlightColor = Color.clone(Color.WHITE); // Only used by Vector3DTilePoints this._coarseDepthTestDistance = options.coarseDepthTestDistance ?? Ellipsoid.default.minimumRadius / 10.0; this._threePointDepthTestDistance = options.threePointDepthTestDistance ?? Ellipsoid.default.minimumRadius / 1000.0; this._uniforms = { u_atlas: () => { return this.textureAtlas.texture; }, u_highlightColor: () => { return this._highlightColor; }, u_coarseDepthTestDistance: () => { return this._coarseDepthTestDistance; }, u_threePointDepthTestDistance: () => { return this._threePointDepthTestDistance; }, }; const scene = this._scene; if (defined(scene) && defined(scene.terrainProviderChanged)) { this._removeCallbackFunc = scene.terrainProviderChanged.addEventListener( function () { const billboards = this._billboards; const length = billboards.length; for (let i = 0; i < length; ++i) { if (defined(billboards[i])) { billboards[i]._updateClamping(); } } }, this, ); } } Object.defineProperties(BillboardCollection.prototype, { /** * Returns the number of billboards in this collection. This is commonly used with * {@link BillboardCollection#get} to iterate over all the billboards * in the collection. * @memberof BillboardCollection.prototype * @type {number} * @readonly */ length: { get: function () { removeBillboards(this); return this._billboards.length; }, }, /** * Gets or sets the textureAtlas. * @memberof BillboardCollection.prototype * @type {TextureAtlas} * @private */ textureAtlas: { get: function () { return this._textureAtlas; }, set: function (value) { //>>includeStart('debug', pragmas.debug); Check.defined("textureAtlas", value); //>>includeEnd('debug'); if (this._textureAtlas !== value) { this._textureAtlas = this._destroyTextureAtlas && this._textureAtlas && this._textureAtlas.destroy(); this._textureAtlas = value; } }, }, /** * Gets or sets a value which determines if the texture atlas is * destroyed when the collection is destroyed. * * If the texture atlas is used by more than one collection, set this to <code>false</code>, * and explicitly destroy the atlas to avoid attempting to destroy it multiple times. * * @memberof BillboardCollection.prototype * @type {boolean} * @private * * @example * // Set destroyTextureAtlas * // Destroy a billboard collection but not its texture atlas. * * const atlas = new TextureAtlas(); * billboards.textureAtlas = atlas; * billboards.destroyTextureAtlas = false; * * billboards = billboards.destroy(); * console.log(atlas.isDestroyed()); // False */ destroyTextureAtlas: { get: function () { return this._destroyTextureAtlas; }, set: function (value) { this._destroyTextureAtlas = value; }, }, /** * Returns the size in bytes of the WebGL texture resources. * @private * @memberof BillboardCollection.prototype * @type {number} * @readonly */ sizeInBytes: { get: function () { return this._textureAtlas.sizeInBytes; }, }, /** * True when all billboards currently in the collection are ready for rendering. * @private * @memberof BillboardCollection.prototype * @type {boolean} * @readonly */ ready: { get: function () { return this._allBillboardsReady; }, }, /** * Cache of loaded billboard images. * @private * @memberof BillboardCollection.prototype * @type {Map<string, BillboardTexture>} * @readonly */ billboardTextureCache: { get: function () { return this._billboardTextureCache; }, }, /** * The distance from the camera, beyond which, billboards are depth-tested against an approximation of * the globe ellipsoid rather than against the full globe depth buffer. When set to <code>0</code>, the * approximate depth test is always applied. When set to <code>Number.POSITIVE_INFINITY</code>, the * approximate depth test is never applied. * <br/><br/> * This setting only applies when a billboard's {@link Billboard#disableDepthTestDistance} value would * otherwise allow depth testing—i.e., distance from the camera to the billboard is less than a * billboard's {@link Billboard#disableDepthTestDistance} value. * @memberof BillboardCollection.prototype * @type {number} */ coarseDepthTestDistance: { get: function () { return this._coarseDepthTestDistance; }, set: function (value) { //>>includeStart('debug', pragmas.debug); Check.typeOf.number("coarseDepthTestDistance", value); //>>includeEnd('debug'); this._coarseDepthTestDistance = value; }, }, /** * The distance from the camera, within which, billboards with a {@link Billboard#heightReference} value * of {@link HeightReference.CLAMP_TO_GROUND} or {@link HeightReference.CLAMP_TO_TERRAIN} are depth tested * against three key points. This ensures that if any key point of the billboard is visible, the whole * billboard will be visible. When set to <code>0</code>, this feature is disabled and portions of a * billboards behind terrain be clipped. * <br/><br/> * This setting only applies when a billboard's {@link Billboard#disableDepthTestDistance} value would * otherwise allow depth testing—i.e., distance from the camera to the billboard is less than a * billboard's {@link Billboard#disableDepthTestDistance} value. * @see {@link https://cesium.com/blog/2018/07/30/billboards-on-terrain-improvements/|Billboards and Labels on Terrain Improvements} * @memberof BillboardCollection.prototype * @type {number} */ threePointDepthTestDistance: { get: function () { return this._threePointDepthTestDistance; }, set: function (value) { //>>includeStart('debug', pragmas.debug); Check.typeOf.number("threePointDepthTestDistance", value); //>>includeEnd('debug'); this._threePointDepthTestDistance = value; }, }, }); function destroyBillboards(billboards) { const length = billboards.length; for (let i = 0; i < length; ++i) { if (billboards[i]) { billboards[i]._destroy(); } } } /** * Creates and adds a billboard with the specified initial properties to the collection. * The added billboard is returned so it can be modified or removed from the collection later. * * @param {Billboard.ConstructorOptions}[options] A template describing the billboard's properties as shown in Example 1. * @returns {Billboard} The billboard that was added to the collection. * * @performance Calling <code>add</code> is expected constant time. However, the collection's vertex buffer * is rewritten - an <code>O(n)</code> operation that also incurs CPU to GPU overhead. For * best performance, add as many billboards as possible before calling <code>update</code>. * * @exception {DeveloperError} This object was destroyed, i.e., destroy() was called. * * * @example * // Example 1: Add a billboard, specifying all the default values. * const b = billboards.add({ * show : true, * position : Cesium.Cartesian3.ZERO, * pixelOffset : Cesium.Cartesian2.ZERO, * eyeOffset : Cesium.Cartesian3.ZERO, * heightReference : Cesium.HeightReference.NONE, * horizontalOrigin : Cesium.HorizontalOrigin.CENTER, * verticalOrigin : Cesium.VerticalOrigin.CENTER, * scale : 1.0, * image : 'url/to/image', * imageSubRegion : undefined, * color : Cesium.Color.WHITE, * id : undefined, * rotation : 0.0, * alignedAxis : Cesium.Cartesian3.ZERO, * width : undefined, * height : undefined, * scaleByDistance : undefined, * translucencyByDistance : undefined, * pixelOffsetScaleByDistance : undefined, * sizeInMeters : false, * distanceDisplayCondition : undefined * }); * * @example * // Example 2: Specify only the billboard's cartographic position. * const b = billboards.add({ * position : Cesium.Cartesian3.fromDegrees(longitude, latitude, height) * }); * * @see BillboardCollection#remove * @see BillboardCollection#removeAll */ BillboardCollection.prototype.add = function (options) { const billboard = new Billboard(options, this); billboard._index = this._billboards.length; this._billboards.push(billboard); this._createVertexArray = true; return billboard; }; /** * Removes a billboard from the collection. * * @param {Billboard} billboard The billboard to remove. * @returns {boolean} <code>true</code> if the billboard was removed; <code>false</code> if the billboard was not found in the collection. * * @performance Calling <code>remove</code> is expected constant time. However, the collection's vertex buffer * is rewritten - an <code>O(n)</code> operation that also incurs CPU to GPU overhead. For * best performance, remove as many billboards as possible before calling <code>update</code>. * If you intend to temporarily hide a billboard, it is usually more efficient to call * {@link Billboard#show} instead of removing and re-adding the billboard. * * @exception {DeveloperError} This object was destroyed, i.e., destroy() was called. * * * @example * const b = billboards.add(...); * billboards.remove(b); // Returns true * * @see BillboardCollection#add * @see BillboardCollection#removeAll * @see Billboard#show */ BillboardCollection.prototype.remove = function (billboard) { if (this.contains(billboard)) { this._billboards[billboard._index] = undefined; // Removed later this._billboardsRemoved = true; this._createVertexArray = true; billboard._destroy(); return true; } return false; }; /** * Removes all billboards from the collection. * * @performance <code>O(n)</code>. It is more efficient to remove all the billboards * from a collection and then add new ones than to create a new collection entirely. * * @exception {DeveloperError} This object was destroyed, i.e., destroy() was called. * * * @example * billboards.add(...); * billboards.add(...); * billboards.removeAll(); * * @see BillboardCollection#add * @see BillboardCollection#remove */ BillboardCollection.prototype.removeAll = function () { destroyBillboards(this._billboards); this._billboards = []; this._billboardsToUpdate = []; this._billboardsToUpdateIndex = 0; this._billboardsRemoved = false; this._createVertexArray = true; }; function removeBillboards(billboardCollection) { if (billboardCollection._billboardsRemoved) { billboardCollection._billboardsRemoved = false; const newBillboards = []; const billboards = billboardCollection._billboards; const length = billboards.length; for (let i = 0, j = 0; i < length; ++i) { const billboard = billboards[i]; if (defined(billboard)) { billboard._index = j++; newBillboards.push(billboard); } } billboardCollection._billboards = newBillboards; } } BillboardCollection.prototype._updateBillboard = function ( billboard, propertyChanged, ) { if (!billboard._dirty) { this._billboardsToUpdate[this._billboardsToUpdateIndex++] = billboard; } ++this._propertiesChanged[propertyChanged]; }; /** * Check whether this collection contains a given billboard. * * @param {Billboard} [billboard] The billboard to check for. * @returns {boolean} true if this collection contains the billboard, false otherwise. * * @see BillboardCollection#get */ BillboardCollection.prototype.contains = function (billboard) { return defined(billboard) && billboard._billboardCollection === this; }; /** * Returns the billboard in the collection at the specified index. Indices are zero-based * and increase as billboards are added. Removing a billboard shifts all billboards after * it to the left, changing their indices. This function is commonly used with * {@link BillboardCollection#length} to iterate over all the billboards * in the collection. * * @param {number} index The zero-based index of the billboard. * @returns {Billboard} The billboard at the specified index. * * @performance Expected constant time. If billboards were removed from the collection and * {@link BillboardCollection#update} was not called, an implicit <code>O(n)</code> * operation is performed. * * @exception {DeveloperError} This object was destroyed, i.e., destroy() was called. * * * @example * // Toggle the show property of every billboard in the collection * const len = billboards.length; * for (let i = 0; i < len; ++i) { * const b = billboards.get(i); * b.show = !b.show; * } * * @see BillboardCollection#length */ BillboardCollection.prototype.get = function (index) { //>>includeStart('debug', pragmas.debug); Check.typeOf.number("index", index); //>>includeEnd('debug'); removeBillboards(this); return this._billboards[index]; }; let getIndexBuffer; function getIndexBufferBatched(context) { const sixteenK = 16 * 1024; let indexBuffer = context.cache.billboardCollection_indexBufferBatched; if (defined(indexBuffer)) { return indexBuffer; } // Subtract 6 because the last index is reserverd for primitive restart. // https://www.khronos.org/registry/webgl/specs/latest/2.0/#5.18 const length = sixteenK * 6 - 6; const indices = new Uint16Array(length); for (let i = 0, j = 0; i < length; i += 6, j += 4) { indices[i] = j; indices[i + 1] = j + 1; indices[i + 2] = j + 2; indices[i + 3] = j + 0; indices[i + 4] = j + 2; indices[i + 5] = j + 3; } // PERFORMANCE_IDEA: Should we reference count billboard collections, and eventually delete this? // Is this too much memory to allocate up front? Should we dynamically grow it? indexBuffer = Buffer.createIndexBuffer({ context: context, typedArray: indices, usage: BufferUsage.STATIC_DRAW, indexDatatype: IndexDatatype.UNSIGNED_SHORT, }); indexBuffer.vertexArrayDestroyable = false; context.cache.billboardCollection_indexBufferBatched = indexBuffer; return indexBuffer; } function getIndexBufferInstanced(context) { let indexBuffer = context.cache.billboardCollection_indexBufferInstanced; if (defined(indexBuffer)) { return indexBuffer; } indexBuffer = Buffer.createIndexBuffer({ context: context, typedArray: new Uint16Array([0, 1, 2, 0, 2, 3]), usage: BufferUsage.STATIC_DRAW, indexDatatype: IndexDatatype.UNSIGNED_SHORT, }); indexBuffer.vertexArrayDestroyable = false; context.cache.billboardCollection_indexBufferInstanced = indexBuffer; return indexBuffer; } function getVertexBufferInstanced(context) { let vertexBuffer = context.cache.billboardCollection_vertexBufferInstanced; if (defined(vertexBuffer)) { return vertexBuffer; } vertexBuffer = Buffer.createVertexBuffer({ context: context, typedArray: new Float32Array([0.0, 0.0, 1.0, 0.0, 1.0, 1.0, 0.0, 1.0]), usage: BufferUsage.STATIC_DRAW, }); vertexBuffer.vertexArrayDestroyable = false; context.cache.billboardCollection_vertexBufferInstanced = vertexBuffer; return vertexBuffer; } BillboardCollection.prototype.computeNewBuffersUsage = function () { const buffersUsage = this._buffersUsage; let usageChanged = false; const properties = this._propertiesChanged; for (let k = 0; k < NUMBER_OF_PROPERTIES; ++k) { const newUsage = properties[k] === 0 ? BufferUsage.STATIC_DRAW : BufferUsage.STREAM_DRAW; usageChanged = usageChanged || buffersUsage[k] !== newUsage; buffersUsage[k] = newUsage; } return usageChanged; }; function createVAF( context, numberOfBillboards, buffersUsage, instanced, batchTable, sdf, ) { const attributes = [ { index: attributeLocations.positionHighAndScale, componentsPerAttribute: 4, componentDatatype: ComponentDatatype.FLOAT, usage: buffersUsage[POSITION_INDEX], }, { index: attributeLocations.positionLowAndRotation, componentsPerAttribute: 4, componentDatatype: ComponentDatatype.FLOAT, usage: buffersUsage[POSITION_INDEX], }, { index: attributeLocations.compressedAttribute0, componentsPerAttribute: 4, componentDatatype: ComponentDatatype.FLOAT, usage: buffersUsage[PIXEL_OFFSET_INDEX], }, { index: attributeLocations.compressedAttribute1, componentsPerAttribute: 4, componentDatatype: ComponentDatatype.FLOAT, usage: buffersUsage[TRANSLUCENCY_BY_DISTANCE_INDEX], }, { index: attributeLocations.compressedAttribute2, componentsPerAttribute: 4, componentDatatype: ComponentDatatype.FLOAT, usage: buffersUsage[COLOR_INDEX], }, { index: attributeLocations.eyeOffset, componentsPerAttribute: 4, componentDatatype: ComponentDatatype.FLOAT, usage: buffersUsage[EYE_OFFSET_INDEX], }, { index: attributeLocations.scaleByDistance, componentsPerAttribute: 4, componentDatatype: ComponentDatatype.FLOAT, usage: buffersUsage[SCALE_BY_DISTANCE_INDEX], }, { index: attributeLocations.pixelOffsetScaleByDistance, componentsPerAttribute: 4, componentDatatype: ComponentDatatype.FLOAT, usage: buffersUsage[PIXEL_OFFSET_SCALE_BY_DISTANCE_INDEX], }, { index: attributeLocations.compressedAttribute3, componentsPerAttribute: 4, componentDatatype: ComponentDatatype.FLOAT, usage: buffersUsage[DISTANCE_DISPLAY_CONDITION_INDEX], }, { index: attributeLocations.textureCoordinateBoundsOrLabelTranslate, componentsPerAttribute: 4, componentDatatype: ComponentDatatype.FLOAT, usage: buffersUsage[TEXTURE_COORDINATE_BOUNDS], }, { index: attributeLocations.splitDirection, componentsPerAttribute: 1, componentDatatype: ComponentDatatype.FLOAT, usage: buffersUsage[SPLIT_DIRECTION_INDEX], }, ]; // Instancing requires one non-instanced attribute. if (instanced) { attributes.push({ index: attributeLocations.direction, componentsPerAttribute: 2, componentDatatype: ComponentDatatype.FLOAT, vertexBuffer: getVertexBufferInstanced(context), }); } if (defined(batchTable)) { attributes.push({ index: attributeLocations.a_batchId, componentsPerAttribute: 1, componentDatatype: ComponentDatatype.FLOAT, bufferUsage: BufferUsage.STATIC_DRAW, }); } if (sdf) { attributes.push({ index: attributeLocations.sdf, componentsPerAttribute: 2, componentDatatype: ComponentDatatype.FLOAT, usage: buffersUsage[SDF_INDEX], }); } // When instancing is enabled, only one vertex is needed for each billboard. const sizeInVertices = instanced ? numberOfBillboards : 4 * numberOfBillboards; return new VertexArrayFacade(context, attributes, sizeInVertices, instanced); } /////////////////////////////////////////////////////////////////////////// // Four vertices per billboard. Each has the same position, etc., but a different screen-space direction vector. // PERFORMANCE_IDEA: Save memory if a property is the same for all billboards, use a latched attribute state, // instead of storing it in a vertex buffer. const writePositionScratch = new EncodedCartesian3(); function writePositionScaleAndRotation( billboardCollection, frameState, vafWriters, billboard, ) { let i; const positionHighWriter = vafWriters[attributeLocations.positionHighAndScale]; const positionLowWriter = vafWriters[attributeLocations.positionLowAndRotation]; const position = billboard._getActualPosition(); if (billboardCollection._mode === SceneMode.SCENE3D) { BoundingSphere.expand( billboardCollection._baseVolume, position, billboardCollection._baseVolume, ); billboardCollection._boundingVolumeDirty = true; } EncodedCartesian3.fromCartesian(position, writePositionScratch); const scale = billboard.scale; const rotation = billboard.rotation; if (rotation !== 0.0) { billboardCollection._shaderRotation = true; } billboardCollection._maxScale = Math.max( billboardCollection._maxScale, scale, ); const high = writePositionScratch.high; const low = writePositionScratch.low; if (billboardCollection._instanced) { i = billboard._index; positionHighWriter(i, high.x, high.y, high.z, scale); positionLowWriter(i, low.x, low.y, low.z, rotation); } else { i = billboard._index * 4; positionHighWriter(i + 0, high.x, high.y, high.z, scale); positionHighWriter(i + 1, high.x, high.y, high.z, scale); positionHighWriter(i + 2, high.x, high.y, high.z, scale); positionHighWriter(i + 3, high.x, high.y, high.z, scale); positionLowWriter(i + 0, low.x, low.y, low.z, rotation); positionLowWriter(i + 1, low.x, low.y, low.z, rotation); positionLowWriter(i + 2, low.x, low.y, low.z, rotation); positionLowWriter(i + 3, low.x, low.y, low.z, rotation); } } const scratchCartesian2 = new Cartesian2(); const UPPER_BOUND = 32768.0; // 2^15 const LEFT_SHIFT16 = 65536.0; // 2^16 const LEFT_SHIFT12 = 4096.0; // 2^12 const LEFT_SHIFT8 = 256.0; // 2^8 const LEFT_SHIFT7 = 128.0; const LEFT_SHIFT5 = 32.0; const LEFT_SHIFT3 = 8.0; const LEFT_SHIFT2 = 4.0; const RIGHT_SHIFT8 = 1.0 / 256.0; const LOWER_LEFT = 0.0; const LOWER_RIGHT = 2.0; const UPPER_RIGHT = 3.0; const UPPER_LEFT = 1.0; const scratchBoundingRectangle = new BoundingRectangle(); function writeCompressedAttrib0( billboardCollection, frameState, vafWriters, billboard, ) { let i; const writer = vafWriters[attributeLocations.compressedAttribute0]; const pixelOffset = billboard.pixelOffset; const pixelOffsetX = pixelOffset.x; const pixelOffsetY = pixelOffset.y; const translate = billboard._translate; const translateX = translate.x; const translateY = translate.y; billboardCollection._maxPixelOffset = Math.max( billboardCollection._maxPixelOffset, Math.abs(pixelOffsetX + translateX), Math.abs(-pixelOffsetY + translateY), ); const horizontalOrigin = billboard.horizontalOrigin; let verticalOrigin = billboard._verticalOrigin; let show = billboard.show && billboard.clusterShow; // If the color alpha is zero, do not show this billboard. This lets us avoid providing // color during the pick pass and also eliminates a discard in the fragment shader. if (billboard.color.alpha === 0.0) { show = false; } // Raw billboards don't distinguish between BASELINE and BOTTOM, only LabelCollection does that. if (verticalOrigin === VerticalOrigin.BASELINE) { verticalOrigin = VerticalOrigin.BOTTOM; } billboardCollection._allHorizontalCenter = billboardCollection._allHorizontalCenter && horizontalOrigin === HorizontalOrigin.CENTER; billboardCollection._allVerticalCenter = billboardCollection._allVerticalCenter && verticalOrigin === VerticalOrigin.CENTER; let bottomLeftX = 0; let bottomLeftY = 0; let width = 0; let height = 0; if (billboard.ready) { const imageRectangle = billboard.computeTextureCoordinates( scratchBoundingRectangle, ); bottomLeftX = imageRectangle.x; bottomLeftY = imageRectangle.y; width = imageRectangle.width; height = imageRectangle.height; } const topRightX = bottomLeftX + width; const topRightY = bottomLeftY + height; let compressed0 = Math.floor( CesiumMath.clamp(pixelOffsetX, -UPPER_BOUND, UPPER_BOUND) + UPPER_BOUND, ) * LEFT_SHIFT7; compressed0 += (horizontalOrigin + 1.0) * LEFT_SHIFT5; compressed0 += (verticalOrigin + 1.0) * LEFT_SHIFT3; compressed0 += (show ? 1.0 : 0.0) * LEFT_SHIFT2; let compressed1 = Math.floor( CesiumMath.clamp(pixelOffsetY, -UPPER_BOUND, UPPER_BOUND) + UPPER_BOUND, ) * LEFT_SHIFT8; // We scale `translate` by LEFT_SHIFT2 before encoding it (and unscale after decoding in the shader) // to preserve some subpixel precision (1 / 4 = 0.25 pixels). This mitigates rounding errors in aligning glyphs. // The cost of increasing this scaling factor is that it decreases the range of representable `translate` values // by the same scaling factor. Value must be kept in sync with the shader. let compressed2 = Math.floor( CesiumMath.clamp(translateX * LEFT_SHIFT2, -UPPER_BOUND, UPPER_BOUND) + UPPER_BOUND, ) * LEFT_SHIFT8; const tempTanslateY = (CesiumMath.clamp(translateY * LEFT_SHIFT2, -UPPER_BOUND, UPPER_BOUND) + UPPER_BOUND) * RIGHT_SHIFT8; const upperTranslateY = Math.floor(tempTanslateY); const lowerTranslateY = Math.floor( (tempTanslateY - upperTranslateY) * LEFT_SHIFT8, ); compressed1 += upperTranslateY; compressed2 += lowerTranslateY; scratchCartesian2.x = bottomLeftX; scratchCartesian2.y = bottomLeftY; const compressedTexCoordsLL = AttributeCompression.compressTextureCoordinates(scratchCartesian2); scratchCartesian2.x = topRightX; const compressedTexCoordsLR = AttributeCompression.compressTextureCoordinates(scratchCartesian2); scratchCartesian2.y = topRightY; const compressedTexCoordsUR = AttributeCompression.compressTextureCoordinates(scratchCartesian2); scratchCartesian2.x = bottomLeftX; const compressedTexCoordsUL = AttributeCompression.compressTextureCoordinates(scratchCartesian2); if (billboardCollection._instanced) { i = billboard._index; writer(i, compressed0, compressed1, compressed2, compressedTexCoordsLL); } else { i = billboard._index * 4; writer( i + 0, compressed0 + LOWER_LEFT, compressed1, compressed2, compressedTexCoordsLL, ); writer( i + 1, compressed0 + LOWER_RIGHT, compressed1, compressed2, compressedTexCoordsLR, ); writer( i + 2, compressed0 + UPPER_RIGHT, compressed1, compressed2, compressedTexCoordsUR, ); writer( i + 3, compressed0 + UPPER_LEFT, compressed1, compressed2, compressedTexCoordsUL, ); } } function writeCompressedAttrib1( billboardCollection, frameState, vafWriters, billboard, ) { let i; const writer = vafWriters[attributeLocations.compressedAttribute1]; const alignedAxis = billboard.alignedAxis; if (!Cartesian3.equals(alignedAxis, Cartesian3.ZERO)) { billboardCollection._shaderAlignedAxis = true; } let near = 0.0; let nearValue = 1.0; let far = 1.0; let farValue = 1.0; const translucency = billboard.translucencyByDistance; if (defined(translucency)) { near = translucency.near; nearValue = translucency.nearValue; far = translucency.far; farValue = translucency.farValue; if (nearValue !== 1.0 || farValue !== 1.0) { // translucency by distance calculation in shader need not be enabled // until a billboard with near and far !== 1.0 is found billboardCollection._shaderTranslucencyByDistance = true; } } const imageWidth = Math.round(billboard.width ?? 0); billboardCollection._maxSize = Math.max( billboardCollection._maxSize, imageWidth, ); let compressed0 = CesiumMath.clamp(imageWidth, 0.0, LEFT_SHIFT16); let compressed1 = 0.0; if ( Math.abs(Cartesian3.magnitudeSquared(alignedAxis) - 1.0) < CesiumMath.EPSILON6 ) { compressed1 = AttributeCompression.octEncodeFloat(alignedAxis); } nearValue = CesiumMath.clamp(nearValue, 0.0, 1.0); nearValue = nearValue === 1.0 ? 255.0 : (nearValue * 255.0) | 0; compressed0 = compressed0 * LEFT_SHIFT8 + nearValue; farValue = CesiumMath.clamp(farValue, 0.0, 1.0); farValue = farValue === 1.0 ? 255.0 : (farValue * 255.0) | 0; compressed1 = compressed1 * LEFT_SHIFT8 + farValue; if (billboardCollection._instanced) { i = billboard._index; writer(i, compressed0, compressed1, near, far); } else { i = billboard._index * 4; writer(i + 0, compressed0, compressed1, near, far); writer(i + 1, compressed0, compressed1, near, far); writer(i + 2, compressed0, compressed1, near, far); writer(i + 3, compressed0, compressed1, near, far); } } function writeCompressedAttrib2( billboardCollection, frameState, vafWriters, billboard, ) { let i; const writer = vafWriters[attributeLocations.compressedAttribute2]; const color = billboard.color; const pickColor = !defined(billboardCollection._batchTable) ? billboard.getPickId(frameState.context).color : Color.WHITE; const sizeInMeters = billboard.sizeInMeters ? 1.0 : 0.0; const validAlignedAxis = Math.abs(Cartesian3.magnitudeSquared(billboard.alignedAxis) - 1.0) < CesiumMath.EPSILON6 ? 1.0 : 0.0; billboardCollection._allSizedInMeters = billboardCollection._allSizedInMeters && sizeInMeters === 1.0; const imageHeight = billboard.height ?? 0; billboardCollection._maxSize = Math.max( billboardCollection._maxSize, imageHeight, ); let labelHorizontalOrigin = billboard._labelHorizontalOrigin ?? -2; labelHorizontalOrigin += 2; const compressed0 = AttributeCompression.encodeRGB8(color); const compressed1 = AttributeCompression.encodeRGB8(pickColor); const compressed2 = Color.floatToByte(color.alpha) * LEFT_SHIFT16 + Color.floatToByte(pickColor.alpha) * LEFT_SHIFT8 + (sizeInMeters * 2.0 + validAlignedAxis); const compressed3 = imageHeight * LEFT_SHIFT2 + labelHorizontalOrigin; if (billboardCollection._instanced) { i = billboard._index; writer(i, compressed0, compressed1, compressed2, compressed3); } else { i = billboard._index * 4; writer(i + 0, compressed0, compressed1, compressed2, compressed3); writer(i + 1, compressed0, compressed1, compressed2, compressed3); writer(i + 2, compressed0, compressed1, compressed2, compressed3); writer(i + 3, compressed0, compressed1, compressed2, compressed3); } } function writeEyeOffset( billboardCollection, frameState, vafWriters, billboard, ) { let i; const writer = vafWriters[attributeLocations.eyeOffset]; const eyeOffset = billboard.eyeOffset; // For billboards that are clamped to ground, move it slightly closer to the camera let eyeOffsetZ = eyeOffset.z; if (billboard._heightReference !== HeightReference.NONE) { eyeOffsetZ *= 1.005; } billboardCollection._maxEyeOffset = Math.max( billboardCollection._maxEyeOffset, Math.abs(eyeOffset.x), Math.abs(eyeOffset.y), Math.abs(eyeOffsetZ), ); if (billboardCollection._instanced) { scratchCartesian2.x = 0; scratchCartesian2.y = 0; if (billboard.ready) { const imageRectangle = billboard.computeTextureCoordinates( scratchBoundingRectangle, ); scratchCartesian2.x = imageRectangle.width; scratchCartesian2.y = imageRectangle.height; } const compressedTexCoordsRange = AttributeCompression.compressTextureCoordinates(scratchCartesian2); i = billboard._index; writer(i, eyeOffset.x, eyeOffset.y, eyeOffsetZ, compressedTexCoordsRange); } else { i = billboard._index * 4; writer(i + 0, eyeOffset.x, eyeOffset.y, eyeOffsetZ, 0.0); writer(i + 1, eyeOffset.x, eyeOffset.y, eyeOffsetZ, 0.0); writer(i + 2, eyeOffset.x, eyeOffset.y, eyeOffsetZ, 0.0); writer(i + 3, eyeOffset.x, eyeOffset.y, eyeOffsetZ, 0.0); } } function writeScaleByDistance( billboardCollection, frameState, vafWriters, billboard, ) { let i; const writer = vafWriters[attributeLocations.scaleByDistance]; let near = 0.0; let nearValue = 1.0; let far = 1.0; let farValue = 1.0; const scale = billboard.scaleByDistance; if (defined(scale)) { near = scale.near; nearValue = scale.nearValue; far = scale.far; farValue = scale.farValue; if (nearValue !== 1.0 || farValue !== 1.0) { // scale by distance calculation in shader need not be enabled // until a billboard with near and far !== 1.0 is found billboardCollection._shaderScaleByDistance = true; } } if (billboardCollection._instanced) { i = billboard._index; writer(i, near, nearValue, far, farValue); } else { i = billboard._index * 4; writer(i + 0, near, nearValue, far, farValue); writer(i + 1, near, nearValue, far, farValue); writer(i + 2, near, nearValue, far, farValue); writer(i + 3, near, nearValue, far, farValue); } } function writePixelOffsetScaleByDistance( billboardCollection, frameState, vafWriters, billboard, ) { let i; const writer = vafWriters[attributeLocations.pixelOffsetScaleByDistance]; let near = 0.0; let nearValue = 1.0; let far = 1.0; let farValue = 1.0; const pixelOffsetScale = billboard.pixelOffsetScaleByDistance; if (defined(pixelOffsetScale)) { near = pixelOffsetScale.near; nearValue = pixelOffsetScale.nearValue; far = pixelOffsetScale.far; farValue = pixelOffsetScale.farValue; if (nearValue !== 1.0 || farValue !== 1.0) { // pixelOffsetScale by distance calculation in shader need not be enabled // until a billboard with near and far !== 1.0 is found billboardCollection._shaderPixelOffsetScaleByDistance = true; } } if (billboardCollection._instanced) { i = billboard._index; writer(i, near, nearValue, far, farValue); } else { i = billboard._index * 4; writer(i + 0, near, nearValue, far, farValue); writer(i + 1, near, nearValue, far, farValue); writer(i + 2, near, nearValue, far, farValue); writer(i + 3, near, nearValue, far, farValue); } } function writeCompressedAttribute3( billboardCollection, frameState, vafWriters, billboard, ) { let i; const writer = vafWriters[attributeLocations.compressedAttribute3]; let near = 0.0; let far = Number.MAX_VALUE; const distanceDisplayCondition = billboard.distanceDisplayCondition; if (defined(distanceDisplayCondition)) { near = distanceDisplayCondition.near; far = distanceDisplayCondition.far; near *= near; far *= far; billboardCollection._shaderDistanceDisplayCondition = true; } let disableDepthTestDistance = billboard.disableDepthTestDistance; const clampToGround = isHeightReferenceClamp(billboard.heightReference) && frameState.context.depthTexture; disableDepthTestDistance *= disableDepthTestDistance; if (clampToGround || disableDepthTestDistance > 0.0) { billboardCollection._shaderDisableDepthDistance = true; if (disableDepthTestDistance === Number.POSITIVE_INFINITY) { disableDepthTestDistance = -1.0; } } let imageHeight; let imageWidth; if (!defined(billboard._labelDimensions)) { imageWidth = billboard.width ?? 0; imageHeight = billboard.height ?? 0; } else { imageWidth = billboard._labelDimensions.x; imageHeight = billboard._labelDimensions.y; } const w = Math.floor(CesiumMath.clamp(imageWidth, 0.0, LEFT_SHIFT12)); const h = Math.floor(CesiumMath.clamp(imageHeight, 0.0, LEFT_SHIFT12)); const dimensions = w * LEFT_SHIFT12 + h; if (billboardCollection._instanced) { i = billboard._index; writer(i, near, far, disableDepthTestDistance, dimensions); } else { i = billboard._index * 4; writer(i + 0, near, far, disableDepthTestDistance, dimensions); writer(i + 1, near, far, disableDepthTestDistance, dimensions); writer(i + 2, near, far, disableDepthTestDistance, dimensions); writer(i + 3, near, far, disableDepthTestDistance, dimensions); } } function writeTextureCoordinateBoundsOrLabelTranslate( billboardCollection, frameState, vafWriters, billboard, ) { if (isHeightReferenceClamp(billboard.heightReference)) { const scene = billboardCollection._scene; const context = frameState.context; const globeTranslucent = frameState.globeTranslucencyState.translucent; const depthTestAgainstTerrain = defined(scene.globe) && scene.globe.depthTestAgainstTerrain; // Only do manual depth test if the globe is opaque and writes depth billboardCollection._shaderClampToGround = context.depthTexture && !globeTranslucent && depthTestAgainstTerrain; } let i; const writer = vafWriters[attributeLocations.textureCoordinateBoundsOrLab