UNPKG

cesium

Version:

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

1,112 lines (964 loc) 90.8 kB
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 defaultValue from '../Core/defaultValue.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 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 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 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'; /** * Provides quadtree tiles representing the surface of the globe. This type is intended to be used * with {@link QuadtreePrimitive}. * * @alias GlobeSurfaceTileProvider * @constructor * * @param {TerrainProvider} options.terrainProvider The terrain provider that describes the surface geometry. * @param {ImageryLayerCollection} option.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. * * @private */ function GlobeSurfaceTileProvider(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.oceanNormalMap = undefined; this.zoomedOutOceanSpecularIntensity = 0.5; this.enableLighting = false; this.dynamicAtmosphereLighting = false; this.dynamicAtmosphereLightingFromSun = false; this.showGroundAtmosphere = false; this.shadows = ShadowMode.RECEIVE_ONLY; /** * 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._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._imageryLayers.layerAdded.addEventListener(GlobeSurfaceTileProvider.prototype._onLayerAdded, this); this._imageryLayers.layerRemoved.addEventListener(GlobeSurfaceTileProvider.prototype._onLayerRemoved, this); this._imageryLayers.layerMoved.addEventListener(GlobeSurfaceTileProvider.prototype._onLayerMoved, this); this._imageryLayers.layerShownOrHidden.addEventListener(GlobeSurfaceTileProvider.prototype._onLayerShownOrHidden, this); this._imageryLayersUpdatedEvent = new Event(); this._layerOrderChanged = false; this._tilesToRenderByTextureCount = []; this._drawCommands = []; this._uniformMaps = []; this._usedDrawCommands = 0; this._vertexArraysToDestroy = []; this._debug = { wireframe : false, boundingSphereTile : undefined }; 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} * @private */ this._clippingPlanes = 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; } Object.defineProperties(GlobeSurfaceTileProvider.prototype, { /** * Gets or sets the color of the globe when no imagery is available. * @memberof GlobeSurfaceTileProvider.prototype * @type {Color} */ baseColor : { get : function() { return this._baseColor; }, set : function(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}. * @memberof GlobeSurfaceTileProvider.prototype * @type {QuadtreePrimitive} */ quadtree : { get : function() { return this._quadtree; }, set : function(value) { //>>includeStart('debug', pragmas.debug); if (!defined(value)) { throw new DeveloperError('value is required.'); } //>>includeEnd('debug'); this._quadtree = value; } }, /** * Gets a value indicating whether or not the provider is ready for use. * @memberof GlobeSurfaceTileProvider.prototype * @type {Boolean} */ ready : { get : function() { return this._terrainProvider.ready && (this._imageryLayers.length === 0 || this._imageryLayers.get(0).imageryProvider.ready); } }, /** * Gets the tiling scheme used by the provider. This property should * not be accessed before {@link GlobeSurfaceTileProvider#ready} returns true. * @memberof GlobeSurfaceTileProvider.prototype * @type {TilingScheme} */ tilingScheme : { get : function() { 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}. * @memberof GlobeSurfaceTileProvider.prototype * @type {Event} */ errorEvent : { get : function() { return this._errorEvent; } }, /** * Gets an event that is raised when an imagery layer is added, shown, hidden, moved, or removed. * @memberof GlobeSurfaceTileProvider.prototype * @type {Event} */ imageryLayersUpdatedEvent : { get : function() { return this._imageryLayersUpdatedEvent; } }, /** * Gets or sets the terrain provider that describes the surface geometry. * @memberof GlobeSurfaceTileProvider.prototype * @type {TerrainProvider} */ terrainProvider : { get : function() { return this._terrainProvider; }, set : function(terrainProvider) { if (this._terrainProvider === terrainProvider) { return; } //>>includeStart('debug', pragmas.debug); if (!defined(terrainProvider)) { throw new DeveloperError('terrainProvider is required.'); } //>>includeEnd('debug'); this._terrainProvider = terrainProvider; if (defined(this._quadtree)) { this._quadtree.invalidateAllTiles(); } } }, /** * The {@link ClippingPlaneCollection} used to selectively disable rendering the tileset. * * @type {ClippingPlaneCollection} * * @private */ clippingPlanes : { get : function() { return this._clippingPlanes; }, set : function(value) { ClippingPlaneCollection.setOwner(value, this, '_clippingPlanes'); } } }); function sortTileImageryByLayerIndex(a, b) { var aImagery = a.loadingImagery; if (!defined(aImagery)) { aImagery = a.readyImagery; } var bImagery = b.loadingImagery; if (!defined(bImagery)) { bImagery = b.readyImagery; } return aImagery.imageryLayer._layerIndex - bImagery.imageryLayer._layerIndex; } /** * Make updates to the tile provider that are not involved in rendering. Called before the render update cycle. */ GlobeSurfaceTileProvider.prototype.update = function(frameState) { // update collection: imagery indices, base layers, raise layer show/hide event this._imageryLayers._update(); }; function updateCredits(surface, frameState) { var creditDisplay = frameState.creditDisplay; if (surface._terrainProvider.ready && defined(surface._terrainProvider.credit)) { creditDisplay.addCredit(surface._terrainProvider.credit); } var imageryLayers = surface._imageryLayers; for (var i = 0, len = imageryLayers.length; i < len; ++i) { var imageryProvider = imageryLayers.get(i).imageryProvider; if (imageryProvider.ready && defined(imageryProvider.credit)) { creditDisplay.addCredit(imageryProvider.credit); } } } /** * Called at the beginning of each render frame, before {@link QuadtreeTileProvider#showTileThisFrame} * @param {FrameState} frameState The frame state. */ GlobeSurfaceTileProvider.prototype.initialize = function(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(function(tile) { tile.data.imagery.sort(sortTileImageryByLayerIndex); }); } // Add credits for terrain and imagery providers. updateCredits(this, frameState); var vertexArraysToDestroy = this._vertexArraysToDestroy; var length = vertexArraysToDestroy.length; for (var 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. */ GlobeSurfaceTileProvider.prototype.beginUpdate = function(frameState) { var tilesToRenderByTextureCount = this._tilesToRenderByTextureCount; for (var i = 0, len = tilesToRenderByTextureCount.length; i < len; ++i) { var tiles = tilesToRenderByTextureCount[i]; if (defined(tiles)) { tiles.length = 0; } } // update clipping planes var clippingPlanes = this._clippingPlanes; if (defined(clippingPlanes) && clippingPlanes.enabled) { clippingPlanes.update(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. */ GlobeSurfaceTileProvider.prototype.endUpdate = function(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 }); } if (!this.backFaceCulling && !defined(this._disableCullingRenderState)) { var rs = clone(this._renderState, true); rs.cull.enabled = false; this._disableCullingRenderState = RenderState.fromCache(rs); rs = 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); } // Add the tile render commands to the command list, sorted by texture count. var tilesToRenderByTextureCount = this._tilesToRenderByTextureCount; for (var textureCountIndex = 0, textureCountLength = tilesToRenderByTextureCount.length; textureCountIndex < textureCountLength; ++textureCountIndex) { var tilesToRender = tilesToRenderByTextureCount[textureCountIndex]; if (!defined(tilesToRender)) { continue; } for (var tileIndex = 0, tileLength = tilesToRender.length; tileIndex < tileLength; ++tileIndex) { var tile = tilesToRender[tileIndex]; var tileBoundingRegion = tile.data.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. */ GlobeSurfaceTileProvider.prototype.updateForPick = function(frameState) { // Add the tile pick commands from the tiles drawn last frame. var drawCommands = this._drawCommands; for (var i = 0, length = this._usedDrawCommands; i < length; ++i) { frameState.commandList.push(drawCommands[i]); } }; /** * Cancels any imagery re-projections in the queue. */ GlobeSurfaceTileProvider.prototype.cancelReprojections = function() { this._imageryLayers.cancelReprojections(); }; /** * Gets the maximum geometric error allowed in a tile at a given level, in meters. This function should not be * called before {@link GlobeSurfaceTileProvider#ready} returns true. * * @param {Number} level The tile level for which to get the maximum geometric error. * @returns {Number} The maximum geometric error in meters. */ GlobeSurfaceTileProvider.prototype.getLevelMaximumGeometricError = function(level) { 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}. This function should * not be called before {@link GlobeSurfaceTileProvider#ready} returns true. * * @param {FrameState} frameState The frame state. * @param {QuadtreeTile} tile The tile to load. * * @exception {DeveloperError} <code>loadTile</code> must not be called before the tile provider is ready. */ GlobeSurfaceTileProvider.prototype.loadTile = function(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. var surfaceTile = tile.data; var terrainOnly = true; var 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._vertexArraysToDestroy, terrainOnly); surfaceTile = tile.data; if (terrainOnly && terrainStateBefore !== tile.data.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, this.quadtree.occluders) && surfaceTile.boundingVolumeSourceTile === tile) { terrainOnly = false; GlobeSurfaceTile.processStateMachine(tile, frameState, this.terrainProvider, this._imageryLayers, this._vertexArraysToDestroy, terrainOnly); } } }; var boundingSphereScratch = new BoundingSphere(); var rectangleIntersectionScratch = new Rectangle(); var splitCartographicLimitRectangleScratch = new Rectangle(); var rectangleCenterScratch = new Cartographic(); // cartographicLimitRectangle may span the IDL, but tiles never will. function clipRectangleAntimeridian(tileRectangle, cartographicLimitRectangle) { if (cartographicLimitRectangle.west < cartographicLimitRectangle.east) { return cartographicLimitRectangle; } var splitRectangle = Rectangle.clone(cartographicLimitRectangle, splitCartographicLimitRectangleScratch); var tileCenter = Rectangle.center(tileRectangle, rectangleCenterScratch); if (tileCenter.longitude > 0.0) { splitRectangle.east = CesiumMath.PI; } else { splitRectangle.west = -CesiumMath.PI; } return splitRectangle; } /** * 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} The visibility of the tile. */ GlobeSurfaceTileProvider.prototype.computeTileVisibility = function(tile, frameState, occluders) { var distance = this.computeDistanceToTile(tile, frameState); tile._distance = distance; if (frameState.fog.enabled) { if (CesiumMath.fog(distance, frameState.fog.density) >= 1.0) { // Tile is completely in fog so return that it is not visible. return Visibility.NONE; } } var surfaceTile = tile.data; var 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; } var cullingVolume = frameState.cullingVolume; var boundingVolume = surfaceTile.orientedBoundingBox; if (!defined(boundingVolume) && defined(surfaceTile.renderedMesh)) { boundingVolume = surfaceTile.renderedMesh.boundingSphere3D; } // Check if the tile is outside the limit area in cartographic space surfaceTile.clippedByBoundaries = false; var clippedCartographicLimitRectangle = clipRectangleAntimeridian(tile.rectangle, this.cartographicLimitRectangle); var 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(surfaceTile.renderedMesh.boundingSphere3D, boundingVolume, boundingVolume); } } if (!defined(boundingVolume)) { return Intersect.INTERSECTING; } var clippingPlanes = this._clippingPlanes; if (defined(clippingPlanes) && clippingPlanes.enabled) { var planeIntersection = clippingPlanes.computeIntersectionWithBoundingVolume(boundingVolume); tile.isClipped = (planeIntersection !== Intersect.INSIDE); if (planeIntersection === Intersect.OUTSIDE) { return Visibility.NONE; } } var intersection = cullingVolume.computeVisibility(boundingVolume); if (intersection === Intersect.OUTSIDE) { return Visibility.NONE; } var ortho3D = frameState.mode === SceneMode.SCENE3D && frameState.camera.frustum instanceof OrthographicFrustum; if (frameState.mode === SceneMode.SCENE3D && !ortho3D && defined(occluders)) { var occludeePointInScaledSpace = surfaceTile.occludeePointInScaledSpace; if (!defined(occludeePointInScaledSpace)) { return intersection; } if (occluders.ellipsoid.isScaledSpacePointVisiblePossiblyUnderEllipsoid(occludeePointInScaledSpace, tileBoundingRegion.minimumHeight)) { return intersection; } return Visibility.NONE; } return intersection; }; /** * 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. */ GlobeSurfaceTileProvider.prototype.canRefine = function(tile) { // 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(tile.data.terrainData)) { return true; } var childAvailable = this.terrainProvider.getTileDataAvailable(tile.x * 2, tile.y * 2, tile.level + 1); return childAvailable !== undefined; }; var readyImageryScratch = []; var canRenderTraversalStack = []; /** * 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. */ GlobeSurfaceTileProvider.prototype.canRenderWithoutLosingDetail = function(tile, frameState) { var surfaceTile = tile.data; var readyImagery = readyImageryScratch; readyImagery.length = this._imageryLayers.length; var terrainReady = false; var initialImageryState = false; var 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; } var i; var 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) { var tileImagery = imagery[i]; var loadingImagery = tileImagery.loadingImagery; var isReady = !defined(loadingImagery) || loadingImagery.state === ImageryState.FAILED || loadingImagery.state === ImageryState.INVALID; var 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]; } } var lastFrame = this.quadtree._lastSelectionFrameNumber; // Traverse the descendants looking for one with terrain or imagery that is not loaded on this tile. var stack = canRenderTraversalStack; stack.length = 0; stack.push(tile.southwestChild, tile.southeastChild, tile.northwestChild, tile.northeastChild); while (stack.length > 0) { var descendant = stack.pop(); var lastFrameSelectionResult = descendant._lastSelectionResultFrame === lastFrame ? descendant._lastSelectionResult : TileSelectionResult.NONE; if (lastFrameSelectionResult === TileSelectionResult.RENDERED) { var 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; } var descendantImagery = descendant.data.imagery; for (i = 0, len = descendantImagery.length; i < len; ++i) { var descendantTileImagery = descendantImagery[i]; var descendantLoadingImagery = descendantTileImagery.loadingImagery; var descendantIsReady = !defined(descendantLoadingImagery) || descendantLoadingImagery.state === ImageryState.FAILED || descendantLoadingImagery.state === ImageryState.INVALID; var 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; }; var tileDirectionScratch = new Cartesian3(); /** * 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. */ GlobeSurfaceTileProvider.prototype.computeTileLoadPriority = function(tile, frameState) { var surfaceTile = tile.data; if (surfaceTile === undefined) { return 0.0; } var obb = surfaceTile.orientedBoundingBox; if (obb === undefined) { return 0.0; } var cameraPosition = frameState.camera.positionWC; var cameraDirection = frameState.camera.directionWC; var tileDirection = Cartesian3.subtract(obb.center, cameraPosition, tileDirectionScratch); var 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; }; var modifiedModelViewScratch = new Matrix4(); var modifiedModelViewProjectionScratch = new Matrix4(); var tileRectangleScratch = new Cartesian4(); var localizedCartographicLimitRectangleScratch = new Cartesian4(); var rtcScratch = new Cartesian3(); var centerEyeScratch = new Cartesian3(); var southwestScratch = new Cartesian3(); var northeastScratch = new Cartesian3(); /** * 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. */ GlobeSurfaceTileProvider.prototype.showTileThisFrame = function(tile, frameState) { var readyTextureCount = 0; var tileImageryCollection = tile.data.imagery; for (var i = 0, len = tileImageryCollection.length; i < len; ++i) { var tileImagery = tileImageryCollection[i]; if (defined(tileImagery.readyImagery) && tileImagery.readyImagery.imageryLayer.alpha !== 0.0) { ++readyTextureCount; } } var tileSet = this._tilesToRenderByTextureCount[readyTextureCount]; if (!defined(tileSet)) { tileSet = []; this._tilesToRenderByTextureCount[readyTextureCount] = tileSet; } tileSet.push(tile); var surfaceTile = tile.data; if (!defined(surfaceTile.vertexArray)) { this._hasFillTilesThisFrame = true; } else { this._hasLoadedTilesThisFrame = true; } var debug = this._debug; ++debug.tilesRendered; debug.texturesRendered += readyTextureCount; }; var cornerPositionsScratch = [new Cartesian3(), new Cartesian3(), new Cartesian3(), new Cartesian3()]; function computeOccludeePoint(tileProvider, center, rectangle, minimumHeight, maximumHeight, result) { var ellipsoidalOccluder = tileProvider.quadtree._occluders.ellipsoid; var ellipsoid = ellipsoidalOccluder.ellipsoid; var cornerPositions = cornerPositionsScratch; Cartesian3.fromRadians(rectangle.west, rectangle.south, maximumHeight, ellipsoid, cornerPositions[0]); Cartesian3.fromRadians(rectangle.east, rectangle.south, maximumHeight, ellipsoid, cornerPositions[1]); Cartesian3.fromRadians(rectangle.west, rectangle.north, maximumHeight, ellipsoid, cornerPositions[2]); Cartesian3.fromRadians(rectangle.east, rectangle.north, maximumHeight, ellipsoid, cornerPositions[3]); return ellipsoidalOccluder.computeHorizonCullingPointPossiblyUnderEllipsoid(center, cornerPositions, minimumHeight, result); } /** * 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. */ GlobeSurfaceTileProvider.prototype.computeDistanceToTile = function(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 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. var heightSource = updateTileBoundingRegion(tile, this.terrainProvider, frameState); var surfaceTile = tile.data; var tileBoundingRegion = surfaceTile.tileBoundingRegion; if (heightSource === 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; } else if (surfaceTile.boundingVolumeSourceTile !== heightSource) { // Heights are from a new source tile, so update the bounding volume. surfaceTile.boundingVolumeSourceTile = heightSource; var rectangle = tile.rectangle; if (defined(rectangle)) { surfaceTile.orientedBoundingBox = OrientedBoundingBox.fromRectangle( tile.rectangle, tileBoundingRegion.minimumHeight, tileBoundingRegion.maximumHeight, tile.tilingScheme.ellipsoid, surfaceTile.orientedBoundingBox); surfaceTile.occludeePointInScaledSpace = computeOccludeePoint(this, surfaceTile.orientedBoundingBox.center, tile.rectangle, tileBoundingRegion.minimumHeight, tileBoundingRegion.maximumHeight, surfaceTile.occludeePointInScaledSpace); } } var min = tileBoundingRegion.minimumHeight; var max = tileBoundingRegion.maximumHeight; if (surfaceTile.boundingVolumeSourceTile !== tile) { var cameraHeight = frameState.camera.positionCartographic.height; var distanceToMin = Math.abs(cameraHeight - min); var distanceToMax = Math.abs(cameraHeight - max); if (distanceToMin > distanceToMax) { tileBoundingRegion.minimumHeight = min; tileBoundingRegion.maximumHeight = min; } else { tileBoundingRegion.minimumHeight = max; tileBoundingRegion.maximumHeight = max; } } var result = tileBoundingRegion.distanceToCamera(frameState); tileBoundingRegion.minimumHeight = min; tileBoundingRegion.maximumHeight = max; return result; }; function updateTileBoundingRegion(tile, terrainProvider, frameState) { var surfaceTile = tile.data; if (surfaceTile === undefined) { surfaceTile = tile.data = new GlobeSurfaceTile(); } if (surfaceTile.tileBoundingRegion === undefined) { surfaceTile.tileBoundingRegion = new TileBoundingRegion({ computeBoundingVolumes : false, rectangle : tile.rectangle, ellipsoid : tile.tilingScheme.ellipsoid, minimumHeight : 0.0, maximumHeight : 0.0 }); } var terrainData = surfaceTile.terrainData; var mesh = surfaceTile.mesh; var tileBoundingRegion = surfaceTile.tileBoundingRegion; if (mesh !== undefined && mesh.minimumHeight !== undefined && mesh.maximumHeight !== undefined) { // We have tight-fitting min/max heights from the mesh. tileBoundingRegion.minimumHeight = mesh.minimumHeight; tileBoundingRegion.maximumHeight = mesh.maximumHeight; return tile; } if (terrainData !== undefined && terrainData._minimumHeight !== undefined && terrainData._maximumHeight !== undefined) { // We have tight-fitting min/max heights from the terrain data. tileBoundingRegion.minimumHeight = terrainData._minimumHeight * frameState.terrainExaggeration; tileBoundingRegion.maximumHeight = terrainData._maximumHeight * frameState.terrainExaggeration; return tile; } // No accurate min/max heights available, so we're stuck with min/max heights from an ancestor tile. tileBoundingRegion.minimumHeight = Number.NaN; tileBoundingRegion.maximumHeight = Number.NaN; var ancestor = tile.parent; while (ancestor !== undefined) { var ancestorSurfaceTile = ancestor.data; if (ancestorSurfaceTile !== undefined) { var ancestorMesh = ancestorSurfaceTile.mesh; if (ancestorMesh !== undefined && ancestorMesh.minimumHeight !== undefined && ancestorMesh.maximumHeight !== undefined) { tileBoundingRegion.minimumHeight = ancestorMesh.minimumHeight; tileBoundingRegion.maximumHeight = ancestorMesh.maximumHeight; return ancestor; } var ancestorTerrainData = ancestorSurfaceTile.terrainData; if (ancestorTerrainData !== undefined && ancestorTerrainData._minimumHeight !== undefined && ancestorTerrainData._maximumHeight !== undefined) { tileBoundingRegion.minimumHeight = ancestorTerrainData._minimumHeight * frameState.terrainExaggeration; tileBoundingRegion.maximumHeight = ancestorTerrainData._maximumHeight * frameState.terrainExaggeration; return ancestor; } } ancestor = ancestor.parent; } return undefined; } /** * 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 */ GlobeSurfaceTileProvider.prototype.isDestroyed = function() { 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(); * * @see GlobeSurfaceTileProvider#isDestroyed */ GlobeSurfaceTileProvider.prototype.destroy = function() { this._tileProvider = this._tileProvider && this._tileProvider.destroy(); this._clippingPlanes = this._clippingPlanes && this._clippingPlanes.destroy(); return destroyObject(this); }; function getTileReadyCallback(tileImageriesToFree, layer, terrainProvider) { return function(tile) { var tileImagery; var imagery; var startIndex = -1; var tileImageryCollection = tile.data.imagery; var length = tileImageryCollection.length; var i; for (i = 0; i < length; ++i) { tileImagery = tileImageryCollection[i]; imagery = defaultValue(tileImagery.readyImagery, tileImagery.loadingImagery); if (imagery.imageryLayer === layer) { startIndex = i; break; } } if (startIndex !== -1) { var endIndex = startIndex + tileImageriesToFree; tileImagery = tileImageryCollection[endIndex]; imagery = defined(tileImagery) ? defaultValue(tileImagery.readyImagery, tileImagery.loadingImagery) : undefined; if (!defined(imagery) || imagery.imageryLayer !== layer) { // Return false to keep the callback if we have to wait on the skeletons // Return true to remove the callback if something went wrong return !(layer._createTileImagerySkeletons(tile, terrainProvider, endIndex)); } for (i = startIndex; i < endIndex; ++i) { tileImageryCollection[i].freeResources(); } tileImageryCollection.splice(startIndex, tileImageriesToFree); } return true; // Everything is done, so remove the callback }; } GlobeSurfaceTileProvider.prototype._onLayerAdded = function(layer, index) { if (layer.show) { var terrainProvider = this._terrainProvider; var that = this; var imageryProvider = layer.imageryProvider; var tileImageryUpdatedEvent = this._imageryLayersUpdatedEvent; imageryProvider._reload = 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; } var i; // Figure out how many TileImageries we will need to remove and where to insert new ones var tileImageryCollection = tile.data.imagery; var length = tileImageryCollection.length; var startIndex = -1; var tileImageriesToFree = 0; for (i = 0; i < length; ++i) { var tileImagery = tileImageryCollection[i]; var imagery = defaultValue(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 var 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; } }); }; // 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)) {