@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
JavaScript
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