@cesium/engine
Version:
CesiumJS is a JavaScript library for creating 3D globes and 2D maps in a web browser without a plugin.
1,454 lines (1,275 loc) • 105 kB
JavaScript
import BoundingSphere from "../Core/BoundingSphere.js";
import BoxOutlineGeometry from "../Core/BoxOutlineGeometry.js";
import Cartesian2 from "../Core/Cartesian2.js";
import Cartesian3 from "../Core/Cartesian3.js";
import Cartesian4 from "../Core/Cartesian4.js";
import Cartographic from "../Core/Cartographic.js";
import clone from "../Core/clone.js";
import Color from "../Core/Color.js";
import ColorGeometryInstanceAttribute from "../Core/ColorGeometryInstanceAttribute.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 Event from "../Core/Event.js";
import GeometryInstance from "../Core/GeometryInstance.js";
import GeometryPipeline from "../Core/GeometryPipeline.js";
import IndexDatatype from "../Core/IndexDatatype.js";
import Intersect from "../Core/Intersect.js";
import CesiumMath from "../Core/Math.js";
import Matrix4 from "../Core/Matrix4.js";
import NearFarScalar from "../Core/NearFarScalar.js";
import OrientedBoundingBox from "../Core/OrientedBoundingBox.js";
import OrthographicFrustum from "../Core/OrthographicFrustum.js";
import PrimitiveType from "../Core/PrimitiveType.js";
import Rectangle from "../Core/Rectangle.js";
import SphereOutlineGeometry from "../Core/SphereOutlineGeometry.js";
import VerticalExaggeration from "../Core/VerticalExaggeration.js";
import TerrainQuantization from "../Core/TerrainQuantization.js";
import Visibility from "../Core/Visibility.js";
import WebMercatorProjection from "../Core/WebMercatorProjection.js";
import Buffer from "../Renderer/Buffer.js";
import BufferUsage from "../Renderer/BufferUsage.js";
import ContextLimits from "../Renderer/ContextLimits.js";
import DrawCommand from "../Renderer/DrawCommand.js";
import Pass from "../Renderer/Pass.js";
import RenderState from "../Renderer/RenderState.js";
import VertexArray from "../Renderer/VertexArray.js";
import BlendingState from "./BlendingState.js";
import ClippingPlaneCollection from "./ClippingPlaneCollection.js";
import ClippingPolygonCollection from "./ClippingPolygonCollection.js";
import DepthFunction from "./DepthFunction.js";
import GlobeSurfaceTile from "./GlobeSurfaceTile.js";
import ImageryLayer from "./ImageryLayer.js";
import ImageryState from "./ImageryState.js";
import PerInstanceColorAppearance from "./PerInstanceColorAppearance.js";
import Primitive from "./Primitive.js";
import QuadtreeTileLoadState from "./QuadtreeTileLoadState.js";
import SceneMode from "./SceneMode.js";
import ShadowMode from "./ShadowMode.js";
import TerrainFillMesh from "./TerrainFillMesh.js";
import TerrainState from "./TerrainState.js";
import TileBoundingRegion from "./TileBoundingRegion.js";
import TileSelectionResult from "./TileSelectionResult.js";
/** @import Context from "../Renderer/Context.js"; */
/** @import EllipsoidalOccluder from "../Core/EllipsoidalOccluder.js"; */
/** @import FrameState from "./FrameState.js"; */
/** @import GlobeSurfaceShaderSet from "./GlobeSurfaceShaderSet.js"; */
/** @import ImageryLayerCollection from "./ImageryLayerCollection.js"; */
/** @import QuadtreeOccluders from "./QuadtreeOccluders.js"; */
/** @import QuadtreePrimitive from "./QuadtreePrimitive.js"; */
/** @import QuadtreeTile from "./QuadtreeTile.js"; */
/** @import TerrainMesh from "../Core/TerrainMesh.js"; */
/** @import TerrainProvider from "../Core/TerrainProvider.js"; */
/** @import TilingScheme from "../Core/TilingScheme.js"; */
/** @import { GlobeSurfaceShaderSetOptions } from "./GlobeSurfaceShaderSet.js"; */
/**
* Provides quadtree tiles representing the surface of the globe. This type is intended to be used
* with {@link QuadtreePrimitive}.
*
* @private
*/
class GlobeSurfaceTileProvider {
/**
* @param {object} options
* @param {TerrainProvider} options.terrainProvider The terrain provider that describes the surface geometry.
* @param {ImageryLayerCollection} options.imageryLayers The collection of imagery layers describing the shading of the surface.
* @param {GlobeSurfaceShaderSet} options.surfaceShaderSet The set of shaders used to render the surface.
*/
constructor(options) {
//>>includeStart('debug', pragmas.debug);
if (!defined(options)) {
throw new DeveloperError("options is required.");
}
if (!defined(options.terrainProvider)) {
throw new DeveloperError("options.terrainProvider is required.");
} else if (!defined(options.imageryLayers)) {
throw new DeveloperError("options.imageryLayers is required.");
} else if (!defined(options.surfaceShaderSet)) {
throw new DeveloperError("options.surfaceShaderSet is required.");
}
//>>includeEnd('debug');
this.lightingFadeOutDistance = 6500000.0;
this.lightingFadeInDistance = 9000000.0;
this.hasWaterMask = false;
this.showWaterEffect = false;
this.oceanNormalMap = undefined;
this.zoomedOutOceanSpecularIntensity = 0.5;
this.enableLighting = false;
this.dynamicAtmosphereLighting = false;
this.dynamicAtmosphereLightingFromSun = false;
this.showGroundAtmosphere = false;
this.shadows = ShadowMode.RECEIVE_ONLY;
this.vertexShadowDarkness = 0.3;
/**
* The color to use to highlight terrain fill tiles. If undefined, fill tiles are not
* highlighted at all. The alpha value is used to alpha blend with the tile's
* actual color. Because terrain fill tiles do not represent the actual terrain surface,
* it may be useful in some applications to indicate visually that they are not to be trusted.
* @type {Color}
* @default undefined
*/
this.fillHighlightColor = undefined;
this.hueShift = 0.0;
this.saturationShift = 0.0;
this.brightnessShift = 0.0;
this.showSkirts = true;
this.backFaceCulling = true;
this.undergroundColor = undefined;
this.undergroundColorAlphaByDistance = undefined;
this.lambertDiffuseMultiplier = 0.0;
this.materialUniformMap = undefined;
this._materialUniformMap = undefined;
this._quadtree = undefined;
this._terrainProvider = options.terrainProvider;
this._imageryLayers = options.imageryLayers;
this._surfaceShaderSet = options.surfaceShaderSet;
this._renderState = undefined;
this._blendRenderState = undefined;
this._disableCullingRenderState = undefined;
this._disableCullingBlendRenderState = undefined;
this._errorEvent = new Event();
this._removeLayerAddedListener =
this._imageryLayers.layerAdded.addEventListener(
GlobeSurfaceTileProvider.prototype._onLayerAdded,
this,
);
this._removeLayerRemovedListener =
this._imageryLayers.layerRemoved.addEventListener(
GlobeSurfaceTileProvider.prototype._onLayerRemoved,
this,
);
this._removeLayerMovedListener =
this._imageryLayers.layerMoved.addEventListener(
GlobeSurfaceTileProvider.prototype._onLayerMoved,
this,
);
this._removeLayerShownListener =
this._imageryLayers.layerShownOrHidden.addEventListener(
GlobeSurfaceTileProvider.prototype._onLayerShownOrHidden,
this,
);
this._imageryLayersUpdatedEvent = new Event();
this._layerOrderChanged = false;
/** @type {QuadtreeTile[][]} */
this._tilesToRenderByTextureCount = [];
/** @type {DrawCommand[]} */
this._drawCommands = [];
/** @type {object[]} */
this._uniformMaps = [];
this._usedDrawCommands = 0;
/** @type {VertexArray[]} */
this._vertexArraysToDestroy = [];
this._debug = {
wireframe: false,
boundingSphereTile: undefined,
tilesRendered: 0,
texturesRendered: 0,
};
this._baseColor = undefined;
this._firstPassInitialColor = undefined;
this.baseColor = new Color(0.0, 0.0, 0.5, 1.0);
/**
* A property specifying a {@link ClippingPlaneCollection} used to selectively disable rendering on the outside of each plane.
* @type {ClippingPlaneCollection}
* @ignore
*/
this._clippingPlanes = undefined;
/**
* A property specifying a {@link ClippingPolygonCollection} used to selectively disable rendering inside or outside a list of polygons.
* @type {ClippingPolygonCollection}
* @ignore
*/
this._clippingPolygons = undefined;
/**
* A property specifying a {@link Rectangle} used to selectively limit terrain and imagery rendering.
* @type {Rectangle}
*/
this.cartographicLimitRectangle = Rectangle.clone(Rectangle.MAX_VALUE);
this._hasLoadedTilesThisFrame = false;
this._hasFillTilesThisFrame = false;
this._oldVerticalExaggeration = undefined;
this._oldVerticalExaggerationRelativeHeight = undefined;
this._oldSceneMode = SceneMode.SCENE3D;
}
/**
* Gets or sets the color of the globe when no imagery is available.
* @type {Color}
*/
get baseColor() {
return this._baseColor;
}
set baseColor(value) {
//>>includeStart('debug', pragmas.debug);
if (!defined(value)) {
throw new DeveloperError("value is required.");
}
//>>includeEnd('debug');
this._baseColor = value;
this._firstPassInitialColor = Cartesian4.fromColor(
value,
this._firstPassInitialColor,
);
}
/**
* Gets or sets the {@link QuadtreePrimitive} for which this provider is
* providing tiles. This property may be undefined if the provider is not yet associated
* with a {@link QuadtreePrimitive}.
* @type {QuadtreePrimitive}
*/
get quadtree() {
return this._quadtree;
}
set quadtree(value) {
//>>includeStart('debug', pragmas.debug);
if (!defined(value)) {
throw new DeveloperError("value is required.");
}
//>>includeEnd('debug');
this._quadtree = value;
}
/**
* Gets the tiling scheme used by the provider.
* @type {TilingScheme}
*/
get tilingScheme() {
if (!defined(this._terrainProvider)) {
return undefined;
}
// @ts-expect-error Missing types.
return this._terrainProvider.tilingScheme;
}
/**
* Gets an event that is raised when the geometry provider encounters an asynchronous error. By subscribing
* to the event, you will be notified of the error and can potentially recover from it. Event listeners
* are passed an instance of {@link TileProviderError}.
* @type {Event<*>}
*/
get errorEvent() {
return this._errorEvent;
}
/**
* Gets an event that is raised when an imagery layer is added, shown, hidden, moved, or removed.
* @type {Event<*>}
*/
get imageryLayersUpdatedEvent() {
return this._imageryLayersUpdatedEvent;
}
/**
* Gets or sets the terrain provider that describes the surface geometry.
* @type {TerrainProvider}
*/
get terrainProvider() {
return this._terrainProvider;
}
set terrainProvider(terrainProvider) {
if (this._terrainProvider === terrainProvider) {
return;
}
this._terrainProvider = terrainProvider;
if (defined(this._quadtree)) {
this._quadtree.invalidateAllTiles();
}
}
/**
* The {@link ClippingPlaneCollection} used to selectively disable rendering.
*
* @type {ClippingPlaneCollection}
*
* @ignore
*/
get clippingPlanes() {
return this._clippingPlanes;
}
set clippingPlanes(value) {
ClippingPlaneCollection.setOwner(value, this, "_clippingPlanes");
}
/**
* The {@link ClippingPolygonCollection} used to selectively disable rendering inside or outside a list of polygons.
*
* @type {ClippingPolygonCollection}
*
* @ignore
*/
get clippingPolygons() {
return this._clippingPolygons;
}
set clippingPolygons(value) {
ClippingPolygonCollection.setOwner(value, this, "_clippingPolygons");
}
/**
* Make updates to the tile provider that are not involved in rendering. Called before the render update cycle.
* @param {FrameState} frameState
*/
update(frameState) {
// update collection: imagery indices, base layers, raise layer show/hide event
this._imageryLayers._update();
}
/**
* Called at the beginning of each render frame, before {@link QuadtreeTileProvider#showTileThisFrame}
* @param {FrameState} frameState The frame state.
*/
initialize(frameState) {
// update each layer for texture reprojection.
this._imageryLayers.queueReprojectionCommands(frameState);
if (this._layerOrderChanged) {
this._layerOrderChanged = false;
// Sort the TileImagery instances in each tile by the layer index.
this._quadtree.forEachLoadedTile(
/** @param {QuadtreeTile} tile */
function (tile) {
const surfaceTile = /** @type {GlobeSurfaceTile} */ (tile.data);
surfaceTile.imagery.sort(sortTileImageryByLayerIndex);
},
);
}
// Add credits for terrain and imagery providers.
updateCredits(this, frameState);
const vertexArraysToDestroy = this._vertexArraysToDestroy;
const length = vertexArraysToDestroy.length;
for (let j = 0; j < length; ++j) {
GlobeSurfaceTile._freeVertexArray(vertexArraysToDestroy[j]);
}
vertexArraysToDestroy.length = 0;
}
/**
* Called at the beginning of the update cycle for each render frame, before {@link QuadtreeTileProvider#showTileThisFrame}
* or any other functions.
*
* @param {FrameState} frameState The frame state.
*/
beginUpdate(frameState) {
const tilesToRenderByTextureCount = this._tilesToRenderByTextureCount;
for (let i = 0, len = tilesToRenderByTextureCount.length; i < len; ++i) {
const tiles = tilesToRenderByTextureCount[i];
if (defined(tiles)) {
tiles.length = 0;
}
}
// update clipping planes
const clippingPlanes = this._clippingPlanes;
// @ts-expect-error Missing types.
if (defined(clippingPlanes) && clippingPlanes.enabled) {
clippingPlanes.update(frameState);
}
// update clipping polygons
const clippingPolygons = this._clippingPolygons;
if (defined(clippingPolygons) && clippingPolygons.enabled) {
// @ts-expect-error Missing types.
clippingPolygons.update(frameState);
// @ts-expect-error Missing types.
clippingPolygons.queueCommands(frameState);
}
this._usedDrawCommands = 0;
this._hasLoadedTilesThisFrame = false;
this._hasFillTilesThisFrame = false;
}
/**
* Called at the end of the update cycle for each render frame, after {@link QuadtreeTileProvider#showTileThisFrame}
* and any other functions.
*
* @param {FrameState} frameState The frame state.
*/
endUpdate(frameState) {
if (!defined(this._renderState)) {
this._renderState = RenderState.fromCache({
// Write color and depth
cull: {
enabled: true,
},
depthTest: {
enabled: true,
func: DepthFunction.LESS,
},
});
this._blendRenderState = RenderState.fromCache({
// Write color and depth
cull: {
enabled: true,
},
depthTest: {
enabled: true,
func: DepthFunction.LESS_OR_EQUAL,
},
blending: BlendingState.ALPHA_BLEND,
});
let rs = /** @type {RenderState} */ (clone(this._renderState, true));
rs.cull.enabled = false;
this._disableCullingRenderState = RenderState.fromCache(rs);
rs = /** @type {RenderState} */ (clone(this._blendRenderState, true));
rs.cull.enabled = false;
this._disableCullingBlendRenderState = RenderState.fromCache(rs);
}
// If this frame has a mix of loaded and fill tiles, we need to propagate
// loaded heights to the fill tiles.
if (this._hasFillTilesThisFrame && this._hasLoadedTilesThisFrame) {
TerrainFillMesh.updateFillTiles(
this,
this._quadtree._tilesToRender,
frameState,
this._vertexArraysToDestroy,
);
}
// When vertical exaggeration changes, all of the loaded tiles need to generate
// geodetic surface normals so they can scale properly when rendered.
// When exaggeration is reset, geodetic surface normals are removed to decrease
// memory usage. Some tiles might have been constructed with the correct
// exaggeration already, so skip over them.
// If the geodetic surface normals can't be created because the tile doesn't
// have a mesh, keep checking until the tile does have a mesh. This can happen
// if the tile's mesh starts construction in a worker thread right before the
// exaggeration changes.
const quadtree = this.quadtree;
const exaggeration = frameState.verticalExaggeration;
const exaggerationRelativeHeight =
frameState.verticalExaggerationRelativeHeight;
const exaggerationChanged =
this._oldVerticalExaggeration !== exaggeration ||
this._oldVerticalExaggerationRelativeHeight !==
exaggerationRelativeHeight;
// Keep track of the next time there is a change in exaggeration
this._oldVerticalExaggeration = exaggeration;
this._oldVerticalExaggerationRelativeHeight = exaggerationRelativeHeight;
if (exaggerationChanged) {
quadtree.forEachLoadedTile(
/** @param {QuadtreeTile} tile */
function (tile) {
const surfaceTile = /** @type {GlobeSurfaceTile} */ (tile.data);
surfaceTile.updateExaggeration(tile, frameState, quadtree);
},
);
}
const sceneModeChanged = this._oldSceneMode !== frameState.mode;
this._oldSceneMode = frameState.mode;
if (sceneModeChanged) {
quadtree.forEachLoadedTile(
/** @param {QuadtreeTile} tile */
function (tile) {
const surfaceTile = /** @type {GlobeSurfaceTile} */ (tile.data);
surfaceTile.updateSceneMode(frameState.mode);
},
);
}
// Add the tile render commands to the command list, sorted by texture count.
const tilesToRenderByTextureCount = this._tilesToRenderByTextureCount;
for (
let textureCountIndex = 0,
textureCountLength = tilesToRenderByTextureCount.length;
textureCountIndex < textureCountLength;
++textureCountIndex
) {
const tilesToRender = tilesToRenderByTextureCount[textureCountIndex];
if (!defined(tilesToRender)) {
continue;
}
for (
let tileIndex = 0, tileLength = tilesToRender.length;
tileIndex < tileLength;
++tileIndex
) {
const tile = tilesToRender[tileIndex];
const surfaceTile = /** @type {GlobeSurfaceTile} */ (tile.data);
const tileBoundingRegion = surfaceTile.tileBoundingRegion;
addDrawCommandsForTile(this, tile, frameState);
frameState.minimumTerrainHeight = Math.min(
frameState.minimumTerrainHeight,
tileBoundingRegion.minimumHeight,
);
}
}
}
/**
* Adds draw commands for tiles rendered in the previous frame for a pick pass.
*
* @param {FrameState} frameState The frame state.
*/
updateForPick(frameState) {
// Add the tile pick commands from the tiles drawn last frame.
const drawCommands = this._drawCommands;
for (let i = 0, length = this._usedDrawCommands; i < length; ++i) {
pushCommand(drawCommands[i], frameState);
}
}
/**
* Cancels any imagery re-projections in the queue.
*/
cancelReprojections() {
this._imageryLayers.cancelReprojections();
}
/**
* Gets the maximum geometric error allowed in a tile at a given level, in meters.
*
* @param {number} level The tile level for which to get the maximum geometric error.
* @returns {number} The maximum geometric error in meters.
*/
getLevelMaximumGeometricError(level) {
if (!defined(this._terrainProvider)) {
return 0;
}
// @ts-expect-error Missing types.
return this._terrainProvider.getLevelMaximumGeometricError(level);
}
/**
* Loads, or continues loading, a given tile. This function will continue to be called
* until {@link QuadtreeTile#state} is no longer {@link QuadtreeTileLoadState#LOADING}.
*
* @param {FrameState} frameState The frame state.
* @param {QuadtreeTile} tile The tile to load.
*/
loadTile(frameState, tile) {
// We don't want to load imagery until we're certain that the terrain tiles are actually visible.
// So if our bounding volume isn't accurate because it came from another tile, load terrain only
// initially. If we load some terrain and suddenly have a more accurate bounding volume and the
// tile is _still_ visible, give the tile a chance to load imagery immediately rather than
// waiting for next frame.
let surfaceTile = /** @type {GlobeSurfaceTile} */ (tile.data);
let terrainOnly = true;
let terrainStateBefore;
if (defined(surfaceTile)) {
terrainOnly =
surfaceTile.boundingVolumeSourceTile !== tile ||
tile._lastSelectionResult === TileSelectionResult.CULLED_BUT_NEEDED;
terrainStateBefore = surfaceTile.terrainState;
}
GlobeSurfaceTile.processStateMachine(
tile,
frameState,
this.terrainProvider,
this._imageryLayers,
this.quadtree,
this._vertexArraysToDestroy,
terrainOnly,
);
surfaceTile = /** @type {GlobeSurfaceTile} */ (tile.data);
if (terrainOnly && terrainStateBefore !== surfaceTile.terrainState) {
// Terrain state changed. If:
// a) The tile is visible, and
// b) The bounding volume is accurate (updated as a side effect of computing visibility)
// Then we'll load imagery, too.
if (
this.computeTileVisibility(
tile,
frameState,
// @ts-expect-error Missing types.
this.quadtree.occluders,
) !== Visibility.NONE &&
surfaceTile.boundingVolumeSourceTile === tile
) {
terrainOnly = false;
GlobeSurfaceTile.processStateMachine(
tile,
frameState,
this.terrainProvider,
this._imageryLayers,
this.quadtree,
this._vertexArraysToDestroy,
terrainOnly,
);
}
}
}
/**
* Determines the visibility of a given tile. The tile may be fully visible, partially visible, or not
* visible at all. Tiles that are renderable and are at least partially visible will be shown by a call
* to {@link GlobeSurfaceTileProvider#showTileThisFrame}.
*
* @param {QuadtreeTile} tile The tile instance.
* @param {FrameState} frameState The state information about the current frame.
* @param {QuadtreeOccluders} occluders The objects that may occlude this tile.
*
* @returns {Visibility} Visibility.NONE if the tile is not visible,
* Visibility.PARTIAL if the tile is partially visible, or
* Visibility.FULL if the tile is fully visible.
*/
computeTileVisibility(tile, frameState, occluders) {
const distance = this.computeDistanceToTile(tile, frameState);
tile._distance = distance;
const undergroundVisible = isUndergroundVisible(this, frameState);
if (frameState.fog.enabled && !undergroundVisible) {
if (CesiumMath.fog(distance, frameState.fog.density) >= 1.0) {
// Tile is completely in fog so return that it is not visible.
return Visibility.NONE;
}
}
const surfaceTile = /** @type {GlobeSurfaceTile} */ (tile.data);
const tileBoundingRegion = surfaceTile.tileBoundingRegion;
if (surfaceTile.boundingVolumeSourceTile === undefined) {
// We have no idea where this tile is, so let's just call it partially visible.
return Visibility.PARTIAL;
}
const cullingVolume = frameState.cullingVolume;
// @ts-expect-error Missing types.
let boundingVolume = tileBoundingRegion.boundingVolume;
if (!defined(boundingVolume)) {
// @ts-expect-error Missing types.
boundingVolume = tileBoundingRegion.boundingSphere;
}
// Check if the tile is outside the limit area in cartographic space
surfaceTile.clippedByBoundaries = false;
const clippedCartographicLimitRectangle = clipRectangleAntimeridian(
tile.rectangle,
this.cartographicLimitRectangle,
);
const areaLimitIntersection = Rectangle.simpleIntersection(
clippedCartographicLimitRectangle,
tile.rectangle,
rectangleIntersectionScratch,
);
if (!defined(areaLimitIntersection)) {
return Visibility.NONE;
}
if (!Rectangle.equals(areaLimitIntersection, tile.rectangle)) {
surfaceTile.clippedByBoundaries = true;
}
if (frameState.mode !== SceneMode.SCENE3D) {
boundingVolume = boundingSphereScratch;
BoundingSphere.fromRectangleWithHeights2D(
tile.rectangle,
frameState.mapProjection,
tileBoundingRegion.minimumHeight,
tileBoundingRegion.maximumHeight,
boundingVolume,
);
Cartesian3.fromElements(
boundingVolume.center.z,
boundingVolume.center.x,
boundingVolume.center.y,
boundingVolume.center,
);
if (
frameState.mode === SceneMode.MORPHING &&
defined(surfaceTile.renderedMesh)
) {
boundingVolume = BoundingSphere.union(
// @ts-expect-error Missing types.
tileBoundingRegion.boundingSphere,
boundingVolume,
boundingVolume,
);
}
}
if (!defined(boundingVolume)) {
return Visibility.PARTIAL;
}
const clippingPlanes = this._clippingPlanes;
// @ts-expect-error Missing types.
if (defined(clippingPlanes) && clippingPlanes.enabled) {
const planeIntersection =
clippingPlanes.computeIntersectionWithBoundingVolume(boundingVolume);
// @ts-expect-error Possibly an error? isClipped is defined on surfaceTile, not tile.
tile.isClipped = planeIntersection !== Intersect.INSIDE;
if (planeIntersection === Intersect.OUTSIDE) {
return Visibility.NONE;
}
}
const clippingPolygons = this._clippingPolygons;
if (defined(clippingPolygons) && clippingPolygons.enabled) {
const polygonIntersection =
clippingPolygons.computeIntersectionWithBoundingVolume(
tileBoundingRegion,
);
// @ts-expect-error Possibly an error? isClipped is defined on surfaceTile, not tile.
tile.isClipped = polygonIntersection !== Intersect.OUTSIDE;
// Polygon clipping intersections are determined by outer rectangles, therefore we cannot
// preemptively determine if a tile is completely clipped or not here.
}
let visibility;
const intersection = cullingVolume.computeVisibility(boundingVolume);
if (intersection === Intersect.OUTSIDE) {
visibility = Visibility.NONE;
} else if (intersection === Intersect.INTERSECTING) {
visibility = Visibility.PARTIAL;
} else if (intersection === Intersect.INSIDE) {
visibility = Visibility.FULL;
}
if (visibility === Visibility.NONE) {
return visibility;
}
const ortho3D =
frameState.mode === SceneMode.SCENE3D &&
frameState.camera.frustum instanceof OrthographicFrustum;
if (
frameState.mode === SceneMode.SCENE3D &&
!ortho3D &&
defined(occluders) &&
!undergroundVisible
) {
const occludeePointInScaledSpace = surfaceTile.occludeePointInScaledSpace;
if (!defined(occludeePointInScaledSpace)) {
return visibility;
}
if (
occluders.ellipsoid.isScaledSpacePointVisiblePossiblyUnderEllipsoid(
occludeePointInScaledSpace,
tileBoundingRegion.minimumHeight,
)
) {
return visibility;
}
return Visibility.NONE;
}
return visibility;
}
/**
* Determines if the given tile can be refined
* @param {QuadtreeTile} tile The tile to check.
* @returns {boolean} True if the tile can be refined, false if it cannot.
*/
canRefine(tile) {
const surfaceTile = /** @type {GlobeSurfaceTile} */ (tile.data);
// Only allow refinement it we know whether or not the children of this tile exist.
// For a tileset with `availability`, we'll always be able to refine.
// We can ask for availability of _any_ child tile because we only need to confirm
// that we get a yes or no answer, it doesn't matter what the answer is.
if (defined(surfaceTile.terrainData)) {
return true;
}
const childAvailable = this.terrainProvider.getTileDataAvailable(
// @ts-expect-error Missing types.
tile.x * 2,
tile.y * 2,
tile.level + 1,
);
return childAvailable !== undefined;
}
/**
* Determines if the given not-fully-loaded tile can be rendered without losing detail that
* was present last frame as a result of rendering descendant tiles. This method will only be
* called if this tile's descendants were rendered last frame. If the tile is fully loaded,
* it is assumed that this method will return true and it will not be called.
* @param {QuadtreeTile} tile The tile to check.
* @returns {boolean} True if the tile can be rendered without losing detail.
*/
canRenderWithoutLosingDetail(tile) {
const surfaceTile = /** @type {GlobeSurfaceTile} */ (tile.data);
const readyImagery = readyImageryScratch;
// @ts-expect-error Missing types.
readyImagery.length = this._imageryLayers.length;
let terrainReady = false;
let initialImageryState = false;
let imagery;
if (defined(surfaceTile)) {
// We can render even with non-ready terrain as long as all our rendered descendants
// are missing terrain geometry too. i.e. if we rendered fills for more detailed tiles
// last frame, it's ok to render a fill for this tile this frame.
terrainReady = surfaceTile.terrainState === TerrainState.READY;
// Initially assume all imagery layers are ready, unless imagery hasn't been initialized at all.
initialImageryState = true;
imagery = surfaceTile.imagery;
}
let i;
let len;
for (i = 0, len = readyImagery.length; i < len; ++i) {
readyImagery[i] = initialImageryState;
}
if (defined(imagery)) {
for (i = 0, len = imagery.length; i < len; ++i) {
const tileImagery = imagery[i];
const loadingImagery = tileImagery.loadingImagery;
const isReady =
!defined(loadingImagery) ||
loadingImagery.state === ImageryState.FAILED ||
loadingImagery.state === ImageryState.INVALID;
const layerIndex = (
tileImagery.loadingImagery || tileImagery.readyImagery
).imageryLayer._layerIndex;
// For a layer to be ready, all tiles belonging to that layer must be ready.
readyImagery[layerIndex] = isReady && readyImagery[layerIndex];
}
}
const lastFrame = this.quadtree._lastSelectionFrameNumber;
// Traverse the descendants looking for one with terrain or imagery that is not loaded on this tile.
const stack = canRenderTraversalStack;
stack.length = 0;
stack.push(
tile.southwestChild,
tile.southeastChild,
tile.northwestChild,
tile.northeastChild,
);
while (stack.length > 0) {
const descendant = stack.pop();
const lastFrameSelectionResult =
descendant._lastSelectionResultFrame === lastFrame
? descendant._lastSelectionResult
: TileSelectionResult.NONE;
if (lastFrameSelectionResult === TileSelectionResult.RENDERED) {
const descendantSurface = descendant.data;
if (!defined(descendantSurface)) {
// Descendant has no data, so it can't block rendering.
continue;
}
if (
!terrainReady &&
descendant.data.terrainState === TerrainState.READY
) {
// Rendered descendant has real terrain, but we don't. Rendering is blocked.
return false;
}
const descendantImagery = descendant.data.imagery;
for (i = 0, len = descendantImagery.length; i < len; ++i) {
const descendantTileImagery = descendantImagery[i];
const descendantLoadingImagery = descendantTileImagery.loadingImagery;
const descendantIsReady =
!defined(descendantLoadingImagery) ||
descendantLoadingImagery.state === ImageryState.FAILED ||
descendantLoadingImagery.state === ImageryState.INVALID;
const descendantLayerIndex = (
descendantTileImagery.loadingImagery ||
descendantTileImagery.readyImagery
).imageryLayer._layerIndex;
// If this imagery tile of a descendant is ready but the layer isn't ready in this tile,
// then rendering is blocked.
if (descendantIsReady && !readyImagery[descendantLayerIndex]) {
return false;
}
}
} else if (lastFrameSelectionResult === TileSelectionResult.REFINED) {
stack.push(
descendant.southwestChild,
descendant.southeastChild,
descendant.northwestChild,
descendant.northeastChild,
);
}
}
return true;
}
/**
* Determines the priority for loading this tile. Lower priority values load sooner.
* @param {QuadtreeTile} tile The tile.
* @param {FrameState} frameState The frame state.
* @returns {number} The load priority value.
*/
computeTileLoadPriority(tile, frameState) {
const surfaceTile = /** @type {GlobeSurfaceTile} */ (tile.data);
if (surfaceTile === undefined) {
return 0.0;
}
const obb = surfaceTile.tileBoundingRegion.boundingVolume;
if (obb === undefined) {
return 0.0;
}
const cameraPosition = frameState.camera.positionWC;
const cameraDirection = frameState.camera.directionWC;
const tileDirection = Cartesian3.subtract(
obb.center,
cameraPosition,
tileDirectionScratch,
);
const magnitude = Cartesian3.magnitude(tileDirection);
if (magnitude < CesiumMath.EPSILON5) {
return 0.0;
}
Cartesian3.divideByScalar(tileDirection, magnitude, tileDirection);
return (
(1.0 - Cartesian3.dot(tileDirection, cameraDirection)) * tile._distance
);
}
/**
* Shows a specified tile in this frame. The provider can cause the tile to be shown by adding
* render commands to the commandList, or use any other method as appropriate. The tile is not
* expected to be visible next frame as well, unless this method is called next frame, too.
*
* @param {QuadtreeTile} tile The tile instance.
* @param {FrameState} frameState The state information of the current rendering frame.
*/
showTileThisFrame(tile, frameState) {
let readyTextureCount = 0;
const surfaceTile = /** @type {GlobeSurfaceTile} */ (tile.data);
const tileImageryCollection = surfaceTile.imagery;
for (let i = 0, len = tileImageryCollection.length; i < len; ++i) {
const tileImagery = tileImageryCollection[i];
if (
defined(tileImagery.readyImagery) &&
tileImagery.readyImagery.imageryLayer.alpha !== 0.0
) {
++readyTextureCount;
}
}
let tileSet = this._tilesToRenderByTextureCount[readyTextureCount];
if (!defined(tileSet)) {
tileSet = [];
this._tilesToRenderByTextureCount[readyTextureCount] = tileSet;
}
tileSet.push(tile);
if (!defined(surfaceTile.vertexArray)) {
this._hasFillTilesThisFrame = true;
} else {
this._hasLoadedTilesThisFrame = true;
}
const debug = this._debug;
++debug.tilesRendered;
debug.texturesRendered += readyTextureCount;
}
/**
* Gets the distance from the camera to the closest point on the tile. This is used for level-of-detail selection.
*
* @param {QuadtreeTile} tile The tile instance.
* @param {FrameState} frameState The state information of the current rendering frame.
*
* @returns {number} The distance from the camera to the closest point on the tile, in meters.
*/
computeDistanceToTile(tile, frameState) {
// The distance should be:
// 1. the actual distance to the tight-fitting bounding volume, or
// 2. a distance that is equal to or greater than the actual distance to the tight-fitting bounding volume.
//
// When we don't know the min/max heights for a tile, but we do know the min/max of an ancestor tile, we can
// build a tight-fitting bounding volume horizontally, but not vertically. The min/max heights from the
// ancestor will likely form a volume that is much bigger than it needs to be. This means that the volume may
// be deemed to be much closer to the camera than it really is, causing us to select tiles that are too detailed.
// Loading too-detailed tiles is super expensive, so we don't want to do that. We don't know where the child
// tile really lies within the parent range of heights, but we _do_ know the child tile can't be any closer than
// the ancestor height surface (min or max) that is _farthest away_ from the camera. So if we compute distance
// based on that conservative metric, we may end up loading tiles that are not detailed enough, but that's much
// better (faster) than loading tiles that are too detailed.
updateTileBoundingRegion(tile, this, frameState);
const surfaceTile = /** @type {GlobeSurfaceTile} */ (tile.data);
const boundingVolumeSourceTile = surfaceTile.boundingVolumeSourceTile;
if (boundingVolumeSourceTile === undefined) {
// Can't find any min/max heights anywhere? Ok, let's just say the
// tile is really far away so we'll load and render it rather than
// refining.
return 9999999999.0;
}
const tileBoundingRegion = surfaceTile.tileBoundingRegion;
const min = tileBoundingRegion.minimumHeight;
const max = tileBoundingRegion.maximumHeight;
if (surfaceTile.boundingVolumeSourceTile !== tile) {
const cameraHeight = frameState.camera.positionCartographic.height;
const distanceToMin = Math.abs(cameraHeight - min);
const distanceToMax = Math.abs(cameraHeight - max);
if (distanceToMin > distanceToMax) {
tileBoundingRegion.minimumHeight = min;
tileBoundingRegion.maximumHeight = min;
} else {
tileBoundingRegion.minimumHeight = max;
tileBoundingRegion.maximumHeight = max;
}
}
const result = tileBoundingRegion.distanceToCamera(frameState);
tileBoundingRegion.minimumHeight = min;
tileBoundingRegion.maximumHeight = max;
return result;
}
/**
* Returns true if this object was destroyed; otherwise, false.
* <br /><br />
* If this object was destroyed, it should not be used; calling any function other than
* <code>isDestroyed</code> will result in a {@link DeveloperError} exception.
*
* @returns {boolean} True if this object was destroyed; otherwise, false.
*
* @see GlobeSurfaceTileProvider#destroy
*/
isDestroyed() {
return false;
}
/**
* Destroys the WebGL resources held by this object. Destroying an object allows for deterministic
* release of WebGL resources, instead of relying on the garbage collector to destroy this object.
* <br /><br />
* Once an object is destroyed, it should not be used; calling any function other than
* <code>isDestroyed</code> will result in a {@link DeveloperError} exception. Therefore,
* assign the return value (<code>undefined</code>) to the object as done in the example.
*
* @exception {DeveloperError} This object was destroyed, i.e., destroy() was called.
*
*
* @example
* provider = provider && provider.destroy();
*
* @see GlobeSurfaceTileProvider#isDestroyed
*/
destroy() {
this._tileProvider = this._tileProvider && this._tileProvider.destroy();
this._clippingPlanes =
this._clippingPlanes && this._clippingPlanes.destroy();
this._clippingPolygons =
this._clippingPolygons && this._clippingPolygons.destroy();
this._removeLayerAddedListener =
this._removeLayerAddedListener && this._removeLayerAddedListener();
this._removeLayerRemovedListener =
this._removeLayerRemovedListener && this._removeLayerRemovedListener();
this._removeLayerMovedListener =
this._removeLayerMovedListener && this._removeLayerMovedListener();
this._removeLayerShownListener =
this._removeLayerShownListener && this._removeLayerShownListener();
return destroyObject(this);
}
_onLayerAdded(layer, index) {
if (this.isDestroyed()) {
return;
}
if (layer.show) {
const terrainProvider = this._terrainProvider;
const that = this;
const tileImageryUpdatedEvent = this._imageryLayersUpdatedEvent;
const reloadFunction = function () {
// Clear the layer's cache
layer._imageryCache = {};
that._quadtree.forEachLoadedTile(function (tile) {
// If this layer is still waiting to for the loaded callback, just return
if (defined(tile._loadedCallbacks[layer._layerIndex])) {
return;
}
let i;
// Figure out how many TileImageries we will need to remove and where to insert new ones
const tileImageryCollection = tile.data.imagery;
const length = tileImageryCollection.length;
let startIndex = -1;
let tileImageriesToFree = 0;
for (i = 0; i < length; ++i) {
const tileImagery = tileImageryCollection[i];
const imagery =
tileImagery.readyImagery ?? tileImagery.loadingImagery;
if (imagery.imageryLayer === layer) {
if (startIndex === -1) {
startIndex = i;
}
++tileImageriesToFree;
} else if (startIndex !== -1) {
// iterated past the section of TileImageries belonging to this layer, no need to continue.
break;
}
}
if (startIndex === -1) {
return;
}
// Insert immediately after existing TileImageries
const insertionPoint = startIndex + tileImageriesToFree;
// Create new TileImageries for all loaded tiles
if (
layer._createTileImagerySkeletons(
tile,
terrainProvider,
insertionPoint,
)
) {
// Add callback to remove old TileImageries when the new TileImageries are ready
tile._loadedCallbacks[layer._layerIndex] = getTileReadyCallback(
tileImageriesToFree,
layer,
terrainProvider,
);
tile.state = QuadtreeTileLoadState.LOADING;
}
});
};
if (layer.ready) {
const imageryProvider = layer.imageryProvider;
imageryProvider._reload = reloadFunction;
}
// create TileImageries for this layer for all previously loaded tiles
this._quadtree.forEachLoadedTile(function (tile) {
if (layer._createTileImagerySkeletons(tile, terrainProvider)) {
tile.state = QuadtreeTileLoadState.LOADING;
// Tiles that are not currently being rendered need to load the new layer before they're renderable.
// We don't mark the rendered tiles non-renderable, though, because that would make the globe disappear.
if (
tile.level !== 0 &&
(tile._lastSelectionResultFrame !==
that.quadtree._lastSelectionFrameNumber ||
tile._lastSelectionResult !== TileSelectionResult.RENDERED)
) {
tile.renderable = false;
}
}
});
this._layerOrderChanged = true;
tileImageryUpdatedEvent.raiseEvent();
}
}
_onLayerRemoved(layer, index) {
// destroy TileImagerys for this layer for all previously loaded tiles
this._quadtree.forEachLoadedTile(function (tile) {
const tileImageryCollection = tile.data.imagery;
let startIndex = -1;
let numDestroyed = 0;
for (let i = 0, len = tileImageryCollection.length; i < len; ++i) {
const tileImagery = tileImageryCollection[i];
let imagery = tileImagery.loadingImagery;
if (!defined(imagery)) {
imagery = tileImagery.readyImagery;
}
if (imagery.imageryLayer === layer) {
if (startIndex === -1) {
startIndex = i;
}
tileImagery.freeResources();
++numDestroyed;
} else if (startIndex !== -1) {
// iterated past the section of TileImagerys belonging to this layer, no need to continue.
break;
}
}
if (startIndex !== -1) {
tileImageryCollection.splice(startIndex, numDestroyed);
}
});
if (defined(layer.imageryProvider)) {
layer.imageryProvider._reload = undefined;
}
this._imageryLayersUpdatedEvent.raiseEvent();
}
_onLayerMoved(layer, newIndex, oldIndex) {
this._layerOrderChanged = true;
this._imageryLayersUpdatedEvent.raiseEvent();
}
_onLayerShownOrHidden(layer, index, show) {
if (show) {
this._onLayerAdded(layer, index);
} else {
this._onLayerRemoved(layer, index);
}
}
}
function sortTileImageryByLayerIndex(a, b) {
let aImagery = a.loadingImagery;
if (!defined(aImagery)) {
aImagery = a.readyImagery;
}
let bImagery = b.loadingImagery;
if (!defined(bImagery)) {
bImagery = b.readyImagery;
}
return aImagery.imageryLayer._layerIndex - bImagery.imageryLayer._layerIndex;
}
/**
* @param {GlobeSurfaceTileProvider} surface
* @param {FrameState} frameState
* @ignore
*/
function updateCredits(surface, frameState) {
const creditDisplay = frameState.creditDisplay;
const terrainProvider = surface._terrainProvider;
// @ts-expect-error Missing types.
if (defined(terrainProvider) && defined(terrainProvider.credit)) {
// @ts-expect-error Missing types.
creditDisplay.addCreditToNextFrame(terrainProvider.credit);
}
const imageryLayers = surface._imageryLayers;
// @ts-expect-error Missing types.
for (let i = 0, len = imageryLayers.length; i < len; ++i) {
const layer = imageryLayers.get(i);
// @ts-expect-error Missing types.
if (layer.ready && layer.show && defined(layer.imageryProvider.credit)) {
// @ts-expect-error Missing types.
creditDisplay.addCreditToNextFrame(layer.imageryProvider.credit);
}
}
}
/**
* @param {DrawCommand} command
* @param {FrameState} frameState
* @ignore
*/
function pushCommand(command, frameState) {
const globeTranslucencyState = frameState.globeTranslucencyState;
if (globeTranslucencyState.translucent) {
// @ts-expect-error Missing types.
const isBlendCommand = command.renderState.blending.enabled;
globeTranslucencyState.pushDerivedCommands(
command,
isBlendCommand,
frameState,
);
} else {
frameState.commandList.push(command);
}
}
const boundingSphereScratch = new BoundingSphere();
const rectangleIntersectionScratch = new Rectangle();
const splitCartographicLimitRectangleScratch = new Rectangle();
const rectangleCenterScratch = new Cartographic();
/**
* @param {Rectangle} tileRectangle
* @param {Rectangle} cartographicLimitRectangle
* @ignore
*/
function clipRectangleAntimeridian(tileRectangle, cartographicLimitRectangle) {
// cartographicLimitRectangle may span the IDL, but tiles never will.
if (cartographicLimitRectangle.west < cartographicLimitRectangle.east) {
return cartographicLimitRectangle;
}
const splitRectangle = Rectangle.clone(
cartographicLimitRectangle,
splitCartographicLimitRectangleScratch,
);
const tileCenter = Rectangle.center(tileRectangle, rectangleCenterScratch);
if (tileCenter.longitude > 0.0) {
splitRectangle.east = CesiumMath.PI;
} else {
splitRectangle.west = -CesiumMath.PI;
}
return splitRectangle;
}
/**
* @param {GlobeSurfaceTileProvider} tileProvider
* @param {FrameState} frameState
* @ignore
*/
function isUndergroundVisible(tileProvider, frameState) {
if (frameState.cameraUnderground) {
return true;
}
if (frameState.globeTranslucencyState.translucent) {
return true;
}
if (tileProvider.backFaceCulling) {
return false;
}
const clippingPlanes = tileProvider._clippingPlanes;
if (defined(clippingPlanes) && clippingPlanes.enabled) {
return true;
}
const clippingPolygons = tileProvider._clippingPolygons;
if (defined(clippingPolygons) && clippingPolygons.enabled) {
return true;
}
if (
!Rectangle.equals(
tileProvider.cartographicLimitRectangle,
Rectangle.MAX_VALUE,
)
) {
return true;
}
return false;
}
const readyImageryScratch = [];
const canRenderTraversalStack = [];
const tileDirectionScratch = new Cartesian3();
const modifiedModelViewScratch = new Matrix4();
const modifiedModelViewProjectionScratch = new Matrix4();
const tileRectangleScratch = new Cartesian4();
const localizedCartographicLimitRectangleScratch = new Cartesian4();
const localizedTranslucencyRectangleScratch = new Cartesian4();
const rtcScratch = new Cartesian3();
const centerEyeScratch = new Cartesian3();
const southwestScratch = new Cartesian3();
const northeastScratch = new Cartesian3();
const cornerPositionsScratch = [
new Cartesian3(),
new Cartesian3(),
new Cartesian3(),
new Cartesian3(),
];
function computeOccludeePoint(
tileProvider,
center,
rectangle,
minimumHeight,
maximumHeight,
result,
) {
/** @type {EllipsoidalOccluder} */
const ellipsoidalOccluder = tileProvider.quadtree._occluders.ellipsoid;
// @ts-expect-error Missing types.
const ellipsoid = ellipsoidalOccluder.ellipsoid;
const cornerPositions = cornerPositionsScratch;
Cartesian3.fromRadians(
rectangle.west,
rectangle.south,
maximumHeigh