@cesium/engine
Version:
CesiumJS is a JavaScript library for creating 3D globes and 2D maps in a web browser without a plugin.
1,207 lines (1,082 loc) • 39.3 kB
JavaScript
import Cartesian3 from "../Core/Cartesian3.js";
import Check from "../Core/Check.js";
import clone from "../Core/clone.js";
import combine from "../Core/combine.js";
import defined from "../Core/defined.js";
import destroyObject from "../Core/destroyObject.js";
import DeveloperError from "../Core/DeveloperError.js";
import CesiumMath from "../Core/Math.js";
import HilbertOrder from "../Core/HilbertOrder.js";
import Matrix3 from "../Core/Matrix3.js";
import Rectangle from "../Core/Rectangle.js";
import S2Cell from "../Core/S2Cell.js";
import ImplicitSubtree from "./ImplicitSubtree.js";
import hasExtension from "./hasExtension.js";
import MetadataSemantic from "./MetadataSemantic.js";
import BoundingVolumeSemantics from "./BoundingVolumeSemantics.js";
/**
* A specialized {@link Cesium3DTileContent} that lazily evaluates an implicit
* tileset. It is somewhat similar in operation to a
* {@link Tileset3DTileContent} in that once the content is constructed, it
* updates the tileset tree with more tiles. However, unlike external tilesets,
* child subtrees are represented as additional placeholder nodes with
* Implicit3DTileContent.
* <p>
* Implements the {@link Cesium3DTileContent} interface.
* </p>
* This object is normally not instantiated directly, use {@link Implicit3DTileContent.fromSubtreeJson}.
*
* @alias Implicit3DTileContent
* @constructor
*
* @param {Cesium3DTileset} tileset The tileset this content belongs to
* @param {Cesium3DTile} tile The tile this content belongs to.
* @param {Resource} resource The resource for the tileset
* @param {object} [json] The JSON object containing the subtree. Mutually exclusive with arrayBuffer.
* @param {ArrayBuffer} [arrayBuffer] The array buffer that stores the content payload. Mutually exclusive with json.
* @param {number} [byteOffset=0] The offset into the array buffer, if one was provided
*
* @exception {DeveloperError} One of json and arrayBuffer must be defined.
*
* @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.
*/
function Implicit3DTileContent(tileset, tile, resource) {
//>>includeStart('debug', pragmas.debug);
Check.defined("tile.implicitTileset", tile.implicitTileset);
Check.defined("tile.implicitCoordinates", tile.implicitCoordinates);
//>>includeEnd('debug');
const implicitTileset = tile.implicitTileset;
const implicitCoordinates = tile.implicitCoordinates;
this._implicitTileset = implicitTileset;
this._implicitCoordinates = implicitCoordinates;
this._implicitSubtree = undefined;
this._tileset = tileset;
this._tile = tile;
this._resource = resource;
this._metadata = undefined;
this.featurePropertiesDirty = false;
this._group = undefined;
const templateValues = implicitCoordinates.getTemplateValues();
const subtreeResource = implicitTileset.subtreeUriTemplate.getDerivedResource(
{
templateValues: templateValues,
},
);
this._url = subtreeResource.getUrlComponent(true);
this._ready = false;
}
Object.defineProperties(Implicit3DTileContent.prototype, {
featuresLength: {
get: function () {
return 0;
},
},
pointsLength: {
get: function () {
return 0;
},
},
trianglesLength: {
get: function () {
return 0;
},
},
geometryByteLength: {
get: function () {
return 0;
},
},
texturesByteLength: {
get: function () {
return 0;
},
},
batchTableByteLength: {
get: function () {
return 0;
},
},
innerContents: {
get: function () {
return undefined;
},
},
/**
* Returns true when the tile's content is ready to render; otherwise false
*
* @memberof Implicit3DTileContent.prototype
*
* @type {boolean}
* @readonly
* @private
*/
ready: {
get: function () {
return this._ready;
},
},
tileset: {
get: function () {
return this._tileset;
},
},
tile: {
get: function () {
return this._tile;
},
},
url: {
get: function () {
return this._url;
},
},
/**
* Part of the {@link Cesium3DTileContent} interface. <code>Implicit3DTileContent</code>
* always returns <code>undefined</code>. Only transcoded tiles have content metadata.
* @memberof Implicit3DTileContent.prototype
* @private
*/
metadata: {
get: function () {
return undefined;
},
set: function () {
//>>includeStart('debug', pragmas.debug);
throw new DeveloperError("Implicit3DTileContent cannot have metadata");
//>>includeEnd('debug');
},
},
batchTable: {
get: function () {
return undefined;
},
},
group: {
get: function () {
return this._group;
},
set: function (value) {
this._group = value;
},
},
});
/**
* Initialize the implicit content by parsing the subtree resource and setting
* up a promise chain to expand the immediate subtree.
*
* @param {Cesium3DTileset} tileset The tileset this content belongs to
* @param {Cesium3DTile} tile The tile this content belongs to.
* @param {Resource} resource The resource for the tileset
* @param {object} [json] The JSON containing the subtree. Mutually exclusive with arrayBuffer.
* @param {ArrayBuffer} [arrayBuffer] The ArrayBuffer containing a subtree binary. Mutually exclusive with json.
* @param {number} [byteOffset=0] The byte offset into the arrayBuffer
* @return {Promise<Implicit3DTileContent>}
*
* @exception {DeveloperError} One of json and arrayBuffer must be defined.
*
* @private
*/
Implicit3DTileContent.fromSubtreeJson = async function (
tileset,
tile,
resource,
json,
arrayBuffer,
byteOffset,
) {
//>>includeStart('debug', pragmas.debug);
Check.defined("tile.implicitTileset", tile.implicitTileset);
Check.defined("tile.implicitCoordinates", tile.implicitCoordinates);
if (defined(json) === defined(arrayBuffer)) {
throw new DeveloperError("One of json and arrayBuffer must be defined.");
}
//>>includeEnd('debug');
byteOffset = byteOffset ?? 0;
let uint8Array;
if (defined(arrayBuffer)) {
uint8Array = new Uint8Array(arrayBuffer, byteOffset);
}
const implicitTileset = tile.implicitTileset;
const implicitCoordinates = tile.implicitCoordinates;
const subtree = await ImplicitSubtree.fromSubtreeJson(
resource,
json,
uint8Array,
implicitTileset,
implicitCoordinates,
);
const content = new Implicit3DTileContent(tileset, tile, resource);
content._implicitSubtree = subtree;
expandSubtree(content, subtree);
content._ready = true;
return content;
};
/**
* Expand a single subtree placeholder tile. This transcodes the subtree into
* a tree of {@link Cesium3DTile}. The root of this tree is stored in
* the placeholder tile's children array. This method also creates placeholder
* tiles for the child subtrees to be lazily expanded as needed.
*
* @param {Implicit3DTileContent} content The content
* @param {ImplicitSubtree} subtree The parsed subtree
* @private
*/
function expandSubtree(content, subtree) {
const placeholderTile = content._tile;
// Parse the tiles inside this immediate subtree
const childIndex = content._implicitCoordinates.childIndex;
const results = transcodeSubtreeTiles(
content,
subtree,
placeholderTile,
childIndex,
);
const statistics = content._tileset.statistics;
// Link the new subtree to the existing placeholder tile.
placeholderTile.children.push(results.rootTile);
statistics.numberOfTilesTotal++;
// for each child subtree, make new placeholder tiles
const childSubtrees = listChildSubtrees(content, subtree, results.bottomRow);
for (let i = 0; i < childSubtrees.length; i++) {
const subtreeLocator = childSubtrees[i];
const leafTile = subtreeLocator.tile;
const implicitChildTile = makePlaceholderChildSubtree(
content,
leafTile,
subtreeLocator.childIndex,
);
leafTile.children.push(implicitChildTile);
statistics.numberOfTilesTotal++;
}
}
/**
* A pair of (tile, childIndex) used for finding child subtrees.
*
* @typedef {object} ChildSubtreeLocator
* @property {Cesium3DTile} tile One of the tiles in the bottommost row of the subtree.
* @property {number} childIndex The morton index of the child tile relative to its parent
* @private
*/
/**
* Determine what child subtrees exist and return a list of information
*
* @param {Implicit3DTileContent} content The implicit content
* @param {ImplicitSubtree} subtree The subtree for looking up availability
* @param {Array<Cesium3DTile|undefined>} bottomRow The bottom row of tiles in a transcoded subtree
* @returns {ChildSubtreeLocator[]} A list of identifiers for the child subtrees.
* @private
*/
function listChildSubtrees(content, subtree, bottomRow) {
const results = [];
const branchingFactor = content._implicitTileset.branchingFactor;
for (let i = 0; i < bottomRow.length; i++) {
const leafTile = bottomRow[i];
if (!defined(leafTile)) {
continue;
}
for (let j = 0; j < branchingFactor; j++) {
const index = i * branchingFactor + j;
if (subtree.childSubtreeIsAvailableAtIndex(index)) {
results.push({
tile: leafTile,
childIndex: j,
});
}
}
}
return results;
}
/**
* Results of transcodeSubtreeTiles, containing the root tile of the
* subtree and the bottom row of nodes for further processing.
*
* @typedef {object} TranscodedSubtree
* @property {Cesium3DTile} rootTile The transcoded root tile of the subtree
* @property {Array<Cesium3DTile|undefined>} bottomRow The bottom row of transcoded tiles. This is helpful for processing child subtrees
* @private
*/
/**
* Transcode the implicitly-defined tiles within this subtree and generate
* explicit {@link Cesium3DTile} objects. This function only transcode tiles,
* child subtrees are handled separately.
*
* @param {Implicit3DTileContent} content The implicit content
* @param {ImplicitSubtree} subtree The subtree to get availability information
* @param {Cesium3DTile} placeholderTile The placeholder tile, used for constructing the subtree root tile
* @param {number} childIndex The Morton index of the root tile relative to parentOfRootTile
* @returns {TranscodedSubtree} The newly created subtree of tiles
* @private
*/
function transcodeSubtreeTiles(content, subtree, placeholderTile, childIndex) {
const rootBitIndex = 0;
const rootParentIsPlaceholder = true;
const rootTile = deriveChildTile(
content,
subtree,
placeholderTile,
childIndex,
rootBitIndex,
rootParentIsPlaceholder,
);
const statistics = content._tileset.statistics;
// Sliding window over the levels of the tree.
// Each row is branchingFactor * length of previous row
// Tiles within a row are ordered by Morton index.
let parentRow = [rootTile];
let currentRow = [];
const implicitTileset = content._implicitTileset;
for (let level = 1; level < implicitTileset.subtreeLevels; level++) {
const levelOffset = subtree.getLevelOffset(level);
const numberOfChildren = implicitTileset.branchingFactor * parentRow.length;
for (
let childMortonIndex = 0;
childMortonIndex < numberOfChildren;
childMortonIndex++
) {
const childBitIndex = levelOffset + childMortonIndex;
if (!subtree.tileIsAvailableAtIndex(childBitIndex)) {
currentRow.push(undefined);
continue;
}
const parentMortonIndex = subtree.getParentMortonIndex(childMortonIndex);
const parentTile = parentRow[parentMortonIndex];
const childChildIndex =
childMortonIndex % implicitTileset.branchingFactor;
const childTile = deriveChildTile(
content,
subtree,
parentTile,
childChildIndex,
childBitIndex,
);
parentTile.children.push(childTile);
statistics.numberOfTilesTotal++;
currentRow.push(childTile);
}
parentRow = currentRow;
currentRow = [];
}
return {
rootTile: rootTile,
// At the end of the last loop, bottomRow was moved to parentRow
bottomRow: parentRow,
};
}
function getGeometricError(tileMetadata, implicitTileset, implicitCoordinates) {
const semantic = MetadataSemantic.TILE_GEOMETRIC_ERROR;
if (defined(tileMetadata) && tileMetadata.hasPropertyBySemantic(semantic)) {
return tileMetadata.getPropertyBySemantic(semantic);
}
return (
implicitTileset.geometricError / Math.pow(2, implicitCoordinates.level)
);
}
/**
* Given a parent tile and information about which child to create, derive
* the properties of the child tile implicitly.
* <p>
* This creates a real tile for rendering, not a placeholder tile like some of
* the other methods of ImplicitTileset.
* </p>
*
* @param {Implicit3DTileContent} implicitContent The implicit content
* @param {ImplicitSubtree} subtree The subtree the child tile belongs to
* @param {Cesium3DTile} parentTile The parent of the new child tile
* @param {number} childIndex The morton index of the child tile relative to its parent
* @param {number} childBitIndex The index of the child tile within the tile's availability information.
* @param {boolean} [parentIsPlaceholderTile=false] True if parentTile is a placeholder tile. This is true for the root of each subtree.
* @returns {Cesium3DTile} The new child tile.
* @private
*/
function deriveChildTile(
implicitContent,
subtree,
parentTile,
childIndex,
childBitIndex,
parentIsPlaceholderTile,
) {
const implicitTileset = implicitContent._implicitTileset;
let implicitCoordinates;
if (parentIsPlaceholderTile ?? false) {
implicitCoordinates = parentTile.implicitCoordinates;
} else {
implicitCoordinates =
parentTile.implicitCoordinates.getChildCoordinates(childIndex);
}
// Parse metadata and bounding volume semantics at the beginning
// as the bounding volumes are needed below.
let tileMetadata;
let tileBounds;
if (defined(subtree.tilePropertyTableJson)) {
tileMetadata = subtree.getTileMetadataView(implicitCoordinates);
tileBounds = BoundingVolumeSemantics.parseAllBoundingVolumeSemantics(
"TILE",
tileMetadata,
);
}
// Content is not loaded at this point, so this flag is set for future reference.
const contentPropertyTableJsons = subtree.contentPropertyTableJsons;
const length = contentPropertyTableJsons.length;
let hasImplicitContentMetadata = false;
for (let i = 0; i < length; i++) {
if (subtree.contentIsAvailableAtCoordinates(implicitCoordinates, i)) {
hasImplicitContentMetadata = true;
break;
}
}
const boundingVolume = getTileBoundingVolume(
implicitTileset,
implicitCoordinates,
childIndex,
parentIsPlaceholderTile,
parentTile,
tileBounds,
);
const contentJsons = [];
for (let i = 0; i < implicitTileset.contentCount; i++) {
if (!subtree.contentIsAvailableAtIndex(childBitIndex, i)) {
continue;
}
const childContentTemplate = implicitTileset.contentUriTemplates[i];
const childContentUri = childContentTemplate.getDerivedResource({
templateValues: implicitCoordinates.getTemplateValues(),
}).url;
const contentJson = {
uri: childContentUri,
};
let contentBounds;
if (subtree.contentPropertyTableJsons.length > 0) {
const contentMetadata = subtree.getContentMetadataView(
implicitCoordinates,
i,
);
contentBounds = BoundingVolumeSemantics.parseAllBoundingVolumeSemantics(
"CONTENT",
contentMetadata,
);
}
const contentBoundingVolume = getContentBoundingVolume(
boundingVolume,
contentBounds,
);
if (defined(contentBoundingVolume)) {
contentJson.boundingVolume = contentBoundingVolume;
}
// combine() is used to pass through any additional properties the
// user specified such as extras or extensions
contentJsons.push(combine(contentJson, implicitTileset.contentHeaders[i]));
}
const childGeometricError = getGeometricError(
tileMetadata,
implicitTileset,
implicitCoordinates,
);
const tileJson = {
boundingVolume: boundingVolume,
geometricError: childGeometricError,
refine: implicitTileset.refine,
contents: contentJsons,
};
// combine() is used to pass through any additional properties the
// user specified such as extras or extensions.
const deep = true;
const rootHeader = clone(implicitTileset.tileHeader, deep);
// The bounding volume was computed above since it may come from metadata
// in the subtree file.
delete rootHeader.boundingVolume;
// Copying the transform to all the transcoded tiles would cause the transform
// to be applied multiple times. Removing it from the header avoids this issue.
delete rootHeader.transform;
// The implicit tiling spec does not specify what should happen if explicit
// tile metadata is added to the placeholder tile. Since implicit tile
// metadata comes from the subtree file, ignore the explicit version.
//
// Also, when a property with the semantic TILE_BOUNDING_VOLUME is added to
// the placeholder tile to set a tight bounding volume (See Cesium3DTile.js)
// propagating it to transcoded tiles causes transcoded tiles to use the
// wrong bounding volume, this can lead to loading far too many tiles.
delete rootHeader.metadata;
const combinedTileJson = combine(tileJson, rootHeader, deep);
const childTile = makeTile(
implicitContent,
implicitTileset.baseResource,
combinedTileJson,
parentTile,
);
childTile.implicitCoordinates = implicitCoordinates;
childTile.implicitSubtree = subtree;
childTile.metadata = tileMetadata;
childTile.hasImplicitContentMetadata = hasImplicitContentMetadata;
return childTile;
}
/**
* Checks whether the bounding volume's heights can be updated.
* Returns true if the minimumHeight/maximumHeight parameter
* is defined and the bounding volume is a region or S2 cell.
*
* @param {object} [boundingVolume] The bounding volume
* @param {object} [tileBounds] The tile bounds
* @param {number} [tileBounds.minimumHeight] The minimum height
* @param {number} [tileBounds.maximumHeight] The maximum height
* @returns {boolean} Whether the bounding volume's heights can be updated
* @private
*/
function canUpdateHeights(boundingVolume, tileBounds) {
return (
defined(boundingVolume) &&
defined(tileBounds) &&
(defined(tileBounds.minimumHeight) || defined(tileBounds.maximumHeight)) &&
(hasExtension(boundingVolume, "3DTILES_bounding_volume_S2") ||
defined(boundingVolume.region))
);
}
/**
* Update the minimum and maximum height of the bounding volume.
* This is typically used to tighten a bounding volume using the
* <code>TILE_MINIMUM_HEIGHT</code> and <code>TILE_MAXIMUM_HEIGHT</code>
* semantics. Heights are only updated if the respective
* minimumHeight/maximumHeight parameter is defined and the
* bounding volume is a region or S2 cell.
*
* @param {object} boundingVolume The bounding volume
* @param {object} [tileBounds] The tile bounds
* @param {number} [tileBounds.minimumHeight] The new minimum height
* @param {number} [tileBounds.maximumHeight] The new maximum height
* @private
*/
function updateHeights(boundingVolume, tileBounds) {
if (!defined(tileBounds)) {
return;
}
if (hasExtension(boundingVolume, "3DTILES_bounding_volume_S2")) {
updateS2CellHeights(
boundingVolume.extensions["3DTILES_bounding_volume_S2"],
tileBounds.minimumHeight,
tileBounds.maximumHeight,
);
} else if (defined(boundingVolume.region)) {
updateRegionHeights(
boundingVolume.region,
tileBounds.minimumHeight,
tileBounds.maximumHeight,
);
}
}
/**
* For a bounding region, update the minimum and maximum height. This
* is typically used to tighten a bounding volume using the
* <code>TILE_MINIMUM_HEIGHT</code> and <code>TILE_MAXIMUM_HEIGHT</code>
* semantics. Heights are only updated if the respective
* minimumHeight/maximumHeight parameter is defined.
*
* @param {Array} region A 6-element array describing the bounding region
* @param {number} [minimumHeight] The new minimum height
* @param {number} [maximumHeight] The new maximum height
* @private
*/
function updateRegionHeights(region, minimumHeight, maximumHeight) {
if (defined(minimumHeight)) {
region[4] = minimumHeight;
}
if (defined(maximumHeight)) {
region[5] = maximumHeight;
}
}
/**
* For a bounding S2 cell, update the minimum and maximum height. This
* is typically used to tighten a bounding volume using the
* <code>TILE_MINIMUM_HEIGHT</code> and <code>TILE_MAXIMUM_HEIGHT</code>
* semantics. Heights are only updated if the respective
* minimumHeight/maximumHeight parameter is defined.
*
* @param {object} s2CellVolume An object describing the S2 cell
* @param {number} [minimumHeight] The new minimum height
* @param {number} [maximumHeight] The new maximum height
* @private
*/
function updateS2CellHeights(s2CellVolume, minimumHeight, maximumHeight) {
if (defined(minimumHeight)) {
s2CellVolume.minimumHeight = minimumHeight;
}
if (defined(maximumHeight)) {
s2CellVolume.maximumHeight = maximumHeight;
}
}
/**
* Gets the tile's bounding volume, which may be specified via
* metadata semantics such as TILE_BOUNDING_BOX or implicitly
* derived from the implicit root tile's bounding volume.
* <p>
* Priority of bounding volume types:
* <ol>
* <li>Explicit min/max height
* <ol>
* <li>With explicit region</li>
* <li>With implicit S2</li>
* <li>With implicit region</li>
* </ol>
* </li>
* <li>Explicit box</li>
* <li>Explicit region</li>
* <li>Explicit sphere</li>
* <li>Implicit S2</li>
* <li>Implicit box</li>
* <li>Implicit region</li>
* </ol>
* </p>
*
* @param {ImplicitTileset} implicitTileset The implicit tileset struct which holds the root bounding volume
* @param {ImplicitTileCoordinates} implicitCoordinates The coordinates of the child tile
* @param {number} childIndex The morton index of the child tile relative to its parent
* @param {boolean} parentIsPlaceholderTile True if parentTile is a placeholder tile. This is true for the root of each subtree.
* @param {Cesium3DTile} parentTile The parent of the new child tile
* @param {object} [tileBounds] The tile bounds
* @returns {object} An object containing the JSON for a bounding volume
* @private
*/
function getTileBoundingVolume(
implicitTileset,
implicitCoordinates,
childIndex,
parentIsPlaceholderTile,
parentTile,
tileBounds,
) {
let boundingVolume;
if (
!defined(tileBounds) ||
!defined(tileBounds.boundingVolume) ||
(!canUpdateHeights(tileBounds.boundingVolume, tileBounds) &&
canUpdateHeights(implicitTileset.boundingVolume, tileBounds))
) {
boundingVolume = deriveBoundingVolume(
implicitTileset,
implicitCoordinates,
childIndex,
parentIsPlaceholderTile ?? false,
parentTile,
);
} else {
boundingVolume = tileBounds.boundingVolume;
}
// The TILE_MINIMUM_HEIGHT and TILE_MAXIMUM_HEIGHT metadata semantics
// can be used to tighten the bounding volume
updateHeights(boundingVolume, tileBounds);
return boundingVolume;
}
/**
* Gets the content bounding volume, which may be specified via
* metadata semantics such as CONTENT_BOUNDING_BOX.
* <p>
* Priority of bounding volume types:
* <ol>
* <li>Explicit min/max height
* <ol>
* <li>With explicit region</li>
* <li>With tile bounding volume (S2 or region)</li>
* </ol>
* </li>
* <li>Explicit box</li>
* <li>Explicit region</li>
* <li>Explicit sphere</li>
* <li>Tile bounding volume (when content.boundingVolume is undefined)</li>
* </ol>
* </p>
*
* @param {object} tileBoundingVolume An object containing the JSON for the tile's bounding volume
* @param {object} [contentBounds] The content bounds
* @returns {object|undefined} An object containing the JSON for a bounding volume, or <code>undefined</code> if there is no bounding volume
* @private
*/
function getContentBoundingVolume(tileBoundingVolume, contentBounds) {
// content bounding volumes can only be specified via
// metadata semantics such as CONTENT_BOUNDING_BOX
let contentBoundingVolume;
if (defined(contentBounds)) {
contentBoundingVolume = contentBounds.boundingVolume;
}
// The CONTENT_MINIMUM_HEIGHT and CONTENT_MAXIMUM_HEIGHT metadata semantics
// can be used to tighten the bounding volume
if (canUpdateHeights(contentBoundingVolume, contentBounds)) {
updateHeights(contentBoundingVolume, contentBounds);
} else if (canUpdateHeights(tileBoundingVolume, contentBounds)) {
contentBoundingVolume = clone(tileBoundingVolume, true);
updateHeights(contentBoundingVolume, contentBounds);
}
return contentBoundingVolume;
}
/**
* Given the coordinates of a tile, derive its bounding volume from the root.
*
* @param {ImplicitTileset} implicitTileset The implicit tileset struct which holds the root bounding volume
* @param {ImplicitTileCoordinates} implicitCoordinates The coordinates of the child tile
* @param {number} childIndex The morton index of the child tile relative to its parent
* @param {boolean} parentIsPlaceholderTile True if parentTile is a placeholder tile. This is true for the root of each subtree.
* @param {Cesium3DTile} parentTile The parent of the new child tile
* @returns {object} An object containing the JSON for a bounding volume
* @private
*/
function deriveBoundingVolume(
implicitTileset,
implicitCoordinates,
childIndex,
parentIsPlaceholderTile,
parentTile,
) {
const rootBoundingVolume = implicitTileset.boundingVolume;
if (hasExtension(rootBoundingVolume, "3DTILES_bounding_volume_S2")) {
return deriveBoundingVolumeS2(
parentIsPlaceholderTile,
parentTile,
childIndex,
implicitCoordinates.level,
implicitCoordinates.x,
implicitCoordinates.y,
implicitCoordinates.z,
);
}
if (defined(rootBoundingVolume.region)) {
const childRegion = deriveBoundingRegion(
rootBoundingVolume.region,
implicitCoordinates.level,
implicitCoordinates.x,
implicitCoordinates.y,
implicitCoordinates.z,
);
return {
region: childRegion,
};
}
const childBox = deriveBoundingBox(
rootBoundingVolume.box,
implicitCoordinates.level,
implicitCoordinates.x,
implicitCoordinates.y,
implicitCoordinates.z,
);
return {
box: childBox,
};
}
/**
* Derive a bounding volume for a descendant tile (child, grandchild, etc.),
* assuming a quadtree or octree implicit tiling scheme. The (level, x, y, [z])
* coordinates are given to select the descendant tile and compute its position
* and dimensions.
* <p>
* If z is present, octree subdivision is used. Otherwise, quadtree subdivision
* is used. Quadtrees are always divided at the midpoint of the the horizontal
* dimensions, i.e. (x, y), leaving the z axis unchanged.
* </p>
*
* @param {boolean} parentIsPlaceholderTile True if parentTile is a placeholder tile. This is true for the root of each subtree.
* @param {Cesium3DTile} parentTile The parent of the new child tile
* @param {number} childIndex The morton index of the child tile relative to its parent
* @param {number} level The level of the descendant tile relative to the root implicit tile
* @param {number} x The x coordinate of the descendant tile
* @param {number} y The y coordinate of the descendant tile
* @param {number} [z] The z coordinate of the descendant tile (octree only)
* @returns {object} An object with the 3DTILES_bounding_volume_S2 extension.
* @private
*/
function deriveBoundingVolumeS2(
parentIsPlaceholderTile,
parentTile,
childIndex,
level,
x,
y,
z,
) {
//>>includeStart('debug', pragmas.debug);
Check.typeOf.bool("parentIsPlaceholderTile", parentIsPlaceholderTile);
Check.typeOf.object("parentTile", parentTile);
Check.typeOf.number("childIndex", childIndex);
Check.typeOf.number("level", level);
Check.typeOf.number("x", x);
Check.typeOf.number("y", y);
if (defined(z)) {
Check.typeOf.number("z", z);
}
//>>includeEnd('debug');
const boundingVolumeS2 = parentTile._boundingVolume;
// Handle the placeholder tile case, where we just duplicate the placeholder's bounding volume.
if (parentIsPlaceholderTile) {
return {
extensions: {
"3DTILES_bounding_volume_S2": {
token: S2Cell.getTokenFromId(boundingVolumeS2.s2Cell._cellId),
minimumHeight: boundingVolumeS2.minimumHeight,
maximumHeight: boundingVolumeS2.maximumHeight,
},
},
};
}
// Extract the first 3 face bits from the 64-bit S2 cell ID.
const face = Number(parentTile._boundingVolume.s2Cell._cellId >> BigInt(61));
// The Hilbert curve is rotated for the "odd" faces on the S2 Earthcube.
// See http://s2geometry.io/devguide/img/s2cell_global.jpg
const position =
face % 2 === 0
? HilbertOrder.encode2D(level, x, y)
: HilbertOrder.encode2D(level, y, x);
const cell = S2Cell.fromFacePositionLevel(face, BigInt(position), level);
let minHeight, maxHeight;
if (defined(z)) {
const midpointHeight =
(boundingVolumeS2.maximumHeight + boundingVolumeS2.minimumHeight) / 2;
minHeight =
childIndex < 4 ? boundingVolumeS2.minimumHeight : midpointHeight;
maxHeight =
childIndex < 4 ? midpointHeight : boundingVolumeS2.maximumHeight;
} else {
minHeight = boundingVolumeS2.minimumHeight;
maxHeight = boundingVolumeS2.maximumHeight;
}
return {
extensions: {
"3DTILES_bounding_volume_S2": {
token: S2Cell.getTokenFromId(cell._cellId),
minimumHeight: minHeight,
maximumHeight: maxHeight,
},
},
};
}
const scratchScaleFactors = new Cartesian3();
const scratchRootCenter = new Cartesian3();
const scratchCenter = new Cartesian3();
const scratchHalfAxes = new Matrix3();
/**
* Derive a bounding volume for a descendant tile (child, grandchild, etc.),
* assuming a quadtree or octree implicit tiling scheme. The (level, x, y, [z])
* coordinates are given to select the descendant tile and compute its position
* and dimensions.
* <p>
* If z is present, octree subdivision is used. Otherwise, quadtree subdivision
* is used. Quadtrees are always divided at the midpoint of the the horizontal
* dimensions, i.e. (x, y), leaving the z axis unchanged.
* </p>
* <p>
* This computes the child volume directly from the root bounding volume rather
* than recursively subdividing to minimize floating point error.
* </p>
*
* @param {number[]} rootBox An array of 12 numbers representing the bounding box of the root tile
* @param {number} level The level of the descendant tile relative to the root implicit tile
* @param {number} x The x coordinate of the descendant tile
* @param {number} y The y coordinate of the descendant tile
* @param {number} [z] The z coordinate of the descendant tile (octree only)
* @returns {number[]} An array of 12 numbers representing the bounding box of the descendant tile.
* @private
*/
function deriveBoundingBox(rootBox, level, x, y, z) {
//>>includeStart('debug', pragmas.debug);
Check.typeOf.object("rootBox", rootBox);
Check.typeOf.number("level", level);
Check.typeOf.number("x", x);
Check.typeOf.number("y", y);
if (defined(z)) {
Check.typeOf.number("z", z);
}
//>>includeEnd('debug');
if (level === 0) {
return rootBox;
}
const rootCenter = Cartesian3.unpack(rootBox, 0, scratchRootCenter);
const rootHalfAxes = Matrix3.unpack(rootBox, 3, scratchHalfAxes);
const tileScale = Math.pow(2, -level);
const modelSpaceX = -1 + (2 * x + 1) * tileScale;
const modelSpaceY = -1 + (2 * y + 1) * tileScale;
let modelSpaceZ = 0;
const scaleFactors = Cartesian3.fromElements(
tileScale,
tileScale,
1,
scratchScaleFactors,
);
if (defined(z)) {
modelSpaceZ = -1 + (2 * z + 1) * tileScale;
scaleFactors.z = tileScale;
}
let center = Cartesian3.fromElements(
modelSpaceX,
modelSpaceY,
modelSpaceZ,
scratchCenter,
);
center = Matrix3.multiplyByVector(rootHalfAxes, center, scratchCenter);
center = Cartesian3.add(center, rootCenter, scratchCenter);
let halfAxes = Matrix3.clone(rootHalfAxes);
halfAxes = Matrix3.multiplyByScale(halfAxes, scaleFactors, halfAxes);
const childBox = new Array(12);
Cartesian3.pack(center, childBox);
Matrix3.pack(halfAxes, childBox, 3);
return childBox;
}
const scratchRectangle = new Rectangle();
/**
* Derive a bounding volume for a descendant tile (child, grandchild, etc.),
* assuming a quadtree or octree implicit tiling scheme. The (level, x, y, [z])
* coordinates are given to select the descendant tile and compute its position
* and dimensions.
* <p>
* If z is present, octree subdivision is used. Otherwise, quadtree subdivision
* is used. Quadtrees are always divided at the midpoint of the the horizontal
* dimensions, i.e. (mid_longitude, mid_latitude), leaving the height values
* unchanged.
* </p>
* <p>
* This computes the child volume directly from the root bounding volume rather
* than recursively subdividing to minimize floating point error.
* </p>
* @param {number[]} rootRegion An array of 6 numbers representing the root implicit tile
* @param {number} level The level of the descendant tile relative to the root implicit tile
* @param {number} x The x coordinate of the descendant tile
* @param {number} y The x coordinate of the descendant tile
* @param {number} [z] The z coordinate of the descendant tile (octree only)
* @returns {number[]} An array of 6 numbers representing the bounding region of the descendant tile
* @private
*/
function deriveBoundingRegion(rootRegion, level, x, y, z) {
//>>includeStart('debug', pragmas.debug);
Check.typeOf.object("rootRegion", rootRegion);
Check.typeOf.number("level", level);
Check.typeOf.number("x", x);
Check.typeOf.number("y", y);
if (defined(z)) {
Check.typeOf.number("z", z);
}
//>>includeEnd('debug');
if (level === 0) {
return rootRegion.slice();
}
const rectangle = Rectangle.unpack(rootRegion, 0, scratchRectangle);
const rootMinimumHeight = rootRegion[4];
const rootMaximumHeight = rootRegion[5];
const tileScale = Math.pow(2, -level);
const childWidth = tileScale * rectangle.width;
const west = CesiumMath.negativePiToPi(rectangle.west + x * childWidth);
const east = CesiumMath.negativePiToPi(west + childWidth);
const childHeight = tileScale * rectangle.height;
const south = CesiumMath.negativePiToPi(rectangle.south + y * childHeight);
const north = CesiumMath.negativePiToPi(south + childHeight);
// Height is only subdivided for octrees; It remains constant for quadtrees.
let minimumHeight = rootMinimumHeight;
let maximumHeight = rootMaximumHeight;
if (defined(z)) {
const childThickness = tileScale * (rootMaximumHeight - rootMinimumHeight);
minimumHeight += z * childThickness;
maximumHeight = minimumHeight + childThickness;
}
return [west, south, east, north, minimumHeight, maximumHeight];
}
/**
* Create a placeholder 3D Tile whose content will be an Implicit3DTileContent
* for lazy evaluation of a child subtree.
*
* @param {Implicit3DTileContent} content The content object.
* @param {Cesium3DTile} parentTile The parent of the new child subtree.
* @param {number} childIndex The morton index of the child tile relative to its parent
* @returns {Cesium3DTile} The new placeholder tile
* @private
*/
function makePlaceholderChildSubtree(content, parentTile, childIndex) {
const implicitTileset = content._implicitTileset;
const implicitCoordinates =
parentTile.implicitCoordinates.getChildCoordinates(childIndex);
const childBoundingVolume = deriveBoundingVolume(
implicitTileset,
implicitCoordinates,
childIndex,
false,
parentTile,
);
// Ignore tile metadata when computing geometric error for the placeholder tile
// since the child subtree's metadata hasn't been loaded yet.
// The actual geometric error will be computed in deriveChildTile.
const childGeometricError = getGeometricError(
undefined,
implicitTileset,
implicitCoordinates,
);
const childContentUri = implicitTileset.subtreeUriTemplate.getDerivedResource(
{
templateValues: implicitCoordinates.getTemplateValues(),
},
).url;
const tileJson = {
boundingVolume: childBoundingVolume,
geometricError: childGeometricError,
refine: implicitTileset.refine,
contents: [
{
uri: childContentUri,
},
],
};
const tile = makeTile(
content,
implicitTileset.baseResource,
tileJson,
parentTile,
);
tile.implicitTileset = implicitTileset;
tile.implicitCoordinates = implicitCoordinates;
return tile;
}
/**
* Make a {@link Cesium3DTile}. This uses the content's tile's constructor instead
* of importing Cesium3DTile. This is to avoid a circular dependency between
* this file and Cesium3DTile.js
* @param {Implicit3DTileContent} content The implicit content
* @param {Resource} baseResource The base resource for the tileset
* @param {object} tileJson The JSON header for the tile
* @param {Cesium3DTile} parentTile The parent of the new tile
* @returns {Cesium3DTile} The newly created tile.
* @private
*/
function makeTile(content, baseResource, tileJson, parentTile) {
const Cesium3DTile = content._tile.constructor;
return new Cesium3DTile(content._tileset, baseResource, tileJson, parentTile);
}
/**
* Part of the {@link Cesium3DTileContent} interface. <code>Implicit3DTileContent</code>
* always returns <code>false</code> since a tile of this type does not have any features.
* @private
*/
Implicit3DTileContent.prototype.hasProperty = function (batchId, name) {
return false;
};
/**
* Part of the {@link Cesium3DTileContent} interface. <code>Implicit3DTileContent</code>
* always returns <code>undefined</code> since a tile of this type does not have any features.
* @private
*/
Implicit3DTileContent.prototype.getFeature = function (batchId) {
return undefined;
};
Implicit3DTileContent.prototype.applyDebugSettings = function (
enabled,
color,
) {};
Implicit3DTileContent.prototype.applyStyle = function (style) {};
Implicit3DTileContent.prototype.update = function (tileset, frameState) {};
Implicit3DTileContent.prototype.pick = function (ray, frameState, result) {
return undefined;
};
Implicit3DTileContent.prototype.isDestroyed = function () {
return false;
};
Implicit3DTileContent.prototype.destroy = function () {
this._implicitSubtree =
this._implicitSubtree && this._implicitSubtree.destroy();
return destroyObject(this);
};
// Exposed for testing
Implicit3DTileContent._deriveBoundingBox = deriveBoundingBox;
Implicit3DTileContent._deriveBoundingRegion = deriveBoundingRegion;
Implicit3DTileContent._deriveBoundingVolumeS2 = deriveBoundingVolumeS2;
export default Implicit3DTileContent;