UNPKG

@cesium/engine

Version:

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

1,628 lines (1,475 loc) 78 kB
import BoundingSphere from "../Core/BoundingSphere.js"; import Cartesian3 from "../Core/Cartesian3.js"; import Color from "../Core/Color.js"; import ColorGeometryInstanceAttribute from "../Core/ColorGeometryInstanceAttribute.js"; import CullingVolume from "../Core/CullingVolume.js"; import defined from "../Core/defined.js"; import deprecationWarning from "../Core/deprecationWarning.js"; import destroyObject from "../Core/destroyObject.js"; import Ellipsoid from "../Core/Ellipsoid.js"; import Intersect from "../Core/Intersect.js"; import JulianDate from "../Core/JulianDate.js"; import CesiumMath from "../Core/Math.js"; import Matrix3 from "../Core/Matrix3.js"; import Matrix4 from "../Core/Matrix4.js"; import OrientedBoundingBox from "../Core/OrientedBoundingBox.js"; import OrthographicFrustum from "../Core/OrthographicFrustum.js"; import Rectangle from "../Core/Rectangle.js"; import Request from "../Core/Request.js"; import RequestScheduler from "../Core/RequestScheduler.js"; import RequestState from "../Core/RequestState.js"; import RequestType from "../Core/RequestType.js"; import Resource from "../Core/Resource.js"; import RuntimeError from "../Core/RuntimeError.js"; import Cesium3DContentGroup from "./Cesium3DContentGroup.js"; import Cesium3DTileContentFactory from "./Cesium3DTileContentFactory.js"; import Cesium3DTileContentState from "./Cesium3DTileContentState.js"; import Cesium3DTileContentType from "./Cesium3DTileContentType.js"; import Cesium3DTileOptimizationHint from "./Cesium3DTileOptimizationHint.js"; import Cesium3DTilePass from "./Cesium3DTilePass.js"; import Cesium3DTileRefine from "./Cesium3DTileRefine.js"; import Empty3DTileContent from "./Empty3DTileContent.js"; import findContentMetadata from "./findContentMetadata.js"; import findGroupMetadata from "./findGroupMetadata.js"; import findTileMetadata from "./findTileMetadata.js"; import hasExtension from "./hasExtension.js"; import Multiple3DTileContent from "./Multiple3DTileContent.js"; import BoundingVolumeSemantics from "./BoundingVolumeSemantics.js"; import preprocess3DTileContent from "./preprocess3DTileContent.js"; import SceneMode from "./SceneMode.js"; import TileBoundingRegion from "./TileBoundingRegion.js"; import TileBoundingS2Cell from "./TileBoundingS2Cell.js"; import TileBoundingSphere from "./TileBoundingSphere.js"; import TileOrientedBoundingBox from "./TileOrientedBoundingBox.js"; import Pass from "../Renderer/Pass.js"; import VerticalExaggeration from "../Core/VerticalExaggeration.js"; /** * A tile in a {@link Cesium3DTileset}. When a tile is first created, its content is not loaded; * the content is loaded on-demand when needed based on the view. * <p> * Do not construct this directly, instead access tiles through {@link Cesium3DTileset#tileVisible}. * </p> * * @alias Cesium3DTile * @constructor * @param {Cesium3DTileset} tileset The tileset * @param {Resource} baseResource The base resource for the tileset * @param {object} header The JSON header for the tile * @param {Cesium3DTile} parent The parent tile of the new tile */ function Cesium3DTile(tileset, baseResource, header, parent) { this._tileset = tileset; this._header = header; const hasContentsArray = defined(header.contents); const hasMultipleContents = (hasContentsArray && header.contents.length > 1) || hasExtension(header, "3DTILES_multiple_contents"); // In the 1.0 schema, content is stored in tile.content instead of tile.contents const contentHeader = hasContentsArray && !hasMultipleContents ? header.contents[0] : header.content; this._contentHeader = contentHeader; /** * The local transform of this tile. * @type {Matrix4} */ this.transform = defined(header.transform) ? Matrix4.unpack(header.transform) : Matrix4.clone(Matrix4.IDENTITY); const parentTransform = defined(parent) ? parent.computedTransform : tileset.modelMatrix; const computedTransform = Matrix4.multiply( parentTransform, this.transform, new Matrix4(), ); const parentInitialTransform = defined(parent) ? parent._initialTransform : Matrix4.IDENTITY; this._initialTransform = Matrix4.multiply( parentInitialTransform, this.transform, new Matrix4(), ); /** * The final computed transform of this tile. * @type {Matrix4} * @readonly */ this.computedTransform = computedTransform; /** * When tile metadata is present (3D Tiles 1.1) or the <code>3DTILES_metadata</code> extension is used, * this stores a {@link TileMetadata} object for accessing tile metadata. * * @type {TileMetadata} * @readonly * @private * @experimental This feature is using part of the 3D Tiles spec that is not final and is subject to change without Cesium's standard deprecation policy. */ this.metadata = findTileMetadata(tileset, header); this._verticalExaggeration = 1.0; this._verticalExaggerationRelativeHeight = 0.0; // Important: tile metadata must be parsed before this line so that the // metadata semantics TILE_BOUNDING_BOX, TILE_BOUNDING_REGION, or TILE_BOUNDING_SPHERE // can override header.boundingVolume (if necessary) this._boundingVolume = this.createBoundingVolume( header.boundingVolume, computedTransform, ); this._boundingVolume2D = undefined; let contentBoundingVolume; if (defined(contentHeader) && defined(contentHeader.boundingVolume)) { // Non-leaf tiles may have a content bounding-volume, which is a tight-fit bounding volume // around only the features in the tile. This box is useful for culling for rendering, // but not for culling for traversing the tree since it does not guarantee spatial coherence, i.e., // since it only bounds features in the tile, not the entire tile, children may be // outside of this box. contentBoundingVolume = this.createBoundingVolume( contentHeader.boundingVolume, computedTransform, ); } this._contentBoundingVolume = contentBoundingVolume; this._contentBoundingVolume2D = undefined; let viewerRequestVolume; if (defined(header.viewerRequestVolume)) { viewerRequestVolume = this.createBoundingVolume( header.viewerRequestVolume, computedTransform, ); } this._viewerRequestVolume = viewerRequestVolume; /** * The error, in meters, introduced if this tile is rendered and its children are not. * This is used to compute screen space error, i.e., the error measured in pixels. * * @type {number} * @readonly */ this.geometricError = header.geometricError; this._geometricError = header.geometricError; if (!defined(this._geometricError)) { this._geometricError = defined(parent) ? parent._geometricError : tileset._geometricError; Cesium3DTile._deprecationWarning( "geometricErrorUndefined", "Required property geometricError is undefined for this tile. Using parent's geometric error instead.", ); } this.updateGeometricErrorScale(); let refine; if (defined(header.refine)) { if (header.refine === "replace" || header.refine === "add") { Cesium3DTile._deprecationWarning( "lowercase-refine", `This tile uses a lowercase refine "${ header.refine }". Instead use "${header.refine.toUpperCase()}".`, ); } refine = header.refine.toUpperCase() === "REPLACE" ? Cesium3DTileRefine.REPLACE : Cesium3DTileRefine.ADD; } else if (defined(parent)) { // Inherit from parent tile if omitted. refine = parent.refine; } else { refine = Cesium3DTileRefine.REPLACE; } /** * Specifies the type of refinement that is used when traversing this tile for rendering. * * @type {Cesium3DTileRefine} * @readonly * @private */ this.refine = refine; /** * Gets the tile's children. * * @type {Cesium3DTile[]} * @readonly */ this.children = []; /** * This tile's parent or <code>undefined</code> if this tile is the root. * <p> * When a tile's content points to an external tileset JSON file, the external tileset's * root tile's parent is not <code>undefined</code>; instead, the parent references * the tile (with its content pointing to an external tileset JSON file) as if the two tilesets were merged. * </p> * * @type {Cesium3DTile} * @readonly */ this.parent = parent; let content; let hasEmptyContent = false; let contentState; let contentResource; let serverKey; baseResource = Resource.createIfNeeded(baseResource); if (hasMultipleContents) { contentState = Cesium3DTileContentState.UNLOADED; // Each content may have its own URI, but they all need to be resolved // relative to the tileset, so the base resource is used. contentResource = baseResource.clone(); } else if (defined(contentHeader)) { let contentHeaderUri = contentHeader.uri; if (defined(contentHeader.url)) { Cesium3DTile._deprecationWarning( "contentUrl", 'This tileset JSON uses the "content.url" property which has been deprecated. Use "content.uri" instead.', ); contentHeaderUri = contentHeader.url; } if (contentHeaderUri === "") { Cesium3DTile._deprecationWarning( "contentUriEmpty", "content.uri property is an empty string, which creates a circular dependency, making this tileset invalid. Omit the content property instead", ); content = new Empty3DTileContent(tileset, this); hasEmptyContent = true; contentState = Cesium3DTileContentState.READY; } else { contentState = Cesium3DTileContentState.UNLOADED; contentResource = baseResource.getDerivedResource({ url: contentHeaderUri, }); serverKey = RequestScheduler.getServerKey( contentResource.getUrlComponent(), ); } } else { content = new Empty3DTileContent(tileset, this); hasEmptyContent = true; contentState = Cesium3DTileContentState.READY; } this._content = content; this._contentResource = contentResource; this._contentState = contentState; this._expiredContent = undefined; this._serverKey = serverKey; /** * When <code>true</code>, the tile has no content. * * @type {boolean} * @readonly * * @private */ this.hasEmptyContent = hasEmptyContent; /** * When <code>true</code>, the tile's content points to an external tileset. * <p> * This is <code>false</code> until the tile's content is loaded. * </p> * * @type {boolean} * @readonly * * @private */ this.hasTilesetContent = false; /** * When <code>true</code>, the tile's content is an implicit tileset. * <p> * This is <code>false</code> until the tile's implicit content is loaded. * </p> * * @type {boolean} * @readonly * * @private * @experimental This feature is using part of the 3D Tiles spec that is not final and is subject to change without Cesium's standard deprecation policy. */ this.hasImplicitContent = false; /** * Determines whether the tile has renderable content. * * The loading starts with the assumption that the tile does have * renderable content, if the content is not empty.<br> * <br> * This turns <code>false</code> only when the tile content is loaded * and turns out to be a single content that points to an external * tileset or implicit content * </p> * * @type {boolean} * @readonly * * @private */ this.hasRenderableContent = !hasEmptyContent; /** * When <code>true</code>, the tile contains content metadata from implicit tiling. This flag is set * for tiles transcoded by <code>Implicit3DTileContent</code>. * <p> * This is <code>false</code> until the tile's content is loaded. * </p> * * @type {boolean} * @readonly * * @private * @experimental This feature is using part of the 3D Tiles spec that is not final and is subject to change without Cesium's standard deprecation policy. */ this.hasImplicitContentMetadata = false; /** * When <code>true</code>, the tile has multiple contents, either in the tile JSON (3D Tiles 1.1) * or via the <code>3DTILES_multiple_contents</code> extension. * * @see {@link https://github.com/CesiumGS/3d-tiles/tree/main/extensions/3DTILES_multiple_contents|3DTILES_multiple_contents extension} * * @type {boolean} * @readonly * * @private */ this.hasMultipleContents = hasMultipleContents; /** * The node in the tileset's LRU cache, used to determine when to unload a tile's content. * * See {@link Cesium3DTilesetCache} * * @type {DoublyLinkedListNode} * @readonly * * @private */ this.cacheNode = undefined; const expire = header.expire; let expireDuration; let expireDate; if (defined(expire)) { expireDuration = expire.duration; if (defined(expire.date)) { expireDate = JulianDate.fromIso8601(expire.date); } } /** * The time in seconds after the tile's content is ready when the content expires and new content is requested. * * @type {number} */ this.expireDuration = expireDuration; /** * The date when the content expires and new content is requested. * * @type {JulianDate} */ this.expireDate = expireDate; /** * The time when a style was last applied to this tile. * * @type {number} * * @private */ this.lastStyleTime = 0.0; /** * Marks whether the tile's children bounds are fully contained within the tile's bounds * * @type {Cesium3DTileOptimizationHint} * * @private */ this._optimChildrenWithinParent = Cesium3DTileOptimizationHint.NOT_COMPUTED; /** * Tracks if the tile's relationship with a ClippingPlaneCollection has changed with regards * to the ClippingPlaneCollection's state. * * @type {boolean} * * @private */ this.clippingPlanesDirty = false; /** * Tracks if the tile's relationship with a ClippingPolygonCollection has changed with regards * to the ClippingPolygonCollection's state. * * @type {boolean} * * @private */ this.clippingPolygonsDirty = false; /** * Tracks if the tile's request should be deferred until all non-deferred * tiles load. * * @type {boolean} * * @private */ this.priorityDeferred = false; /** * For implicit tiling, an ImplicitTileset object will be attached to a * placeholder tile with either implicit tiling in the JSON (3D Tiles 1.1) * or the <code>3DTILES_implicit_tiling</code> extension. * This way the {@link Implicit3DTileContent} can access the tile later once the content is fetched. * * @type {ImplicitTileset|undefined} * * @private * @experimental This feature is using part of the 3D Tiles spec that is not final and is subject to change without Cesium's standard deprecation policy. */ this.implicitTileset = undefined; /** * For implicit tiling, the (level, x, y, [z]) coordinates within the * implicit tileset are stored in the tile. * * @type {ImplicitTileCoordinates|undefined} * * @private * @experimental This feature is using part of the 3D Tiles spec that is not final and is subject to change without Cesium's standard deprecation policy. */ this.implicitCoordinates = undefined; /** * For implicit tiling, each transcoded tile will hold a weak reference to * the {@link ImplicitSubtree}. * * @type {ImplicitSubtree|undefined} * * @private * @experimental This feature is using part of the 3D Tiles spec that is not final and is subject to change without Cesium's standard deprecation policy. */ this.implicitSubtree = undefined; // Members that are updated every frame for tree traversal and rendering optimizations: this._distanceToCamera = 0.0; this._centerZDepth = 0.0; this._screenSpaceError = 0.0; this._screenSpaceErrorProgressiveResolution = 0.0; // The screen space error at a given screen height of tileset.progressiveResolutionHeightFraction * screenHeight this._visibilityPlaneMask = 0; this._visible = false; this._inRequestVolume = false; this._finalResolution = true; this._depth = 0; this._stackLength = 0; this._selectionDepth = 0; this._updatedVisibilityFrame = 0; this._touchedFrame = 0; this._visitedFrame = 0; this._selectedFrame = 0; this._wasSelectedLastFrame = false; this._requestedFrame = 0; this._ancestorWithContent = undefined; this._ancestorWithContentAvailable = undefined; this._refines = false; this._shouldSelect = false; this._isClipped = true; this._isClippedByPolygon = false; this._clippingPlanesState = 0; // encapsulates (_isClipped, clippingPlanes.enabled) and number/function this._clippingPolygonsState = 0; // encapsulates (_isClipped, clippingPolygons.enabled) and number/function this._debugBoundingVolume = undefined; this._debugContentBoundingVolume = undefined; this._debugViewerRequestVolume = undefined; this._debugColor = Color.fromRandom({ alpha: 1.0 }); this._debugColorizeTiles = false; this._priority = 0.0; // The priority used for request sorting this._priorityHolder = this; // Reference to the ancestor up the tree that holds the _foveatedFactor and _distanceToCamera for all tiles in the refinement chain. this._priorityProgressiveResolution = false; this._priorityProgressiveResolutionScreenSpaceErrorLeaf = false; this._priorityReverseScreenSpaceError = 0.0; this._foveatedFactor = 0.0; this._wasMinPriorityChild = false; // Needed for knowing when to continue a refinement chain. Gets reset in updateTile in traversal and gets set in updateAndPushChildren in traversal. this._loadTimestamp = new JulianDate(); this._commandsLength = 0; this._color = undefined; this._colorDirty = false; this._request = undefined; } // This can be overridden for testing purposes Cesium3DTile._deprecationWarning = deprecationWarning; Object.defineProperties(Cesium3DTile.prototype, { /** * The tileset containing this tile. * * @memberof Cesium3DTile.prototype * * @type {Cesium3DTileset} * @readonly */ tileset: { get: function () { return this._tileset; }, }, /** * The tile's content. This represents the actual tile's payload, * not the content's metadata in the tileset JSON file. * * @memberof Cesium3DTile.prototype * * @type {Cesium3DTileContent} * @readonly */ content: { get: function () { return this._content; }, }, /** * Get the tile's bounding volume. * * @memberof Cesium3DTile.prototype * * @type {TileBoundingVolume} * @readonly * @private */ boundingVolume: { get: function () { return this._boundingVolume; }, }, /** * Get the bounding volume of the tile's contents. This defaults to the * tile's bounding volume when the content's bounding volume is * <code>undefined</code>. * * @memberof Cesium3DTile.prototype * * @type {TileBoundingVolume} * @readonly * @private */ contentBoundingVolume: { get: function () { return this._contentBoundingVolume ?? this._boundingVolume; }, }, /** * Get the bounding sphere derived from the tile's bounding volume. * * @memberof Cesium3DTile.prototype * * @type {BoundingSphere} * @readonly */ boundingSphere: { get: function () { return this._boundingVolume.boundingSphere; }, }, /** * Determines if the tile is visible within the current field of view * * @memberof Cesium3DTile.prototype * * @type {boolean} * @readonly * * @private */ isVisible: { get: function () { return this._visible && this._inRequestVolume; }, }, /** * Returns the <code>extras</code> property in the tileset JSON for this tile, which contains application specific metadata. * Returns <code>undefined</code> if <code>extras</code> does not exist. * * @memberof Cesium3DTile.prototype * * @type {object} * @readonly * @see {@link https://github.com/CesiumGS/3d-tiles/tree/main/specification#specifying-extensions-and-application-specific-extras|Extras in the 3D Tiles specification.} */ extras: { get: function () { return this._header.extras; }, }, /** * Gets or sets the tile's highlight color. * * @memberof Cesium3DTile.prototype * * @type {Color} * * @default {@link Color.WHITE} * * @private */ color: { get: function () { if (!defined(this._color)) { this._color = new Color(); } return Color.clone(this._color); }, set: function (value) { this._color = Color.clone(value, this._color); this._colorDirty = true; }, }, /** * Determines if the tile has available content to render. <code>true</code> if the tile's * content is ready or if it has expired content that renders while new content loads; otherwise, * <code>false</code>. * * @memberof Cesium3DTile.prototype * * @type {boolean} * @readonly * * @private */ contentAvailable: { get: function () { return ( (this.contentReady && this.hasRenderableContent) || (defined(this._expiredContent) && !this.contentFailed) ); }, }, /** * Determines if the tile's content is ready. This is automatically <code>true</code> for * tile's with empty content. * * @memberof Cesium3DTile.prototype * * @type {boolean} * @readonly * * @private */ contentReady: { get: function () { return this._contentState === Cesium3DTileContentState.READY; }, }, /** * Determines if the tile's content has not be requested. <code>true</code> if tile's * content has not be requested; otherwise, <code>false</code>. * * @memberof Cesium3DTile.prototype * * @type {boolean} * @readonly * * @private */ contentUnloaded: { get: function () { return this._contentState === Cesium3DTileContentState.UNLOADED; }, }, /** * Determines if the tile has renderable content which is unloaded * * @memberof Cesium3DTile.prototype * * @type {boolean} * @readonly * * @private */ hasUnloadedRenderableContent: { get: function () { return this.hasRenderableContent && this.contentUnloaded; }, }, /** * Determines if the tile's content is expired. <code>true</code> if tile's * content is expired; otherwise, <code>false</code>. * * @memberof Cesium3DTile.prototype * * @type {boolean} * @readonly * * @private */ contentExpired: { get: function () { return this._contentState === Cesium3DTileContentState.EXPIRED; }, }, /** * Determines if the tile's content failed to load. <code>true</code> if the tile's * content failed to load; otherwise, <code>false</code>. * * @memberof Cesium3DTile.prototype * * @type {boolean} * @readonly * * @private */ contentFailed: { get: function () { return this._contentState === Cesium3DTileContentState.FAILED; }, }, /** * Returns the number of draw commands used by this tile. * * @readonly * * @private */ commandsLength: { get: function () { return this._commandsLength; }, }, }); const scratchCartesian = new Cartesian3(); /** * @private * @param {Cesium3DTile} tile * @param {FrameState} frameState * @returns {boolean} */ function isPriorityDeferred(tile, frameState) { const { tileset, boundingSphere } = tile; const { radius, center } = boundingSphere; const { camera } = frameState; // If closest point on line is inside the sphere then set foveatedFactor to 0. // Otherwise, the dot product is with the line from camera to the point on the sphere that is closest to the line. const scaledCameraDirection = Cartesian3.multiplyByScalar( camera.directionWC, tile._centerZDepth, scratchCartesian, ); const closestPointOnLine = Cartesian3.add( camera.positionWC, scaledCameraDirection, scratchCartesian, ); // The distance from the camera's view direction to the tile. const toLine = Cartesian3.subtract( closestPointOnLine, center, scratchCartesian, ); const distanceToCenterLine = Cartesian3.magnitude(toLine); const notTouchingSphere = distanceToCenterLine > radius; // If camera's direction vector is inside the bounding sphere then consider // this tile right along the line of sight and set _foveatedFactor to 0. // Otherwise,_foveatedFactor is one minus the dot product of the camera's direction // and the vector between the camera and the point on the bounding sphere closest to the view line. if (notTouchingSphere) { const toLineNormalized = Cartesian3.normalize(toLine, scratchCartesian); const scaledToLine = Cartesian3.multiplyByScalar( toLineNormalized, radius, scratchCartesian, ); const closestOnSphere = Cartesian3.add( center, scaledToLine, scratchCartesian, ); const toClosestOnSphere = Cartesian3.subtract( closestOnSphere, camera.positionWC, scratchCartesian, ); const toClosestOnSphereNormalize = Cartesian3.normalize( toClosestOnSphere, scratchCartesian, ); tile._foveatedFactor = 1.0 - Math.abs(Cartesian3.dot(camera.directionWC, toClosestOnSphereNormalize)); } else { tile._foveatedFactor = 0.0; } // Skip this feature if: non-skipLevelOfDetail and replace refine, if the foveated settings are turned off, if tile is progressive resolution and replace refine and skipLevelOfDetail (will help get rid of ancestor artifacts faster) // Or if the tile is a preload of any kind const replace = tile.refine === Cesium3DTileRefine.REPLACE; const skipLevelOfDetail = tileset.isSkippingLevelOfDetail; if ( (replace && !skipLevelOfDetail) || !tileset.foveatedScreenSpaceError || tileset.foveatedConeSize === 1.0 || (tile._priorityProgressiveResolution && replace && skipLevelOfDetail) || tileset._pass === Cesium3DTilePass.PRELOAD_FLIGHT || tileset._pass === Cesium3DTilePass.PRELOAD ) { return false; } const maximumFovatedFactor = 1.0 - Math.cos(camera.frustum.fov * 0.5); // 0.14 for fov = 60. NOTE very hard to defer vertically foveated tiles since max is based on fovy (which is fov). Lowering the 0.5 to a smaller fraction of the screen height will start to defer vertically foveated tiles. const foveatedConeFactor = tileset.foveatedConeSize * maximumFovatedFactor; // If it's inside the user-defined view cone, then it should not be deferred. if (tile._foveatedFactor <= foveatedConeFactor) { return false; } // Relax SSE based on how big the angle is between the tile and the edge of the foveated cone. const range = maximumFovatedFactor - foveatedConeFactor; const normalizedFoveatedFactor = CesiumMath.clamp( (tile._foveatedFactor - foveatedConeFactor) / range, 0.0, 1.0, ); const sseRelaxation = tileset.foveatedInterpolationCallback( tileset.foveatedMinimumScreenSpaceErrorRelaxation, tileset.memoryAdjustedScreenSpaceError, normalizedFoveatedFactor, ); const sse = tile._screenSpaceError === 0.0 && defined(tile.parent) ? tile.parent._screenSpaceError * 0.5 : tile._screenSpaceError; return tileset.memoryAdjustedScreenSpaceError - sseRelaxation <= sse; } const scratchJulianDate = new JulianDate(); /** * Get the tile's screen space error. * * @private * @param {FrameState} frameState * @param {boolean} useParentGeometricError * @param {number} progressiveResolutionHeightFraction */ Cesium3DTile.prototype.getScreenSpaceError = function ( frameState, useParentGeometricError, progressiveResolutionHeightFraction, ) { const tileset = this._tileset; const heightFraction = progressiveResolutionHeightFraction ?? 1.0; const parentGeometricError = defined(this.parent) ? this.parent.geometricError : tileset._scaledGeometricError; const geometricError = useParentGeometricError ? parentGeometricError : this.geometricError; if (geometricError === 0.0) { // Leaf tiles do not have any error so save the computation return 0.0; } const { camera, context } = frameState; let frustum = camera.frustum; const width = context.drawingBufferWidth; const height = context.drawingBufferHeight * heightFraction; let error; if ( frameState.mode === SceneMode.SCENE2D || frustum instanceof OrthographicFrustum ) { const offCenterFrustum = frustum.offCenterFrustum; if (defined(offCenterFrustum)) { frustum = offCenterFrustum; } const pixelSize = Math.max(frustum.top - frustum.bottom, frustum.right - frustum.left) / Math.max(width, height); error = geometricError / pixelSize; } else { // Avoid divide by zero when viewer is inside the tile const distance = Math.max(this._distanceToCamera, CesiumMath.EPSILON7); const sseDenominator = frustum.sseDenominator; error = (geometricError * height) / (distance * sseDenominator); if (tileset.dynamicScreenSpaceError) { const density = tileset._dynamicScreenSpaceErrorComputedDensity; const factor = tileset.dynamicScreenSpaceErrorFactor; const dynamicError = CesiumMath.fog(distance, density) * factor; error -= dynamicError; } } error /= frameState.pixelRatio; return error; }; /** * @private * @param {Cesium3DTileset} tileset * @param {Cesium3DTile} tile * @returns {boolean} */ function isPriorityProgressiveResolution(tileset, tile) { if ( tileset.progressiveResolutionHeightFraction <= 0.0 || tileset.progressiveResolutionHeightFraction > 0.5 ) { return false; } const maximumScreenSpaceError = tileset.memoryAdjustedScreenSpaceError; let isProgressiveResolutionTile = tile._screenSpaceErrorProgressiveResolution > maximumScreenSpaceError; // Mark non-SSE leaves tile._priorityProgressiveResolutionScreenSpaceErrorLeaf = false; // Needed for skipLOD const parent = tile.parent; const tilePasses = tile._screenSpaceErrorProgressiveResolution <= maximumScreenSpaceError; const parentFails = defined(parent) && parent._screenSpaceErrorProgressiveResolution > maximumScreenSpaceError; if (tilePasses && parentFails) { // A progressive resolution SSE leaf, promote its priority as well tile._priorityProgressiveResolutionScreenSpaceErrorLeaf = true; isProgressiveResolutionTile = true; } return isProgressiveResolutionTile; } /** * @private * @param {Cesium3DTileset} tileset * @param {Cesium3DTile} tile * @returns {number} */ function getPriorityReverseScreenSpaceError(tileset, tile) { const parent = tile.parent; const useParentScreenSpaceError = defined(parent) && (!tileset.isSkippingLevelOfDetail || tile._screenSpaceError === 0.0 || parent.hasTilesetContent || parent.hasImplicitContent); const screenSpaceError = useParentScreenSpaceError ? parent._screenSpaceError : tile._screenSpaceError; return tileset.root._screenSpaceError - screenSpaceError; } /** * Update the tile's visibility. * * @private * @param {FrameState} frameState */ Cesium3DTile.prototype.updateVisibility = function (frameState) { const { parent, tileset } = this; if (this._updatedVisibilityFrame === tileset._updatedVisibilityFrame) { // The tile has already been updated for this frame return; } const parentTransform = defined(parent) ? parent.computedTransform : tileset.modelMatrix; const parentVisibilityPlaneMask = defined(parent) ? parent._visibilityPlaneMask : CullingVolume.MASK_INDETERMINATE; this.updateTransform(parentTransform, frameState); this._distanceToCamera = this.distanceToTile(frameState); this._centerZDepth = this.distanceToTileCenter(frameState); this._screenSpaceError = this.getScreenSpaceError(frameState, false); this._screenSpaceErrorProgressiveResolution = this.getScreenSpaceError( frameState, false, tileset.progressiveResolutionHeightFraction, ); this._visibilityPlaneMask = this.visibility( frameState, parentVisibilityPlaneMask, ); // Use parent's plane mask to speed up visibility test this._visible = this._visibilityPlaneMask !== CullingVolume.MASK_OUTSIDE; this._inRequestVolume = this.insideViewerRequestVolume(frameState); this._priorityReverseScreenSpaceError = getPriorityReverseScreenSpaceError( tileset, this, ); this._priorityProgressiveResolution = isPriorityProgressiveResolution( tileset, this, ); this.priorityDeferred = isPriorityDeferred(this, frameState); this._updatedVisibilityFrame = tileset._updatedVisibilityFrame; }; /** * Update whether the tile has expired. * * @private */ Cesium3DTile.prototype.updateExpiration = function () { if ( defined(this.expireDate) && this.contentReady && !this.hasEmptyContent && !this.hasMultipleContents ) { const now = JulianDate.now(scratchJulianDate); if (JulianDate.lessThan(this.expireDate, now)) { this._contentState = Cesium3DTileContentState.EXPIRED; this._expiredContent = this._content; } } }; /** * @private * @param {Cesium3DTile} tile */ function updateExpireDate(tile) { if (!defined(tile.expireDuration)) { return; } const expireDurationDate = JulianDate.now(scratchJulianDate); JulianDate.addSeconds( expireDurationDate, tile.expireDuration, expireDurationDate, ); if (defined(tile.expireDate)) { if (JulianDate.lessThan(tile.expireDate, expireDurationDate)) { JulianDate.clone(expireDurationDate, tile.expireDate); } } else { tile.expireDate = JulianDate.clone(expireDurationDate); } } /** * @private * @param {Cesium3DTile} tile * @returns {Function} */ function createPriorityFunction(tile) { return function () { return tile._priority; }; } /** * Requests the tile's content. * <p> * The request may not be made if the Cesium Request Scheduler can't prioritize it. * </p> * * @return {Promise<Cesium3DTileContent>|undefined} A promise that resolves when the request completes, or undefined if there is no request needed, or the request cannot be scheduled. * @private */ Cesium3DTile.prototype.requestContent = function () { // empty contents don't require any HTTP requests if (this.hasEmptyContent) { return; } if (this.hasMultipleContents) { return requestMultipleContents(this); } return requestSingleContent(this); }; /** * Multiple {@link Cesium3DTileContent}s are allowed within a single tile either through * the tile JSON (3D Tiles 1.1) or the <code>3DTILES_multiple_contents</code> extension. * Due to differences in request scheduling, this is handled separately. * <p> * This implementation of multiple contents does not * support tile expiry like requestSingleContent does. If this changes, * note that the resource.setQueryParameters() details must go inside {@link Multiple3DTileContent} since that is per-request. * </p> * * @private * @param {Cesium3DTile} tile * @returns {Promise<Cesium3DTileContent>|Promise<undefined>|undefined} A promise that resolves to the tile content once loaded, or a promise that resolves to undefined if the request was cancelled mid-flight, or undefined if the request cannot be scheduled this frame */ function requestMultipleContents(tile) { let multipleContents = tile._content; const tileset = tile._tileset; if (!defined(multipleContents)) { // Create the content object immediately, it will handle scheduling // requests for inner contents. const contentsJson = hasExtension(tile._header, "3DTILES_multiple_contents") ? tile._header.extensions["3DTILES_multiple_contents"] : tile._header; multipleContents = new Multiple3DTileContent( tileset, tile, tile._contentResource.clone(), contentsJson, ); tile._content = multipleContents; } const promise = multipleContents.requestInnerContents(); if (!defined(promise)) { // Request could not all be scheduled this frame return; } tile._contentState = Cesium3DTileContentState.LOADING; return promise .then((content) => { if (tile.isDestroyed()) { // Tile is unloaded before the content can process return; } // Tile was canceled, try again later if (!defined(content)) { return; } tile._contentState = Cesium3DTileContentState.PROCESSING; return multipleContents; }) .catch((error) => { if (tile.isDestroyed()) { // Tile is unloaded before the content can process return; } tile._contentState = Cesium3DTileContentState.FAILED; throw error; }); } async function processArrayBuffer( tile, tileset, request, expired, requestPromise, ) { const previousState = tile._contentState; tile._contentState = Cesium3DTileContentState.LOADING; ++tileset.statistics.numberOfPendingRequests; let arrayBuffer; try { arrayBuffer = await requestPromise; } catch (error) { --tileset.statistics.numberOfPendingRequests; if (tile.isDestroyed()) { // Tile is unloaded before the content can process return; } if (request.cancelled || request.state === RequestState.CANCELLED) { // Cancelled due to low priority - try again later. tile._contentState = previousState; ++tileset.statistics.numberOfAttemptedRequests; return; } tile._contentState = Cesium3DTileContentState.FAILED; throw error; } if (tile.isDestroyed()) { --tileset.statistics.numberOfPendingRequests; // Tile is unloaded before the content can process return; } if (request.cancelled || request.state === RequestState.CANCELLED) { // Cancelled due to low priority - try again later. tile._contentState = previousState; --tileset.statistics.numberOfPendingRequests; ++tileset.statistics.numberOfAttemptedRequests; return; } try { const content = await makeContent(tile, arrayBuffer); --tileset.statistics.numberOfPendingRequests; if (tile.isDestroyed()) { // Tile is unloaded before the content can process return; } if (expired) { tile.expireDate = undefined; } tile._content = content; tile._contentState = Cesium3DTileContentState.PROCESSING; return content; } catch (error) { --tileset.statistics.numberOfPendingRequests; if (tile.isDestroyed()) { // Tile is unloaded before the content can process return; } tile._contentState = Cesium3DTileContentState.FAILED; throw error; } } /** * @private * @param {Cesium3DTile} tile * @returns {Promise<Cesium3DTileContent>|Promise<undefined>|undefined} A promise that resolves to the tile content once loaded; a promise that resolves to undefined if the tile was destroyed before processing can happen or the request was cancelled mid-flight; or undefined if the request cannot be scheduled this frame. */ function requestSingleContent(tile) { // it is important to clone here. The fetchArrayBuffer() below uses // throttling, but other uses of the resources do not. const resource = tile._contentResource.clone(); const expired = tile.contentExpired; if (expired) { // Append a query parameter of the tile expiration date to prevent caching resource.setQueryParameters({ expired: tile.expireDate.toString(), }); } const request = new Request({ throttle: true, throttleByServer: true, type: RequestType.TILES3D, priorityFunction: createPriorityFunction(tile), serverKey: tile._serverKey, }); tile._request = request; resource.request = request; const tileset = tile._tileset; const promise = resource.fetchArrayBuffer(); if (!defined(promise)) { ++tileset.statistics.numberOfAttemptedRequests; return; } return processArrayBuffer(tile, tileset, request, expired, promise); } /** * Given a downloaded content payload, construct a {@link Cesium3DTileContent}. * <p> * This is only used for single contents. * </p> * * @param {Cesium3DTile} tile The tile * @param {ArrayBuffer} arrayBuffer The downloaded payload containing data for the content * @return {Promise<Cesium3DTileContent>} A content object * @private */ async function makeContent(tile, arrayBuffer) { const preprocessed = preprocess3DTileContent(arrayBuffer); // Vector and Geometry tile rendering do not support the skip LOD optimization. const tileset = tile._tileset; tileset._disableSkipLevelOfDetail = tileset._disableSkipLevelOfDetail || preprocessed.contentType === Cesium3DTileContentType.GEOMETRY || preprocessed.contentType === Cesium3DTileContentType.VECTOR; if ( preprocessed.contentType === Cesium3DTileContentType.IMPLICIT_SUBTREE || preprocessed.contentType === Cesium3DTileContentType.IMPLICIT_SUBTREE_JSON ) { tile.hasImplicitContent = true; tile.hasRenderableContent = false; } if (preprocessed.contentType === Cesium3DTileContentType.EXTERNAL_TILESET) { tile.hasTilesetContent = true; tile.hasRenderableContent = false; } let content; const contentFactory = Cesium3DTileContentFactory[preprocessed.contentType]; if (tile.isDestroyed()) { return; } if (defined(preprocessed.binaryPayload)) { content = await Promise.resolve( contentFactory( tileset, tile, tile._contentResource, preprocessed.binaryPayload.buffer, 0, ), ); } else { // JSON formats content = await Promise.resolve( contentFactory( tileset, tile, tile._contentResource, preprocessed.jsonPayload, ), ); } const contentHeader = tile._contentHeader; if (tile.hasImplicitContentMetadata) { const subtree = tile.implicitSubtree; const coordinates = tile.implicitCoordinates; content.metadata = subtree.getContentMetadataView(coordinates, 0); } else if (!tile.hasImplicitContent) { content.metadata = findContentMetadata(tileset, contentHeader); } const groupMetadata = findGroupMetadata(tileset, contentHeader); if (defined(groupMetadata)) { content.group = new Cesium3DContentGroup({ metadata: groupMetadata, }); } return content; } /** * Cancel requests for the tile's contents. This is called when the tile * goes out of view. * * @private */ Cesium3DTile.prototype.cancelRequests = function () { if (this.hasMultipleContents) { this._content.cancelRequests(); } else { this._request.cancel(); } }; /** * Unloads the tile's content. * * @private */ Cesium3DTile.prototype.unloadContent = function () { if (!this.hasRenderableContent) { return; } this._content = this._content && this._content.destroy(); this._contentState = Cesium3DTileContentState.UNLOADED; this.lastStyleTime = 0.0; this.clippingPlanesDirty = this._clippingPlanesState === 0; this._clippingPlanesState = 0; this.clippingPolygonsDirty = this._clippingPolygonsState === 0; this._clippingPolygonsState = 0; this._debugColorizeTiles = false; this._debugBoundingVolume = this._debugBoundingVolume && this._debugBoundingVolume.destroy(); this._debugContentBoundingVolume = this._debugContentBoundingVolume && this._debugContentBoundingVolume.destroy(); this._debugViewerRequestVolume = this._debugViewerRequestVolume && this._debugViewerRequestVolume.destroy(); }; const scratchProjectedBoundingSphere = new BoundingSphere(); /** * @private * @param {Cesium3DTile} tile * @param {FrameState} frameState * @returns {TileBoundingVolume} */ function getBoundingVolume(tile, frameState) { if ( frameState.mode !== SceneMode.SCENE3D && !defined(tile._boundingVolume2D) ) { const boundingSphere = tile._boundingVolume.boundingSphere; const sphere = BoundingSphere.projectTo2D( boundingSphere, frameState.mapProjection, scratchProjectedBoundingSphere, ); tile._boundingVolume2D = new TileBoundingSphere( sphere.center, sphere.radius, ); } return frameState.mode !== SceneMode.SCENE3D ? tile._boundingVolume2D : tile._boundingVolume; } /** * @private * @param {Cesium3DTile} tile * @param {FrameState} frameState * @returns {TileBoundingVolume} */ function getContentBoundingVolume(tile, frameState) { if ( frameState.mode !== SceneMode.SCENE3D && !defined(tile._contentBoundingVolume2D) ) { const boundingSphere = tile._contentBoundingVolume.boundingSphere; const sphere = BoundingSphere.projectTo2D( boundingSphere, frameState.mapProjection, scratchProjectedBoundingSphere, ); tile._contentBoundingVolume2D = new TileBoundingSphere( sphere.center, sphere.radius, ); } return frameState.mode !== SceneMode.SCENE3D ? tile._contentBoundingVolume2D : tile._contentBoundingVolume; } /** * Determines whether the tile's bounding volume intersects the culling volume. * * @param {FrameState} frameState The frame state. * @param {number} parentVisibilityPlaneMask The parent's plane mask to speed up the visibility check. * @returns {number} A plane mask as described above in {@link CullingVolume#computeVisibilityWithPlaneMask}. * * @private */ Cesium3DTile.prototype.visibility = function ( frameState, parentVisibilityPlaneMask, ) { const cullingVolume = frameState.cullingVolume; const boundingVolume = getBoundingVolume(this, frameState); const tileset = this._tileset; const clippingPlanes = tileset.clippingPlanes; if (defined(clippingPlanes) && clippingPlanes.enabled) { const intersection = clippingPlanes.computeIntersectionWithBoundingVolume( boundingVolume, tileset.clippingPlanesOriginMatrix, ); this._isClipped = intersection !== Intersect.INSIDE; if (intersection === Intersect.OUTSIDE) { return CullingVolume.MASK_OUTSIDE; } } const clippingPolygons = tileset.clippingPolygons; if (defined(clippingPolygons) && clippingPolygons.enabled) { const intersection = clippingPolygons.computeIntersectionWithBoundingVolume(boundingVolume); this._isClippedByPolygon = intersection !== Intersect.OUTSIDE; // Polygon clipping intersections are determined by outer rectangles, therefore we cannot // preemptively determine if a tile is completely clipped or not here. } return cullingVolume.computeVisibilityWithPlaneMask( boundingVolume, parentVisibilityPlaneMask, ); }; /** * Assuming the tile's bounding volume intersects the culling volume, determines * whether the tile's content's bounding volume intersects the culling volume. * * @param {FrameState} frameState The frame state. * @returns {Intersect} The result of the intersection: the tile's content is completely outside, completely inside, or intersecting the culling volume. * * @private */ Cesium3DTile.prototype.contentVisibility = function (frameState) { // Assumes the tile's bounding volume intersects the culling volume already, so // just return Intersect.INSIDE if there is no content bounding volume. if (!defined(this._contentBoundingVolume)) { return Intersect.INSIDE; } if (this._visibilityPlaneMask === CullingVolume.MASK_INSIDE) { // The tile's bounding volume is completely inside the culling volume so // the content bounding volume must also be inside. return Intersect.INSIDE; } // PERFORMANCE_IDEA: is it possible to burn less CPU on this test since we know the // tile's (not the content's) bounding volume intersects the culling volume? const cullingVolume = frameState.cullingVolume; const boundingVolume = getContentBoundingVolume(this, frameState); const tileset = this._tileset; const clippingPlanes = tileset.clippingPlanes; if (defined(clippingPlanes) && clippingPlanes.enabled) { const intersection = clippingPlanes.computeIntersectionWithBoundingVolume( boundingVolume, tileset.clippingPlanesOriginMatrix, ); this._isClipped = intersection !== Intersect.INSIDE; if (intersection === Intersect.OUTSIDE) { return Intersect.OUTSIDE; } } const clippingPolygons = tileset.clippingPolygons; if (defined(clippingPolygons) && clippingPolygons.enabled) { const intersection = clippingPolygons.computeIntersectionWithBoundingVolume(boundingVolume); this._isClippedByPolygon = intersection !== Intersect.OUTSIDE; if (intersection === Intersect.INSIDE) { return Intersect.OUTSIDE; } } return cullingVolume.computeVisibility(boundingVolume); }; /** * Computes the (potentially approximate) distance from the closest point of the tile's bounding volume to the camera. * * @param {FrameState} frameState The frame state. * @returns {number} The distance, in meters, or zero if the camera is inside the bounding volume. * * @private */ Cesium3DTile.prototype.distanceToTile = function (frameState) { const boundingVolume = getBoundingVolume(this, frameState); return boundingVolume.distanceToCamera(frameState); }; const scratchToTileCenter = new Cartesian3(); /** * Computes the distance from the center of the tile's bounding volume to the camera's plane defined by its position and view direction. * * @param {FrameState} frameState The frame state. * @returns {number} The distance, in meters. * * @private */ Cesium3DTile.prototype.distanceToTileCenter = function (frameState) { const tileBoundingVolume = getBoundingVolume(this, frameState); const boundingVolume = tileBoundingVolume.boundingVolume; // Gets the underlying OrientedBoundingBox or BoundingSphere const toCenter = Cartesian3.subtract( boundingVolume.center, frameState.camera.posi