2455 lines
75 KiB
JavaScript
2455 lines
75 KiB
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 defaultValue from "../Core/defaultValue.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;
|
|
|
|
/**
|
|
* 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 defaultValue(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's content is renderable. <code>false</code> if the
|
|
* tile has empty content or if it points to an external tileset or implicit content
|
|
*
|
|
* @memberof Cesium3DTile.prototype
|
|
*
|
|
* @type {boolean}
|
|
* @readonly
|
|
*
|
|
* @private
|
|
*/
|
|
hasRenderableContent: {
|
|
get: function () {
|
|
return (
|
|
!this.hasEmptyContent &&
|
|
!this.hasTilesetContent &&
|
|
!this.hasImplicitContent
|
|
);
|
|
},
|
|
},
|
|
|
|
/**
|
|
* 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 = defaultValue(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;
|
|
}
|
|
|
|
if (preprocessed.contentType === Cesium3DTileContentType.EXTERNAL_TILESET) {
|
|
tile.hasTilesetContent = true;
|
|
}
|
|
|
|
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.positionWC,
|
|
scratchToTileCenter
|
|
);
|
|
return Cartesian3.dot(frameState.camera.directionWC, toCenter);
|
|
};
|
|
|
|
/**
|
|
* Checks if the camera is inside the viewer request volume.
|
|
*
|
|
* @param {FrameState} frameState The frame state.
|
|
* @returns {boolean} Whether the camera is inside the volume.
|
|
*
|
|
* @private
|
|
*/
|
|
Cesium3DTile.prototype.insideViewerRequestVolume = function (frameState) {
|
|
const viewerRequestVolume = this._viewerRequestVolume;
|
|
return (
|
|
!defined(viewerRequestVolume) ||
|
|
viewerRequestVolume.distanceToCamera(frameState) === 0.0
|
|
);
|
|
};
|
|
|
|
const scratchMatrix = new Matrix3();
|
|
const scratchScale = new Cartesian3();
|
|
const scratchHalfAxes = new Matrix3();
|
|
const scratchCenter = new Cartesian3();
|
|
const scratchRectangle = new Rectangle();
|
|
const scratchOrientedBoundingBox = new OrientedBoundingBox();
|
|
const scratchTransform = new Matrix4();
|
|
|
|
/**
|
|
* @private
|
|
* @param {Array} box An array of 12 numbers that define an oriented bounding box
|
|
* @param {Matrix4} transform
|
|
* @param {TileBoundingVolume} [result]
|
|
* @returns {TileOrientedBoundingBox}
|
|
*/
|
|
function createBox(box, transform, result) {
|
|
let center = Cartesian3.fromElements(box[0], box[1], box[2], scratchCenter);
|
|
let halfAxes = Matrix3.fromArray(box, 3, scratchHalfAxes);
|
|
|
|
// Find the transformed center and halfAxes
|
|
center = Matrix4.multiplyByPoint(transform, center, center);
|
|
const rotationScale = Matrix4.getMatrix3(transform, scratchMatrix);
|
|
halfAxes = Matrix3.multiply(rotationScale, halfAxes, halfAxes);
|
|
|
|
if (defined(result)) {
|
|
result.update(center, halfAxes);
|
|
return result;
|
|
}
|
|
return new TileOrientedBoundingBox(center, halfAxes);
|
|
}
|
|
|
|
/**
|
|
* @private
|
|
* @param {Array} region An array of six numbers that define a bounding geographic region in EPSG:4979 coordinates with the order [west, south, east, north, minimum height, maximum height]
|
|
* @param {Matrix4} transform
|
|
* @param {Matrix4} initialTransform
|
|
* @param {TileOrientedBoundingBox} [result]
|
|
* @returns {TileOrientedBoundingBox}
|
|
*/
|
|
function createBoxFromTransformedRegion(
|
|
region,
|
|
transform,
|
|
initialTransform,
|
|
result
|
|
) {
|
|
const rectangle = Rectangle.unpack(region, 0, scratchRectangle);
|
|
const minimumHeight = region[4];
|
|
const maximumHeight = region[5];
|
|
|
|
const orientedBoundingBox = OrientedBoundingBox.fromRectangle(
|
|
rectangle,
|
|
minimumHeight,
|
|
maximumHeight,
|
|
Ellipsoid.WGS84,
|
|
scratchOrientedBoundingBox
|
|
);
|
|
let center = orientedBoundingBox.center;
|
|
let halfAxes = orientedBoundingBox.halfAxes;
|
|
|
|
// A region bounding volume is not transformed by the transform in the tileset JSON,
|
|
// but may be transformed by additional transforms applied in Cesium.
|
|
// This is why the transform is calculated as the difference between the initial transform and the current transform.
|
|
transform = Matrix4.multiplyTransformation(
|
|
transform,
|
|
Matrix4.inverseTransformation(initialTransform, scratchTransform),
|
|
scratchTransform
|
|
);
|
|
center = Matrix4.multiplyByPoint(transform, center, center);
|
|
const rotationScale = Matrix4.getMatrix3(transform, scratchMatrix);
|
|
halfAxes = Matrix3.multiply(rotationScale, halfAxes, halfAxes);
|
|
|
|
if (defined(result) && result instanceof TileOrientedBoundingBox) {
|
|
result.update(center, halfAxes);
|
|
return result;
|
|
}
|
|
|
|
return new TileOrientedBoundingBox(center, halfAxes);
|
|
}
|
|
|
|
/**
|
|
* @private
|
|
* @param {Array} region An array of six numbers that define a bounding geographic region in EPSG:4979 coordinates with the order [west, south, east, north, minimum height, maximum height]
|
|
* @param {Matrix4} transform
|
|
* @param {Matrix4} initialTransform
|
|
* @param {TileBoundingVolume} [result]
|
|
* @returns {TileBoundingVolume}
|
|
*/
|
|
function createRegion(region, transform, initialTransform, result) {
|
|
if (
|
|
!Matrix4.equalsEpsilon(transform, initialTransform, CesiumMath.EPSILON8)
|
|
) {
|
|
return createBoxFromTransformedRegion(
|
|
region,
|
|
transform,
|
|
initialTransform,
|
|
result
|
|
);
|
|
}
|
|
|
|
const rectangleRegion = Rectangle.unpack(region, 0, scratchRectangle);
|
|
|
|
if (defined(result)) {
|
|
result.rectangle = Rectangle.clone(rectangleRegion, result.rectangle);
|
|
result.minimumHeight = region[4];
|
|
result.maximumHeight = region[5];
|
|
// The TileBoundingRegion was already constructed with the default
|
|
// WGS84 ellipsoid, so keep it consistent when updating.
|
|
result.computeBoundingVolumes(Ellipsoid.WGS84);
|
|
return result;
|
|
}
|
|
|
|
return new TileBoundingRegion({
|
|
rectangle: rectangleRegion,
|
|
minimumHeight: region[4],
|
|
maximumHeight: region[5],
|
|
});
|
|
}
|
|
|
|
/**
|
|
* @private
|
|
* @param {Array} sphere An array of four numbers that define a bounding sphere
|
|
* @param {Matrix4} transform
|
|
* @param {TileBoundingVolume} [result]
|
|
* @returns {TileBoundingSphere}
|
|
*/
|
|
function createSphere(sphere, transform, result) {
|
|
let center = Cartesian3.fromElements(
|
|
sphere[0],
|
|
sphere[1],
|
|
sphere[2],
|
|
scratchCenter
|
|
);
|
|
let radius = sphere[3];
|
|
|
|
// Find the transformed center and radius
|
|
center = Matrix4.multiplyByPoint(transform, center, center);
|
|
const scale = Matrix4.getScale(transform, scratchScale);
|
|
const uniformScale = Cartesian3.maximumComponent(scale);
|
|
radius *= uniformScale;
|
|
|
|
if (defined(result)) {
|
|
result.update(center, radius);
|
|
return result;
|
|
}
|
|
return new TileBoundingSphere(center, radius);
|
|
}
|
|
|
|
/**
|
|
* Create a bounding volume from the tile's bounding volume header.
|
|
*
|
|
* @param {object} boundingVolumeHeader The tile's bounding volume header.
|
|
* @param {Matrix4} transform The transform to apply to the bounding volume.
|
|
* @param {TileBoundingVolume} [result] The object onto which to store the result.
|
|
*
|
|
* @returns {TileBoundingVolume} The modified result parameter or a new TileBoundingVolume instance if none was provided.
|
|
*
|
|
* @private
|
|
*/
|
|
Cesium3DTile.prototype.createBoundingVolume = function (
|
|
boundingVolumeHeader,
|
|
transform,
|
|
result
|
|
) {
|
|
// if explicit tile metadata includes TILE_BOUNDING_BOX, TILE_BOUNDING_REGION,
|
|
// or TILE_BOUNDING_SPHERE, override tile.boundingVolume.
|
|
const tileMetadata = this.metadata;
|
|
let metadataBoundingVolumeHeader;
|
|
if (defined(tileMetadata)) {
|
|
metadataBoundingVolumeHeader = BoundingVolumeSemantics.parseBoundingVolumeSemantic(
|
|
"TILE",
|
|
tileMetadata
|
|
);
|
|
}
|
|
if (defined(metadataBoundingVolumeHeader)) {
|
|
boundingVolumeHeader = metadataBoundingVolumeHeader;
|
|
}
|
|
|
|
if (!defined(boundingVolumeHeader)) {
|
|
throw new RuntimeError("boundingVolume must be defined");
|
|
}
|
|
|
|
if (hasExtension(boundingVolumeHeader, "3DTILES_bounding_volume_S2")) {
|
|
return new TileBoundingS2Cell(
|
|
boundingVolumeHeader.extensions["3DTILES_bounding_volume_S2"]
|
|
);
|
|
}
|
|
|
|
const { box, region, sphere } = boundingVolumeHeader;
|
|
if (defined(box)) {
|
|
const tileOrientedBoundingBox = createBox(box, transform, result);
|
|
if (this._verticalExaggeration !== 1.0) {
|
|
exaggerateBoundingBox(
|
|
tileOrientedBoundingBox,
|
|
this._verticalExaggeration,
|
|
this._verticalExaggerationRelativeHeight
|
|
);
|
|
}
|
|
return tileOrientedBoundingBox;
|
|
}
|
|
if (defined(region)) {
|
|
const tileBoundingVolume = createRegion(
|
|
region,
|
|
transform,
|
|
this._initialTransform,
|
|
result
|
|
);
|
|
if (this._verticalExaggeration === 1.0) {
|
|
return tileBoundingVolume;
|
|
}
|
|
if (tileBoundingVolume instanceof TileOrientedBoundingBox) {
|
|
exaggerateBoundingBox(
|
|
tileBoundingVolume,
|
|
this._verticalExaggeration,
|
|
this._verticalExaggerationRelativeHeight
|
|
);
|
|
} else {
|
|
tileBoundingVolume.minimumHeight = VerticalExaggeration.getHeight(
|
|
tileBoundingVolume.minimumHeight,
|
|
this._verticalExaggeration,
|
|
this._verticalExaggerationRelativeHeight
|
|
);
|
|
tileBoundingVolume.maximumHeight = VerticalExaggeration.getHeight(
|
|
tileBoundingVolume.maximumHeight,
|
|
this._verticalExaggeration,
|
|
this._verticalExaggerationRelativeHeight
|
|
);
|
|
tileBoundingVolume.computeBoundingVolumes(Ellipsoid.WGS84);
|
|
}
|
|
return tileBoundingVolume;
|
|
}
|
|
if (defined(sphere)) {
|
|
const tileBoundingSphere = createSphere(sphere, transform, result);
|
|
if (this._verticalExaggeration !== 1.0) {
|
|
const exaggeratedCenter = VerticalExaggeration.getPosition(
|
|
tileBoundingSphere.center,
|
|
Ellipsoid.WGS84,
|
|
this._verticalExaggeration,
|
|
this._verticalExaggerationRelativeHeight,
|
|
scratchCenter
|
|
);
|
|
const exaggeratedRadius =
|
|
tileBoundingSphere.radius * this._verticalExaggeration;
|
|
tileBoundingSphere.update(exaggeratedCenter, exaggeratedRadius);
|
|
}
|
|
return tileBoundingSphere;
|
|
}
|
|
throw new RuntimeError(
|
|
"boundingVolume must contain a sphere, region, or box"
|
|
);
|
|
};
|
|
|
|
const scratchExaggeratedCorners = Cartesian3.unpackArray(
|
|
new Array(8 * 3).fill(0)
|
|
);
|
|
|
|
/**
|
|
* Exaggerates the bounding box of a tile based on the provided exaggeration factors.
|
|
*
|
|
* @private
|
|
* @param {TileOrientedBoundingBox} tileOrientedBoundingBox - The oriented bounding box of the tile.
|
|
* @param {number} exaggeration - The exaggeration factor to apply to the tile's bounding box.
|
|
* @param {number} exaggerationRelativeHeight - The height relative to which exaggeration will be applied.
|
|
*/
|
|
function exaggerateBoundingBox(
|
|
tileOrientedBoundingBox,
|
|
exaggeration,
|
|
exaggerationRelativeHeight
|
|
) {
|
|
const exaggeratedCorners = tileOrientedBoundingBox.boundingVolume
|
|
.computeCorners(scratchExaggeratedCorners)
|
|
.map((corner) =>
|
|
VerticalExaggeration.getPosition(
|
|
corner,
|
|
Ellipsoid.WGS84,
|
|
exaggeration,
|
|
exaggerationRelativeHeight,
|
|
corner
|
|
)
|
|
);
|
|
const exaggeratedBox = OrientedBoundingBox.fromPoints(
|
|
exaggeratedCorners,
|
|
scratchOrientedBoundingBox
|
|
);
|
|
tileOrientedBoundingBox.update(
|
|
exaggeratedBox.center,
|
|
exaggeratedBox.halfAxes
|
|
);
|
|
}
|
|
|
|
/**
|
|
* Update the tile's transform. The transform is applied to the tile's bounding volumes.
|
|
*
|
|
* @private
|
|
* @param {Matrix4} parentTransform
|
|
* @param {FrameState} [frameState]
|
|
*/
|
|
Cesium3DTile.prototype.updateTransform = function (
|
|
parentTransform,
|
|
frameState
|
|
) {
|
|
parentTransform = defaultValue(parentTransform, Matrix4.IDENTITY);
|
|
const computedTransform = Matrix4.multiplyTransformation(
|
|
parentTransform,
|
|
this.transform,
|
|
scratchTransform
|
|
);
|
|
const transformChanged = !Matrix4.equals(
|
|
computedTransform,
|
|
this.computedTransform
|
|
);
|
|
const exaggerationChanged =
|
|
defined(frameState) &&
|
|
(this._verticalExaggeration !== frameState.verticalExaggeration ||
|
|
this._verticalExaggerationRelativeHeight !==
|
|
frameState.verticalExaggerationRelativeHeight);
|
|
|
|
if (!transformChanged && !exaggerationChanged) {
|
|
return;
|
|
}
|
|
if (transformChanged) {
|
|
Matrix4.clone(computedTransform, this.computedTransform);
|
|
}
|
|
if (exaggerationChanged) {
|
|
this._verticalExaggeration = frameState.verticalExaggeration;
|
|
this._verticalExaggerationRelativeHeight =
|
|
frameState.verticalExaggerationRelativeHeight;
|
|
}
|
|
|
|
// Update the bounding volumes
|
|
const header = this._header;
|
|
const contentHeader = this._contentHeader;
|
|
this._boundingVolume = this.createBoundingVolume(
|
|
header.boundingVolume,
|
|
this.computedTransform,
|
|
this._boundingVolume
|
|
);
|
|
if (defined(this._contentBoundingVolume)) {
|
|
this._contentBoundingVolume = this.createBoundingVolume(
|
|
contentHeader.boundingVolume,
|
|
this.computedTransform,
|
|
this._contentBoundingVolume
|
|
);
|
|
}
|
|
if (defined(this._viewerRequestVolume)) {
|
|
this._viewerRequestVolume = this.createBoundingVolume(
|
|
header.viewerRequestVolume,
|
|
this.computedTransform,
|
|
this._viewerRequestVolume
|
|
);
|
|
}
|
|
|
|
this.updateGeometricErrorScale();
|
|
|
|
// Destroy the debug bounding volumes. They will be generated fresh.
|
|
this._debugBoundingVolume =
|
|
this._debugBoundingVolume && this._debugBoundingVolume.destroy();
|
|
this._debugContentBoundingVolume =
|
|
this._debugContentBoundingVolume &&
|
|
this._debugContentBoundingVolume.destroy();
|
|
this._debugViewerRequestVolume =
|
|
this._debugViewerRequestVolume && this._debugViewerRequestVolume.destroy();
|
|
};
|
|
|
|
Cesium3DTile.prototype.updateGeometricErrorScale = function () {
|
|
const scale = Matrix4.getScale(this.computedTransform, scratchScale);
|
|
const uniformScale = Cartesian3.maximumComponent(scale);
|
|
this.geometricError = this._geometricError * uniformScale;
|
|
|
|
if (!defined(this.parent)) {
|
|
// Update the tileset's geometric error
|
|
const tileset = this._tileset;
|
|
tileset._scaledGeometricError = tileset._geometricError * uniformScale;
|
|
}
|
|
};
|
|
|
|
/**
|
|
* @private
|
|
* @param {Cesium3DTile} tile
|
|
* @param {Cesium3DTileset} tileset
|
|
* @param {FrameState} frameState
|
|
* @param {object} passOptions
|
|
*/
|
|
function applyDebugSettings(tile, tileset, frameState, passOptions) {
|
|
if (!passOptions.isRender) {
|
|
return;
|
|
}
|
|
|
|
const hasContentBoundingVolume =
|
|
defined(tile._contentHeader) && defined(tile._contentHeader.boundingVolume);
|
|
|
|
const showVolume =
|
|
tileset.debugShowBoundingVolume ||
|
|
(tileset.debugShowContentBoundingVolume && !hasContentBoundingVolume);
|
|
if (showVolume) {
|
|
let color;
|
|
if (!tile._finalResolution) {
|
|
color = Color.YELLOW;
|
|
} else if (!tile.hasRenderableContent) {
|
|
color = Color.DARKGRAY;
|
|
} else {
|
|
color = Color.WHITE;
|
|
}
|
|
if (!defined(tile._debugBoundingVolume)) {
|
|
tile._debugBoundingVolume = tile._boundingVolume.createDebugVolume(color);
|
|
}
|
|
tile._debugBoundingVolume.update(frameState);
|
|
const attributes = tile._debugBoundingVolume.getGeometryInstanceAttributes(
|
|
"outline"
|
|
);
|
|
attributes.color = ColorGeometryInstanceAttribute.toValue(
|
|
color,
|
|
attributes.color
|
|
);
|
|
} else if (!showVolume && defined(tile._debugBoundingVolume)) {
|
|
tile._debugBoundingVolume = tile._debugBoundingVolume.destroy();
|
|
}
|
|
|
|
if (tileset.debugShowContentBoundingVolume && hasContentBoundingVolume) {
|
|
if (!defined(tile._debugContentBoundingVolume)) {
|
|
tile._debugContentBoundingVolume = tile._contentBoundingVolume.createDebugVolume(
|
|
Color.BLUE
|
|
);
|
|
}
|
|
tile._debugContentBoundingVolume.update(frameState);
|
|
} else if (
|
|
!tileset.debugShowContentBoundingVolume &&
|
|
defined(tile._debugContentBoundingVolume)
|
|
) {
|
|
tile._debugContentBoundingVolume = tile._debugContentBoundingVolume.destroy();
|
|
}
|
|
|
|
if (
|
|
tileset.debugShowViewerRequestVolume &&
|
|
defined(tile._viewerRequestVolume)
|
|
) {
|
|
if (!defined(tile._debugViewerRequestVolume)) {
|
|
tile._debugViewerRequestVolume = tile._viewerRequestVolume.createDebugVolume(
|
|
Color.YELLOW
|
|
);
|
|
}
|
|
tile._debugViewerRequestVolume.update(frameState);
|
|
} else if (
|
|
!tileset.debugShowViewerRequestVolume &&
|
|
defined(tile._debugViewerRequestVolume)
|
|
) {
|
|
tile._debugViewerRequestVolume = tile._debugViewerRequestVolume.destroy();
|
|
}
|
|
|
|
const debugColorizeTilesOn =
|
|
(tileset.debugColorizeTiles && !tile._debugColorizeTiles) ||
|
|
defined(tileset._heatmap.tilePropertyName);
|
|
const debugColorizeTilesOff =
|
|
!tileset.debugColorizeTiles && tile._debugColorizeTiles;
|
|
|
|
if (debugColorizeTilesOn) {
|
|
tileset._heatmap.colorize(tile, frameState); // Skipped if tileset._heatmap.tilePropertyName is undefined
|
|
tile._debugColorizeTiles = true;
|
|
tile.color = tile._debugColor;
|
|
} else if (debugColorizeTilesOff) {
|
|
tile._debugColorizeTiles = false;
|
|
tile.color = Color.WHITE;
|
|
}
|
|
|
|
if (tile._colorDirty) {
|
|
tile._colorDirty = false;
|
|
tile._content.applyDebugSettings(true, tile._color);
|
|
}
|
|
|
|
if (debugColorizeTilesOff) {
|
|
tileset.makeStyleDirty(); // Re-apply style now that colorize is switched off
|
|
}
|
|
}
|
|
|
|
/**
|
|
* @private
|
|
* @param {Cesium3DTile} tile
|
|
* @param {Cesium3DTileset} tileset
|
|
* @param {FrameState} frameState
|
|
*/
|
|
function updateContent(tile, tileset, frameState) {
|
|
const expiredContent = tile._expiredContent;
|
|
|
|
// expired content is not supported for multiple contents
|
|
if (!tile.hasMultipleContents && defined(expiredContent)) {
|
|
if (!tile.contentReady) {
|
|
// Render the expired content while the content loads
|
|
try {
|
|
expiredContent.update(tileset, frameState);
|
|
} catch (error) {
|
|
// Eat error for expired content
|
|
}
|
|
return;
|
|
}
|
|
|
|
// New content is ready, destroy expired content
|
|
tile._expiredContent.destroy();
|
|
tile._expiredContent = undefined;
|
|
}
|
|
|
|
if (!defined(tile.content)) {
|
|
// Implicit placeholder tile
|
|
return;
|
|
}
|
|
|
|
try {
|
|
tile.content.update(tileset, frameState);
|
|
} catch (error) {
|
|
tile._contentState = Cesium3DTileContentState.FAILED;
|
|
throw error;
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Compute and compare ClippingPlanes state:
|
|
* - enabled-ness - are clipping planes enabled? is this tile clipped?
|
|
* - clipping plane count
|
|
* - clipping function (union v. intersection)
|
|
|
|
* @private
|
|
* @param {Cesium3DTile} tile
|
|
* @param {Cesium3DTileset} tileset
|
|
*/
|
|
function updateClippingPlanes(tile, tileset) {
|
|
const clippingPlanes = tileset.clippingPlanes;
|
|
let currentClippingPlanesState = 0;
|
|
if (defined(clippingPlanes) && tile._isClipped && clippingPlanes.enabled) {
|
|
currentClippingPlanesState = clippingPlanes.clippingPlanesState;
|
|
}
|
|
// If clippingPlaneState for tile changed, mark clippingPlanesDirty so content can update
|
|
if (currentClippingPlanesState !== tile._clippingPlanesState) {
|
|
tile._clippingPlanesState = currentClippingPlanesState;
|
|
tile.clippingPlanesDirty = true;
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Compute and compare ClippingPolygons state:
|
|
* - enabled-ness - are clipping polygons enabled? is this tile clipped?
|
|
* - clipping polygon count & position count
|
|
* - clipping function (inverse)
|
|
|
|
* @private
|
|
* @param {Cesium3DTile} tile
|
|
* @param {Cesium3DTileset} tileset
|
|
*/
|
|
function updateClippingPolygons(tile, tileset) {
|
|
const clippingPolygons = tileset.clippingPolygons;
|
|
let currentClippingPolygonsState = 0;
|
|
if (
|
|
defined(clippingPolygons) &&
|
|
tile._isClippedByPolygon &&
|
|
clippingPolygons.enabled
|
|
) {
|
|
currentClippingPolygonsState = clippingPolygons.clippingPolygonsState;
|
|
}
|
|
// If clippingPolygonState for tile changed, mark clippingPolygonsDirty so content can update
|
|
if (currentClippingPolygonsState !== tile._clippingPolygonsState) {
|
|
tile._clippingPolygonsState = currentClippingPolygonsState;
|
|
tile.clippingPolygonsDirty = true;
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Get the draw commands needed to render this tile.
|
|
*
|
|
* @private
|
|
* @param {Cesium3DTileset} tileset
|
|
* @param {FrameState} frameState
|
|
* @param {object} passOptions
|
|
*/
|
|
Cesium3DTile.prototype.update = function (tileset, frameState, passOptions) {
|
|
const { commandList } = frameState;
|
|
const commandStart = commandList.length;
|
|
|
|
updateClippingPlanes(this, tileset);
|
|
updateClippingPolygons(this, tileset);
|
|
applyDebugSettings(this, tileset, frameState, passOptions);
|
|
updateContent(this, tileset, frameState);
|
|
|
|
const commandEnd = commandList.length;
|
|
this._commandsLength = commandEnd - commandStart;
|
|
|
|
for (let i = commandStart; i < commandEnd; ++i) {
|
|
const command = commandList[i];
|
|
const translucent = command.pass === Pass.TRANSLUCENT;
|
|
command.depthForTranslucentClassification = translucent;
|
|
}
|
|
|
|
this.clippingPlanesDirty = false; // reset after content update
|
|
this.clippingPolygonsDirty = false;
|
|
};
|
|
|
|
const scratchCommandList = [];
|
|
|
|
/**
|
|
* Processes the tile's content, e.g., create WebGL resources, to move from the PROCESSING to READY state.
|
|
*
|
|
* @param {Cesium3DTileset} tileset The tileset containing this tile.
|
|
* @param {FrameState} frameState The frame state.
|
|
*
|
|
* @private
|
|
*/
|
|
Cesium3DTile.prototype.process = function (tileset, frameState) {
|
|
if (!this.contentExpired && !this.contentReady && this._content.ready) {
|
|
updateExpireDate(this);
|
|
|
|
// Refresh style for expired content
|
|
this._selectedFrame = 0;
|
|
this.lastStyleTime = 0.0;
|
|
|
|
JulianDate.now(this._loadTimestamp);
|
|
this._contentState = Cesium3DTileContentState.READY;
|
|
|
|
if (!this.hasTilesetContent && !this.hasImplicitContent) {
|
|
// RESEARCH_IDEA: ability to unload tiles (without content) for an
|
|
// external tileset when all the tiles are unloaded.
|
|
tileset._statistics.incrementLoadCounts(this.content);
|
|
++tileset._statistics.numberOfTilesWithContentReady;
|
|
++tileset._statistics.numberOfLoadedTilesTotal;
|
|
|
|
// Add to the tile cache. Previously expired tiles are already in the cache and won't get re-added.
|
|
tileset._cache.add(this);
|
|
}
|
|
}
|
|
|
|
const savedCommandList = frameState.commandList;
|
|
frameState.commandList = scratchCommandList;
|
|
|
|
try {
|
|
this._content.update(tileset, frameState);
|
|
} catch (error) {
|
|
this._contentState = Cesium3DTileContentState.FAILED;
|
|
throw error;
|
|
}
|
|
|
|
scratchCommandList.length = 0;
|
|
frameState.commandList = savedCommandList;
|
|
};
|
|
|
|
/**
|
|
* @private
|
|
* @param {number} normalizedValue
|
|
* @param {number} numberOfDigits
|
|
* @param {number} leftShift
|
|
* @returns {number}
|
|
*/
|
|
function isolateDigits(normalizedValue, numberOfDigits, leftShift) {
|
|
const scaled = normalizedValue * Math.pow(10, numberOfDigits);
|
|
const integer = parseInt(scaled);
|
|
return integer * Math.pow(10, leftShift);
|
|
}
|
|
|
|
/**
|
|
* @private
|
|
* @param {number} value
|
|
* @param {number} minimum
|
|
* @param {number} maximum
|
|
* @returns {number}
|
|
*/
|
|
function priorityNormalizeAndClamp(value, minimum, maximum) {
|
|
// Subtract epsilon since we only want decimal digits present in the output.
|
|
return Math.max(
|
|
CesiumMath.normalize(value, minimum, maximum) - CesiumMath.EPSILON7,
|
|
0.0
|
|
);
|
|
}
|
|
|
|
/**
|
|
* Sets the priority of the tile based on distance and depth
|
|
* @private
|
|
*/
|
|
Cesium3DTile.prototype.updatePriority = function () {
|
|
const tileset = this.tileset;
|
|
const preferLeaves = tileset.preferLeaves;
|
|
const minimumPriority = tileset._minimumPriority;
|
|
const maximumPriority = tileset._maximumPriority;
|
|
|
|
// Combine priority systems together by mapping them into a base 10 number where each priority controls a specific set of digits in the number.
|
|
// For number priorities, map them to a 0.xxxxx number then left shift it up into a set number of digits before the decimal point. Chop of the fractional part then left shift again into the position it needs to go.
|
|
// For blending number priorities, normalize them to 0-1 and interpolate to get a combined 0-1 number, then proceed as normal.
|
|
// Booleans can just be 0 or 10^leftshift.
|
|
// Think of digits as penalties since smaller numbers are higher priority. If a tile has some large quantity or has a flag raised it's (usually) penalized for it, expressed as a higher number for the digit.
|
|
// Priority number format: preloadFlightDigits(1) | foveatedDeferDigits(1) | foveatedDigits(4) | preloadProgressiveResolutionDigits(1) | preferredSortingDigits(4) . depthDigits(the decimal digits)
|
|
// Certain flags like preferLeaves will flip / turn off certain digits to get desired load order.
|
|
|
|
// Setup leftShifts, digit counts, and scales (for booleans)
|
|
const digitsForANumber = 4;
|
|
const digitsForABoolean = 1;
|
|
|
|
const preferredSortingLeftShift = 0;
|
|
const preferredSortingDigitsCount = digitsForANumber;
|
|
|
|
const foveatedLeftShift =
|
|
preferredSortingLeftShift + preferredSortingDigitsCount;
|
|
const foveatedDigitsCount = digitsForANumber;
|
|
|
|
const preloadProgressiveResolutionLeftShift =
|
|
foveatedLeftShift + foveatedDigitsCount;
|
|
const preloadProgressiveResolutionDigitsCount = digitsForABoolean;
|
|
const preloadProgressiveResolutionScale = Math.pow(
|
|
10,
|
|
preloadProgressiveResolutionLeftShift
|
|
);
|
|
|
|
const foveatedDeferLeftShift =
|
|
preloadProgressiveResolutionLeftShift +
|
|
preloadProgressiveResolutionDigitsCount;
|
|
const foveatedDeferDigitsCount = digitsForABoolean;
|
|
const foveatedDeferScale = Math.pow(10, foveatedDeferLeftShift);
|
|
|
|
const preloadFlightLeftShift =
|
|
foveatedDeferLeftShift + foveatedDeferDigitsCount;
|
|
const preloadFlightScale = Math.pow(10, preloadFlightLeftShift);
|
|
|
|
// Compute the digits for each priority
|
|
let depthDigits = priorityNormalizeAndClamp(
|
|
this._depth,
|
|
minimumPriority.depth,
|
|
maximumPriority.depth
|
|
);
|
|
depthDigits = preferLeaves ? 1.0 - depthDigits : depthDigits;
|
|
|
|
// Map 0-1 then convert to digit. Include a distance sort when doing non-skipLOD and replacement refinement, helps things like non-skipLOD photogrammetry
|
|
const useDistance =
|
|
!tileset.isSkippingLevelOfDetail &&
|
|
this.refine === Cesium3DTileRefine.REPLACE;
|
|
const normalizedPreferredSorting = useDistance
|
|
? priorityNormalizeAndClamp(
|
|
this._priorityHolder._distanceToCamera,
|
|
minimumPriority.distance,
|
|
maximumPriority.distance
|
|
)
|
|
: priorityNormalizeAndClamp(
|
|
this._priorityReverseScreenSpaceError,
|
|
minimumPriority.reverseScreenSpaceError,
|
|
maximumPriority.reverseScreenSpaceError
|
|
);
|
|
const preferredSortingDigits = isolateDigits(
|
|
normalizedPreferredSorting,
|
|
preferredSortingDigitsCount,
|
|
preferredSortingLeftShift
|
|
);
|
|
|
|
const preloadProgressiveResolutionDigits = this._priorityProgressiveResolution
|
|
? 0
|
|
: preloadProgressiveResolutionScale;
|
|
|
|
const normalizedFoveatedFactor = priorityNormalizeAndClamp(
|
|
this._priorityHolder._foveatedFactor,
|
|
minimumPriority.foveatedFactor,
|
|
maximumPriority.foveatedFactor
|
|
);
|
|
const foveatedDigits = isolateDigits(
|
|
normalizedFoveatedFactor,
|
|
foveatedDigitsCount,
|
|
foveatedLeftShift
|
|
);
|
|
|
|
const foveatedDeferDigits = this.priorityDeferred ? foveatedDeferScale : 0;
|
|
|
|
const preloadFlightDigits =
|
|
tileset._pass === Cesium3DTilePass.PRELOAD_FLIGHT ? 0 : preloadFlightScale;
|
|
|
|
// Get the final base 10 number
|
|
this._priority =
|
|
depthDigits +
|
|
preferredSortingDigits +
|
|
preloadProgressiveResolutionDigits +
|
|
foveatedDigits +
|
|
foveatedDeferDigits +
|
|
preloadFlightDigits;
|
|
};
|
|
|
|
/**
|
|
* @private
|
|
*/
|
|
Cesium3DTile.prototype.isDestroyed = function () {
|
|
return false;
|
|
};
|
|
|
|
/**
|
|
* @private
|
|
*/
|
|
Cesium3DTile.prototype.destroy = function () {
|
|
// For the interval between new content being requested and downloaded, expiredContent === content, so don't destroy twice
|
|
this._content = this._content && this._content.destroy();
|
|
this._expiredContent =
|
|
this._expiredContent &&
|
|
!this._expiredContent.isDestroyed() &&
|
|
this._expiredContent.destroy();
|
|
this._debugBoundingVolume =
|
|
this._debugBoundingVolume && this._debugBoundingVolume.destroy();
|
|
this._debugContentBoundingVolume =
|
|
this._debugContentBoundingVolume &&
|
|
this._debugContentBoundingVolume.destroy();
|
|
this._debugViewerRequestVolume =
|
|
this._debugViewerRequestVolume && this._debugViewerRequestVolume.destroy();
|
|
return destroyObject(this);
|
|
};
|
|
|
|
export default Cesium3DTile;
|