904 lines
38 KiB
JavaScript
904 lines
38 KiB
JavaScript
|
|
define([
|
||
|
|
'../Core/Cartesian3',
|
||
|
|
'../Core/Cartographic',
|
||
|
|
'../Core/defaultValue',
|
||
|
|
'../Core/defined',
|
||
|
|
'../Core/defineProperties',
|
||
|
|
'../Core/DeveloperError',
|
||
|
|
'../Core/Event',
|
||
|
|
'../Core/getTimestamp',
|
||
|
|
'../Core/Math',
|
||
|
|
'../Core/OrthographicFrustum',
|
||
|
|
'../Core/Ray',
|
||
|
|
'../Core/Rectangle',
|
||
|
|
'../Core/Visibility',
|
||
|
|
'./QuadtreeOccluders',
|
||
|
|
'./QuadtreeTile',
|
||
|
|
'./QuadtreeTileLoadState',
|
||
|
|
'./SceneMode',
|
||
|
|
'./TileReplacementQueue'
|
||
|
|
], function(
|
||
|
|
Cartesian3,
|
||
|
|
Cartographic,
|
||
|
|
defaultValue,
|
||
|
|
defined,
|
||
|
|
defineProperties,
|
||
|
|
DeveloperError,
|
||
|
|
Event,
|
||
|
|
getTimestamp,
|
||
|
|
CesiumMath,
|
||
|
|
OrthographicFrustum,
|
||
|
|
Ray,
|
||
|
|
Rectangle,
|
||
|
|
Visibility,
|
||
|
|
QuadtreeOccluders,
|
||
|
|
QuadtreeTile,
|
||
|
|
QuadtreeTileLoadState,
|
||
|
|
SceneMode,
|
||
|
|
TileReplacementQueue) {
|
||
|
|
'use strict';
|
||
|
|
|
||
|
|
/**
|
||
|
|
* Renders massive sets of data by utilizing level-of-detail and culling. The globe surface is divided into
|
||
|
|
* a quadtree of tiles with large, low-detail tiles at the root and small, high-detail tiles at the leaves.
|
||
|
|
* The set of tiles to render is selected by projecting an estimate of the geometric error in a tile onto
|
||
|
|
* the screen to estimate screen-space error, in pixels, which must be below a user-specified threshold.
|
||
|
|
* The actual content of the tiles is arbitrary and is specified using a {@link QuadtreeTileProvider}.
|
||
|
|
*
|
||
|
|
* @alias QuadtreePrimitive
|
||
|
|
* @constructor
|
||
|
|
* @private
|
||
|
|
*
|
||
|
|
* @param {QuadtreeTileProvider} options.tileProvider The tile provider that loads, renders, and estimates
|
||
|
|
* the distance to individual tiles.
|
||
|
|
* @param {Number} [options.maximumScreenSpaceError=2] The maximum screen-space error, in pixels, that is allowed.
|
||
|
|
* A higher maximum error will render fewer tiles and improve performance, while a lower
|
||
|
|
* value will improve visual quality.
|
||
|
|
* @param {Number} [options.tileCacheSize=100] The maximum number of tiles that will be retained in the tile cache.
|
||
|
|
* Note that tiles will never be unloaded if they were used for rendering the last
|
||
|
|
* frame, so the actual number of resident tiles may be higher. The value of
|
||
|
|
* this property will not affect visual quality.
|
||
|
|
*/
|
||
|
|
function QuadtreePrimitive(options) {
|
||
|
|
//>>includeStart('debug', pragmas.debug);
|
||
|
|
if (!defined(options) || !defined(options.tileProvider)) {
|
||
|
|
throw new DeveloperError('options.tileProvider is required.');
|
||
|
|
}
|
||
|
|
if (defined(options.tileProvider.quadtree)) {
|
||
|
|
throw new DeveloperError('A QuadtreeTileProvider can only be used with a single QuadtreePrimitive');
|
||
|
|
}
|
||
|
|
//>>includeEnd('debug');
|
||
|
|
|
||
|
|
this._tileProvider = options.tileProvider;
|
||
|
|
this._tileProvider.quadtree = this;
|
||
|
|
|
||
|
|
this._debug = {
|
||
|
|
enableDebugOutput : false,
|
||
|
|
|
||
|
|
maxDepth : 0,
|
||
|
|
tilesVisited : 0,
|
||
|
|
tilesCulled : 0,
|
||
|
|
tilesRendered : 0,
|
||
|
|
tilesWaitingForChildren : 0,
|
||
|
|
|
||
|
|
lastMaxDepth : -1,
|
||
|
|
lastTilesVisited : -1,
|
||
|
|
lastTilesCulled : -1,
|
||
|
|
lastTilesRendered : -1,
|
||
|
|
lastTilesWaitingForChildren : -1,
|
||
|
|
|
||
|
|
suspendLodUpdate : false
|
||
|
|
};
|
||
|
|
|
||
|
|
var tilingScheme = this._tileProvider.tilingScheme;
|
||
|
|
var ellipsoid = tilingScheme.ellipsoid;
|
||
|
|
|
||
|
|
this._tilesToRender = [];
|
||
|
|
this._tileLoadQueueHigh = []; // high priority tiles are preventing refinement
|
||
|
|
this._tileLoadQueueMedium = []; // medium priority tiles are being rendered
|
||
|
|
this._tileLoadQueueLow = []; // low priority tiles were refined past or are non-visible parts of quads.
|
||
|
|
this._tileReplacementQueue = new TileReplacementQueue();
|
||
|
|
this._levelZeroTiles = undefined;
|
||
|
|
this._loadQueueTimeSlice = 5.0;
|
||
|
|
this._tilesInvalidated = false;
|
||
|
|
|
||
|
|
this._addHeightCallbacks = [];
|
||
|
|
this._removeHeightCallbacks = [];
|
||
|
|
|
||
|
|
this._tileToUpdateHeights = [];
|
||
|
|
this._lastTileIndex = 0;
|
||
|
|
this._updateHeightsTimeSlice = 2.0;
|
||
|
|
|
||
|
|
/**
|
||
|
|
* Gets or sets the maximum screen-space error, in pixels, that is allowed.
|
||
|
|
* A higher maximum error will render fewer tiles and improve performance, while a lower
|
||
|
|
* value will improve visual quality.
|
||
|
|
* @type {Number}
|
||
|
|
* @default 2
|
||
|
|
*/
|
||
|
|
this.maximumScreenSpaceError = defaultValue(options.maximumScreenSpaceError, 2);
|
||
|
|
|
||
|
|
/**
|
||
|
|
* Gets or sets the maximum number of tiles that will be retained in the tile cache.
|
||
|
|
* Note that tiles will never be unloaded if they were used for rendering the last
|
||
|
|
* frame, so the actual number of resident tiles may be higher. The value of
|
||
|
|
* this property will not affect visual quality.
|
||
|
|
* @type {Number}
|
||
|
|
* @default 100
|
||
|
|
*/
|
||
|
|
this.tileCacheSize = defaultValue(options.tileCacheSize, 100);
|
||
|
|
|
||
|
|
this._occluders = new QuadtreeOccluders({
|
||
|
|
ellipsoid : ellipsoid
|
||
|
|
});
|
||
|
|
|
||
|
|
this._tileLoadProgressEvent = new Event();
|
||
|
|
this._lastTileLoadQueueLength = 0;
|
||
|
|
}
|
||
|
|
|
||
|
|
defineProperties(QuadtreePrimitive.prototype, {
|
||
|
|
/**
|
||
|
|
* Gets the provider of {@link QuadtreeTile} instances for this quadtree.
|
||
|
|
* @type {QuadtreeTile}
|
||
|
|
* @memberof QuadtreePrimitive.prototype
|
||
|
|
*/
|
||
|
|
tileProvider : {
|
||
|
|
get : function() {
|
||
|
|
return this._tileProvider;
|
||
|
|
}
|
||
|
|
},
|
||
|
|
/**
|
||
|
|
* Gets an event that's raised when the length of the tile load queue has changed since the last render frame. When the load queue is empty,
|
||
|
|
* all terrain and imagery for the current view have been loaded. The event passes the new length of the tile load queue.
|
||
|
|
*
|
||
|
|
* @memberof QuadtreePrimitive.prototype
|
||
|
|
* @type {Event}
|
||
|
|
*/
|
||
|
|
tileLoadProgressEvent : {
|
||
|
|
get : function() {
|
||
|
|
return this._tileLoadProgressEvent;
|
||
|
|
}
|
||
|
|
}
|
||
|
|
});
|
||
|
|
|
||
|
|
/**
|
||
|
|
* Invalidates and frees all the tiles in the quadtree. The tiles must be reloaded
|
||
|
|
* before they can be displayed.
|
||
|
|
*
|
||
|
|
* @memberof QuadtreePrimitive
|
||
|
|
*/
|
||
|
|
QuadtreePrimitive.prototype.invalidateAllTiles = function() {
|
||
|
|
this._tilesInvalidated = true;
|
||
|
|
};
|
||
|
|
|
||
|
|
function invalidateAllTiles(primitive) {
|
||
|
|
// Clear the replacement queue
|
||
|
|
var replacementQueue = primitive._tileReplacementQueue;
|
||
|
|
replacementQueue.head = undefined;
|
||
|
|
replacementQueue.tail = undefined;
|
||
|
|
replacementQueue.count = 0;
|
||
|
|
|
||
|
|
clearTileLoadQueue(primitive);
|
||
|
|
|
||
|
|
// Free and recreate the level zero tiles.
|
||
|
|
var levelZeroTiles = primitive._levelZeroTiles;
|
||
|
|
if (defined(levelZeroTiles)) {
|
||
|
|
for (var i = 0; i < levelZeroTiles.length; ++i) {
|
||
|
|
var tile = levelZeroTiles[i];
|
||
|
|
var customData = tile.customData;
|
||
|
|
var customDataLength = customData.length;
|
||
|
|
|
||
|
|
for (var j = 0; j < customDataLength; ++j) {
|
||
|
|
var data = customData[j];
|
||
|
|
data.level = 0;
|
||
|
|
primitive._addHeightCallbacks.push(data);
|
||
|
|
}
|
||
|
|
|
||
|
|
levelZeroTiles[i].freeResources();
|
||
|
|
}
|
||
|
|
}
|
||
|
|
|
||
|
|
primitive._levelZeroTiles = undefined;
|
||
|
|
|
||
|
|
primitive._tileProvider.cancelReprojections();
|
||
|
|
}
|
||
|
|
|
||
|
|
/**
|
||
|
|
* Invokes a specified function for each {@link QuadtreeTile} that is partially
|
||
|
|
* or completely loaded.
|
||
|
|
*
|
||
|
|
* @param {Function} tileFunction The function to invoke for each loaded tile. The
|
||
|
|
* function is passed a reference to the tile as its only parameter.
|
||
|
|
*/
|
||
|
|
QuadtreePrimitive.prototype.forEachLoadedTile = function(tileFunction) {
|
||
|
|
var tile = this._tileReplacementQueue.head;
|
||
|
|
while (defined(tile)) {
|
||
|
|
if (tile.state !== QuadtreeTileLoadState.START) {
|
||
|
|
tileFunction(tile);
|
||
|
|
}
|
||
|
|
tile = tile.replacementNext;
|
||
|
|
}
|
||
|
|
};
|
||
|
|
|
||
|
|
/**
|
||
|
|
* Invokes a specified function for each {@link QuadtreeTile} that was rendered
|
||
|
|
* in the most recent frame.
|
||
|
|
*
|
||
|
|
* @param {Function} tileFunction The function to invoke for each rendered tile. The
|
||
|
|
* function is passed a reference to the tile as its only parameter.
|
||
|
|
*/
|
||
|
|
QuadtreePrimitive.prototype.forEachRenderedTile = function(tileFunction) {
|
||
|
|
var tilesRendered = this._tilesToRender;
|
||
|
|
for (var i = 0, len = tilesRendered.length; i < len; ++i) {
|
||
|
|
tileFunction(tilesRendered[i]);
|
||
|
|
}
|
||
|
|
};
|
||
|
|
|
||
|
|
/**
|
||
|
|
* Calls the callback when a new tile is rendered that contains the given cartographic. The only parameter
|
||
|
|
* is the cartesian position on the tile.
|
||
|
|
*
|
||
|
|
* @param {Cartographic} cartographic The cartographic position.
|
||
|
|
* @param {Function} callback The function to be called when a new tile is loaded containing cartographic.
|
||
|
|
* @returns {Function} The function to remove this callback from the quadtree.
|
||
|
|
*/
|
||
|
|
QuadtreePrimitive.prototype.updateHeight = function(cartographic, callback) {
|
||
|
|
var primitive = this;
|
||
|
|
var object = {
|
||
|
|
positionOnEllipsoidSurface : undefined,
|
||
|
|
positionCartographic : cartographic,
|
||
|
|
level : -1,
|
||
|
|
callback : callback
|
||
|
|
};
|
||
|
|
|
||
|
|
object.removeFunc = function() {
|
||
|
|
var addedCallbacks = primitive._addHeightCallbacks;
|
||
|
|
var length = addedCallbacks.length;
|
||
|
|
for (var i = 0; i < length; ++i) {
|
||
|
|
if (addedCallbacks[i] === object) {
|
||
|
|
addedCallbacks.splice(i, 1);
|
||
|
|
break;
|
||
|
|
}
|
||
|
|
}
|
||
|
|
primitive._removeHeightCallbacks.push(object);
|
||
|
|
};
|
||
|
|
|
||
|
|
primitive._addHeightCallbacks.push(object);
|
||
|
|
return object.removeFunc;
|
||
|
|
};
|
||
|
|
|
||
|
|
/**
|
||
|
|
* Updates the tile provider imagery and continues to process the tile load queue.
|
||
|
|
* @private
|
||
|
|
*/
|
||
|
|
QuadtreePrimitive.prototype.update = function(frameState) {
|
||
|
|
if (defined(this._tileProvider.update)) {
|
||
|
|
this._tileProvider.update(frameState);
|
||
|
|
}
|
||
|
|
};
|
||
|
|
|
||
|
|
function clearTileLoadQueue(primitive) {
|
||
|
|
var debug = primitive._debug;
|
||
|
|
debug.maxDepth = 0;
|
||
|
|
debug.tilesVisited = 0;
|
||
|
|
debug.tilesCulled = 0;
|
||
|
|
debug.tilesRendered = 0;
|
||
|
|
debug.tilesWaitingForChildren = 0;
|
||
|
|
|
||
|
|
primitive._tileLoadQueueHigh.length = 0;
|
||
|
|
primitive._tileLoadQueueMedium.length = 0;
|
||
|
|
primitive._tileLoadQueueLow.length = 0;
|
||
|
|
}
|
||
|
|
|
||
|
|
/**
|
||
|
|
* Initializes values for a new render frame and prepare the tile load queue.
|
||
|
|
* @private
|
||
|
|
*/
|
||
|
|
QuadtreePrimitive.prototype.beginFrame = function(frameState) {
|
||
|
|
var passes = frameState.passes;
|
||
|
|
if (!passes.render) {
|
||
|
|
return;
|
||
|
|
}
|
||
|
|
|
||
|
|
if (this._tilesInvalidated) {
|
||
|
|
invalidateAllTiles(this);
|
||
|
|
this._tilesInvalidated = false;
|
||
|
|
}
|
||
|
|
|
||
|
|
// Gets commands for any texture re-projections
|
||
|
|
this._tileProvider.initialize(frameState);
|
||
|
|
|
||
|
|
if (this._debug.suspendLodUpdate) {
|
||
|
|
return;
|
||
|
|
}
|
||
|
|
|
||
|
|
clearTileLoadQueue(this);
|
||
|
|
this._tileReplacementQueue.markStartOfRenderFrame();
|
||
|
|
};
|
||
|
|
|
||
|
|
/**
|
||
|
|
* Selects new tiles to load based on the frame state and creates render commands.
|
||
|
|
* @private
|
||
|
|
*/
|
||
|
|
QuadtreePrimitive.prototype.render = function(frameState) {
|
||
|
|
var passes = frameState.passes;
|
||
|
|
var tileProvider = this._tileProvider;
|
||
|
|
|
||
|
|
if (passes.render) {
|
||
|
|
tileProvider.beginUpdate(frameState);
|
||
|
|
|
||
|
|
selectTilesForRendering(this, frameState);
|
||
|
|
createRenderCommandsForSelectedTiles(this, frameState);
|
||
|
|
|
||
|
|
tileProvider.endUpdate(frameState);
|
||
|
|
}
|
||
|
|
|
||
|
|
if (passes.pick && this._tilesToRender.length > 0) {
|
||
|
|
tileProvider.updateForPick(frameState);
|
||
|
|
}
|
||
|
|
};
|
||
|
|
|
||
|
|
/**
|
||
|
|
* Checks if the load queue length has changed since the last time we raised a queue change event - if so, raises
|
||
|
|
* a new change event at the end of the render cycle.
|
||
|
|
*/
|
||
|
|
function updateTileLoadProgress(primitive, frameState) {
|
||
|
|
var currentLoadQueueLength = primitive._tileLoadQueueHigh.length + primitive._tileLoadQueueMedium.length + primitive._tileLoadQueueLow.length;
|
||
|
|
|
||
|
|
if (currentLoadQueueLength !== primitive._lastTileLoadQueueLength || primitive._tilesInvalidated) {
|
||
|
|
frameState.afterRender.push(Event.prototype.raiseEvent.bind(primitive._tileLoadProgressEvent, currentLoadQueueLength));
|
||
|
|
primitive._lastTileLoadQueueLength = currentLoadQueueLength;
|
||
|
|
}
|
||
|
|
|
||
|
|
var debug = primitive._debug;
|
||
|
|
if (debug.enableDebugOutput && !debug.suspendLodUpdate) {
|
||
|
|
if (debug.tilesVisited !== debug.lastTilesVisited ||
|
||
|
|
debug.tilesRendered !== debug.lastTilesRendered ||
|
||
|
|
debug.tilesCulled !== debug.lastTilesCulled ||
|
||
|
|
debug.maxDepth !== debug.lastMaxDepth ||
|
||
|
|
debug.tilesWaitingForChildren !== debug.lastTilesWaitingForChildren) {
|
||
|
|
|
||
|
|
console.log('Visited ' + debug.tilesVisited + ', Rendered: ' + debug.tilesRendered + ', Culled: ' + debug.tilesCulled + ', Max Depth: ' + debug.maxDepth + ', Waiting for children: ' + debug.tilesWaitingForChildren);
|
||
|
|
|
||
|
|
debug.lastTilesVisited = debug.tilesVisited;
|
||
|
|
debug.lastTilesRendered = debug.tilesRendered;
|
||
|
|
debug.lastTilesCulled = debug.tilesCulled;
|
||
|
|
debug.lastMaxDepth = debug.maxDepth;
|
||
|
|
debug.lastTilesWaitingForChildren = debug.tilesWaitingForChildren;
|
||
|
|
}
|
||
|
|
}
|
||
|
|
}
|
||
|
|
|
||
|
|
/**
|
||
|
|
* Updates terrain heights.
|
||
|
|
* @private
|
||
|
|
*/
|
||
|
|
QuadtreePrimitive.prototype.endFrame = function(frameState) {
|
||
|
|
var passes = frameState.passes;
|
||
|
|
if (!passes.render || frameState.mode === SceneMode.MORPHING) {
|
||
|
|
// Only process the load queue for a single pass.
|
||
|
|
// Don't process the load queue or update heights during the morph flights.
|
||
|
|
return;
|
||
|
|
}
|
||
|
|
|
||
|
|
// Load/create resources for terrain and imagery. Prepare texture re-projections for the next frame.
|
||
|
|
processTileLoadQueue(this, frameState);
|
||
|
|
updateHeights(this, frameState);
|
||
|
|
updateTileLoadProgress(this, frameState);
|
||
|
|
};
|
||
|
|
|
||
|
|
/**
|
||
|
|
* 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.
|
||
|
|
*
|
||
|
|
* @memberof QuadtreePrimitive
|
||
|
|
*
|
||
|
|
* @returns {Boolean} True if this object was destroyed; otherwise, false.
|
||
|
|
*
|
||
|
|
* @see QuadtreePrimitive#destroy
|
||
|
|
*/
|
||
|
|
QuadtreePrimitive.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.
|
||
|
|
*
|
||
|
|
* @memberof QuadtreePrimitive
|
||
|
|
*
|
||
|
|
* @exception {DeveloperError} This object was destroyed, i.e., destroy() was called.
|
||
|
|
*
|
||
|
|
*
|
||
|
|
* @example
|
||
|
|
* primitive = primitive && primitive.destroy();
|
||
|
|
*
|
||
|
|
* @see QuadtreePrimitive#isDestroyed
|
||
|
|
*/
|
||
|
|
QuadtreePrimitive.prototype.destroy = function() {
|
||
|
|
this._tileProvider = this._tileProvider && this._tileProvider.destroy();
|
||
|
|
};
|
||
|
|
|
||
|
|
var comparisonPoint;
|
||
|
|
var centerScratch = new Cartographic();
|
||
|
|
function compareDistanceToPoint(a, b) {
|
||
|
|
var center = Rectangle.center(a.rectangle, centerScratch);
|
||
|
|
var alon = center.longitude - comparisonPoint.longitude;
|
||
|
|
var alat = center.latitude - comparisonPoint.latitude;
|
||
|
|
|
||
|
|
center = Rectangle.center(b.rectangle, centerScratch);
|
||
|
|
var blon = center.longitude - comparisonPoint.longitude;
|
||
|
|
var blat = center.latitude - comparisonPoint.latitude;
|
||
|
|
|
||
|
|
return (alon * alon + alat * alat) - (blon * blon + blat * blat);
|
||
|
|
}
|
||
|
|
|
||
|
|
function selectTilesForRendering(primitive, frameState) {
|
||
|
|
var debug = primitive._debug;
|
||
|
|
if (debug.suspendLodUpdate) {
|
||
|
|
return;
|
||
|
|
}
|
||
|
|
|
||
|
|
// Clear the render list.
|
||
|
|
var tilesToRender = primitive._tilesToRender;
|
||
|
|
tilesToRender.length = 0;
|
||
|
|
|
||
|
|
// We can't render anything before the level zero tiles exist.
|
||
|
|
var tileProvider = primitive._tileProvider;
|
||
|
|
if (!defined(primitive._levelZeroTiles)) {
|
||
|
|
if (tileProvider.ready) {
|
||
|
|
var tilingScheme = tileProvider.tilingScheme;
|
||
|
|
primitive._levelZeroTiles = QuadtreeTile.createLevelZeroTiles(tilingScheme);
|
||
|
|
} else {
|
||
|
|
// Nothing to do until the provider is ready.
|
||
|
|
return;
|
||
|
|
}
|
||
|
|
}
|
||
|
|
|
||
|
|
primitive._occluders.ellipsoid.cameraPosition = frameState.camera.positionWC;
|
||
|
|
|
||
|
|
var tile;
|
||
|
|
var levelZeroTiles = primitive._levelZeroTiles;
|
||
|
|
var occluders = levelZeroTiles.length > 1 ? primitive._occluders : undefined;
|
||
|
|
|
||
|
|
// Sort the level zero tiles by the distance from the center to the camera.
|
||
|
|
// The level zero tiles aren't necessarily a nice neat quad, so we can't use the
|
||
|
|
// quadtree ordering we use elsewhere in the tree
|
||
|
|
comparisonPoint = frameState.camera.positionCartographic;
|
||
|
|
levelZeroTiles.sort(compareDistanceToPoint);
|
||
|
|
|
||
|
|
var customDataAdded = primitive._addHeightCallbacks;
|
||
|
|
var customDataRemoved = primitive._removeHeightCallbacks;
|
||
|
|
var frameNumber = frameState.frameNumber;
|
||
|
|
|
||
|
|
var i;
|
||
|
|
var len;
|
||
|
|
if (customDataAdded.length > 0 || customDataRemoved.length > 0) {
|
||
|
|
for (i = 0, len = levelZeroTiles.length; i < len; ++i) {
|
||
|
|
tile = levelZeroTiles[i];
|
||
|
|
tile._updateCustomData(frameNumber, customDataAdded, customDataRemoved);
|
||
|
|
}
|
||
|
|
|
||
|
|
customDataAdded.length = 0;
|
||
|
|
customDataRemoved.length = 0;
|
||
|
|
}
|
||
|
|
|
||
|
|
// Our goal with load ordering is to first load all of the tiles we need to
|
||
|
|
// render the current scene at full detail. Loading any other tiles is just
|
||
|
|
// a form of prefetching, and we need not do it at all (other concerns aside). This
|
||
|
|
// simple and obvious statement gets more complicated when we realize that, because
|
||
|
|
// we don't have bounding volumes for the entire terrain tile pyramid, we don't
|
||
|
|
// precisely know which tiles we need to render the scene at full detail, until we do
|
||
|
|
// some loading.
|
||
|
|
//
|
||
|
|
// So our load priority is (from high to low):
|
||
|
|
// 1. Tiles that we _would_ render, except that they're not sufficiently loaded yet.
|
||
|
|
// Ideally this would only include tiles that we've already determined to be visible,
|
||
|
|
// but since we don't have reliable visibility information until a tile is loaded,
|
||
|
|
// and because we (currently) must have all children in a quad renderable before we
|
||
|
|
// can refine, this pretty much means tiles we'd like to refine to, regardless of
|
||
|
|
// visibility. (high)
|
||
|
|
// 2. Tiles that we're rendering. (medium)
|
||
|
|
// 3. All other tiles. (low)
|
||
|
|
//
|
||
|
|
// Within each priority group, tiles should be loaded in approximate near-to-far order,
|
||
|
|
// but currently they're just loaded in our traversal order which makes no guarantees
|
||
|
|
// about depth ordering.
|
||
|
|
|
||
|
|
// Traverse in depth-first, near-to-far order.
|
||
|
|
for (i = 0, len = levelZeroTiles.length; i < len; ++i) {
|
||
|
|
tile = levelZeroTiles[i];
|
||
|
|
primitive._tileReplacementQueue.markTileRendered(tile);
|
||
|
|
if (!tile.renderable) {
|
||
|
|
if (tile.needsLoading) {
|
||
|
|
primitive._tileLoadQueueHigh.push(tile);
|
||
|
|
}
|
||
|
|
++debug.tilesWaitingForChildren;
|
||
|
|
} else if (tileProvider.computeTileVisibility(tile, frameState, occluders) !== Visibility.NONE) {
|
||
|
|
visitTile(primitive, frameState, tile);
|
||
|
|
} else {
|
||
|
|
if (tile.needsLoading) {
|
||
|
|
primitive._tileLoadQueueLow.push(tile);
|
||
|
|
}
|
||
|
|
++debug.tilesCulled;
|
||
|
|
}
|
||
|
|
}
|
||
|
|
}
|
||
|
|
|
||
|
|
function visitTile(primitive, frameState, tile) {
|
||
|
|
var debug = primitive._debug;
|
||
|
|
|
||
|
|
++debug.tilesVisited;
|
||
|
|
|
||
|
|
primitive._tileReplacementQueue.markTileRendered(tile);
|
||
|
|
tile._updateCustomData(frameState.frameNumber);
|
||
|
|
|
||
|
|
if (tile.level > debug.maxDepth) {
|
||
|
|
debug.maxDepth = tile.level;
|
||
|
|
}
|
||
|
|
|
||
|
|
if (screenSpaceError(primitive, frameState, tile) < primitive.maximumScreenSpaceError) {
|
||
|
|
// This tile meets SSE requirements, so render it.
|
||
|
|
if (tile.needsLoading) {
|
||
|
|
// Rendered tile meeting SSE loads with medium priority.
|
||
|
|
primitive._tileLoadQueueMedium.push(tile);
|
||
|
|
}
|
||
|
|
addTileToRenderList(primitive, tile);
|
||
|
|
return;
|
||
|
|
}
|
||
|
|
|
||
|
|
var southwestChild = tile.southwestChild;
|
||
|
|
var southeastChild = tile.southeastChild;
|
||
|
|
var northwestChild = tile.northwestChild;
|
||
|
|
var northeastChild = tile.northeastChild;
|
||
|
|
var allAreRenderable = southwestChild.renderable && southeastChild.renderable &&
|
||
|
|
northwestChild.renderable && northeastChild.renderable;
|
||
|
|
var allAreUpsampled = southwestChild.upsampledFromParent && southeastChild.upsampledFromParent &&
|
||
|
|
northwestChild.upsampledFromParent && northeastChild.upsampledFromParent;
|
||
|
|
|
||
|
|
if (allAreRenderable) {
|
||
|
|
if (allAreUpsampled) {
|
||
|
|
// No point in rendering the children because they're all upsampled. Render this tile instead.
|
||
|
|
addTileToRenderList(primitive, tile);
|
||
|
|
|
||
|
|
// Load the children even though we're (currently) not going to render them.
|
||
|
|
// A tile that is "upsampled only" right now might change its tune once it does more loading.
|
||
|
|
// A tile that is upsampled now and forever should also be done loading, so no harm done.
|
||
|
|
queueChildLoadNearToFar(primitive, frameState.camera.positionCartographic, southwestChild, southeastChild, northwestChild, northeastChild);
|
||
|
|
|
||
|
|
if (tile.needsLoading) {
|
||
|
|
// Rendered tile that's not waiting on children loads with medium priority.
|
||
|
|
primitive._tileLoadQueueMedium.push(tile);
|
||
|
|
}
|
||
|
|
} else {
|
||
|
|
// SSE is not good enough and children are loaded, so refine.
|
||
|
|
// No need to add the children to the load queue because they'll be added (if necessary) when they're visited.
|
||
|
|
visitVisibleChildrenNearToFar(primitive, southwestChild, southeastChild, northwestChild, northeastChild, frameState);
|
||
|
|
|
||
|
|
if (tile.needsLoading) {
|
||
|
|
// Tile is not rendered, so load it with low priority.
|
||
|
|
primitive._tileLoadQueueLow.push(tile);
|
||
|
|
}
|
||
|
|
}
|
||
|
|
} else {
|
||
|
|
// We'd like to refine but can't because not all of our children are renderable. Load the refinement blockers with high priority and
|
||
|
|
// render this tile in the meantime.
|
||
|
|
queueChildLoadNearToFar(primitive, frameState.camera.positionCartographic, southwestChild, southeastChild, northwestChild, northeastChild);
|
||
|
|
addTileToRenderList(primitive, tile);
|
||
|
|
|
||
|
|
if (tile.needsLoading) {
|
||
|
|
// We will refine this tile when it's possible, so load this tile only with low priority.
|
||
|
|
primitive._tileLoadQueueLow.push(tile);
|
||
|
|
}
|
||
|
|
}
|
||
|
|
}
|
||
|
|
|
||
|
|
function queueChildLoadNearToFar(primitive, cameraPosition, southwest, southeast, northwest, northeast) {
|
||
|
|
if (cameraPosition.longitude < southwest.east) {
|
||
|
|
if (cameraPosition.latitude < southwest.north) {
|
||
|
|
// Camera in southwest quadrant
|
||
|
|
queueChildTileLoad(primitive, southwest);
|
||
|
|
queueChildTileLoad(primitive, southeast);
|
||
|
|
queueChildTileLoad(primitive, northwest);
|
||
|
|
queueChildTileLoad(primitive, northeast);
|
||
|
|
} else {
|
||
|
|
// Camera in northwest quadrant
|
||
|
|
queueChildTileLoad(primitive, northwest);
|
||
|
|
queueChildTileLoad(primitive, southwest);
|
||
|
|
queueChildTileLoad(primitive, northeast);
|
||
|
|
queueChildTileLoad(primitive, southeast);
|
||
|
|
}
|
||
|
|
} else if (cameraPosition.latitude < southwest.north) {
|
||
|
|
// Camera southeast quadrant
|
||
|
|
queueChildTileLoad(primitive, southeast);
|
||
|
|
queueChildTileLoad(primitive, southwest);
|
||
|
|
queueChildTileLoad(primitive, northeast);
|
||
|
|
queueChildTileLoad(primitive, northwest);
|
||
|
|
} else {
|
||
|
|
// Camera in northeast quadrant
|
||
|
|
queueChildTileLoad(primitive, northeast);
|
||
|
|
queueChildTileLoad(primitive, northwest);
|
||
|
|
queueChildTileLoad(primitive, southeast);
|
||
|
|
queueChildTileLoad(primitive, southwest);
|
||
|
|
}
|
||
|
|
}
|
||
|
|
|
||
|
|
function queueChildTileLoad(primitive, childTile) {
|
||
|
|
primitive._tileReplacementQueue.markTileRendered(childTile);
|
||
|
|
if (childTile.needsLoading) {
|
||
|
|
if (childTile.renderable) {
|
||
|
|
primitive._tileLoadQueueLow.push(childTile);
|
||
|
|
} else {
|
||
|
|
// A tile blocking refine loads with high priority
|
||
|
|
primitive._tileLoadQueueHigh.push(childTile);
|
||
|
|
}
|
||
|
|
}
|
||
|
|
}
|
||
|
|
|
||
|
|
function visitVisibleChildrenNearToFar(primitive, southwest, southeast, northwest, northeast, frameState) {
|
||
|
|
var cameraPosition = frameState.camera.positionCartographic;
|
||
|
|
var tileProvider = primitive._tileProvider;
|
||
|
|
var occluders = primitive._occluders;
|
||
|
|
|
||
|
|
if (cameraPosition.longitude < southwest.rectangle.east) {
|
||
|
|
if (cameraPosition.latitude < southwest.rectangle.north) {
|
||
|
|
// Camera in southwest quadrant
|
||
|
|
visitIfVisible(primitive, southwest, tileProvider, frameState, occluders);
|
||
|
|
visitIfVisible(primitive, southeast, tileProvider, frameState, occluders);
|
||
|
|
visitIfVisible(primitive, northwest, tileProvider, frameState, occluders);
|
||
|
|
visitIfVisible(primitive, northeast, tileProvider, frameState, occluders);
|
||
|
|
} else {
|
||
|
|
// Camera in northwest quadrant
|
||
|
|
visitIfVisible(primitive, northwest, tileProvider, frameState, occluders);
|
||
|
|
visitIfVisible(primitive, southwest, tileProvider, frameState, occluders);
|
||
|
|
visitIfVisible(primitive, northeast, tileProvider, frameState, occluders);
|
||
|
|
visitIfVisible(primitive, southeast, tileProvider, frameState, occluders);
|
||
|
|
}
|
||
|
|
} else if (cameraPosition.latitude < southwest.rectangle.north) {
|
||
|
|
// Camera southeast quadrant
|
||
|
|
visitIfVisible(primitive, southeast, tileProvider, frameState, occluders);
|
||
|
|
visitIfVisible(primitive, southwest, tileProvider, frameState, occluders);
|
||
|
|
visitIfVisible(primitive, northeast, tileProvider, frameState, occluders);
|
||
|
|
visitIfVisible(primitive, northwest, tileProvider, frameState, occluders);
|
||
|
|
} else {
|
||
|
|
// Camera in northeast quadrant
|
||
|
|
visitIfVisible(primitive, northeast, tileProvider, frameState, occluders);
|
||
|
|
visitIfVisible(primitive, northwest, tileProvider, frameState, occluders);
|
||
|
|
visitIfVisible(primitive, southeast, tileProvider, frameState, occluders);
|
||
|
|
visitIfVisible(primitive, southwest, tileProvider, frameState, occluders);
|
||
|
|
}
|
||
|
|
}
|
||
|
|
|
||
|
|
function visitIfVisible(primitive, tile, tileProvider, frameState, occluders) {
|
||
|
|
if (tileProvider.computeTileVisibility(tile, frameState, occluders) !== Visibility.NONE) {
|
||
|
|
visitTile(primitive, frameState, tile);
|
||
|
|
} else {
|
||
|
|
++primitive._debug.tilesCulled;
|
||
|
|
primitive._tileReplacementQueue.markTileRendered(tile);
|
||
|
|
|
||
|
|
// We've decided this tile is not visible, but if it's not fully loaded yet, we've made
|
||
|
|
// this determination based on possibly-incorrect information. We need to load this
|
||
|
|
// culled tile with low priority just in case it turns out to be visible after all.
|
||
|
|
if (tile.needsLoading) {
|
||
|
|
primitive._tileLoadQueueLow.push(tile);
|
||
|
|
}
|
||
|
|
}
|
||
|
|
}
|
||
|
|
|
||
|
|
function screenSpaceError(primitive, frameState, tile) {
|
||
|
|
if (frameState.mode === SceneMode.SCENE2D || frameState.camera.frustum instanceof OrthographicFrustum) {
|
||
|
|
return screenSpaceError2D(primitive, frameState, tile);
|
||
|
|
}
|
||
|
|
|
||
|
|
var maxGeometricError = primitive._tileProvider.getLevelMaximumGeometricError(tile.level);
|
||
|
|
|
||
|
|
var distance = tile._distance;
|
||
|
|
var height = frameState.context.drawingBufferHeight;
|
||
|
|
var sseDenominator = frameState.camera.frustum.sseDenominator;
|
||
|
|
|
||
|
|
var error = (maxGeometricError * height) / (distance * sseDenominator);
|
||
|
|
|
||
|
|
if (frameState.fog.enabled) {
|
||
|
|
error = error - CesiumMath.fog(distance, frameState.fog.density) * frameState.fog.sse;
|
||
|
|
}
|
||
|
|
|
||
|
|
return error;
|
||
|
|
}
|
||
|
|
|
||
|
|
function screenSpaceError2D(primitive, frameState, tile) {
|
||
|
|
var camera = frameState.camera;
|
||
|
|
var frustum = camera.frustum;
|
||
|
|
if (defined(frustum._offCenterFrustum)) {
|
||
|
|
frustum = frustum._offCenterFrustum;
|
||
|
|
}
|
||
|
|
|
||
|
|
var context = frameState.context;
|
||
|
|
var width = context.drawingBufferWidth;
|
||
|
|
var height = context.drawingBufferHeight;
|
||
|
|
|
||
|
|
var maxGeometricError = primitive._tileProvider.getLevelMaximumGeometricError(tile.level);
|
||
|
|
var pixelSize = Math.max(frustum.top - frustum.bottom, frustum.right - frustum.left) / Math.max(width, height);
|
||
|
|
var error = maxGeometricError / pixelSize;
|
||
|
|
|
||
|
|
if (frameState.fog.enabled && frameState.mode !== SceneMode.SCENE2D) {
|
||
|
|
error = error - CesiumMath.fog(tile._distance, frameState.fog.density) * frameState.fog.sse;
|
||
|
|
}
|
||
|
|
|
||
|
|
return error;
|
||
|
|
}
|
||
|
|
|
||
|
|
function addTileToRenderList(primitive, tile) {
|
||
|
|
primitive._tilesToRender.push(tile);
|
||
|
|
++primitive._debug.tilesRendered;
|
||
|
|
}
|
||
|
|
|
||
|
|
function processTileLoadQueue(primitive, frameState) {
|
||
|
|
var tileLoadQueueHigh = primitive._tileLoadQueueHigh;
|
||
|
|
var tileLoadQueueMedium = primitive._tileLoadQueueMedium;
|
||
|
|
var tileLoadQueueLow = primitive._tileLoadQueueLow;
|
||
|
|
|
||
|
|
if (tileLoadQueueHigh.length === 0 && tileLoadQueueMedium.length === 0 && tileLoadQueueLow.length === 0) {
|
||
|
|
return;
|
||
|
|
}
|
||
|
|
|
||
|
|
// Remove any tiles that were not used this frame beyond the number
|
||
|
|
// we're allowed to keep.
|
||
|
|
primitive._tileReplacementQueue.trimTiles(primitive.tileCacheSize);
|
||
|
|
|
||
|
|
var endTime = getTimestamp() + primitive._loadQueueTimeSlice;
|
||
|
|
var tileProvider = primitive._tileProvider;
|
||
|
|
|
||
|
|
processSinglePriorityLoadQueue(primitive, frameState, tileProvider, endTime, tileLoadQueueHigh);
|
||
|
|
processSinglePriorityLoadQueue(primitive, frameState, tileProvider, endTime, tileLoadQueueMedium);
|
||
|
|
processSinglePriorityLoadQueue(primitive, frameState, tileProvider, endTime, tileLoadQueueLow);
|
||
|
|
}
|
||
|
|
|
||
|
|
function processSinglePriorityLoadQueue(primitive, frameState, tileProvider, endTime, loadQueue) {
|
||
|
|
for (var i = 0, len = loadQueue.length; i < len && getTimestamp() < endTime; ++i) {
|
||
|
|
var tile = loadQueue[i];
|
||
|
|
primitive._tileReplacementQueue.markTileRendered(tile);
|
||
|
|
tileProvider.loadTile(frameState, tile);
|
||
|
|
}
|
||
|
|
}
|
||
|
|
|
||
|
|
var scratchRay = new Ray();
|
||
|
|
var scratchCartographic = new Cartographic();
|
||
|
|
var scratchPosition = new Cartesian3();
|
||
|
|
var scratchArray = [];
|
||
|
|
|
||
|
|
function updateHeights(primitive, frameState) {
|
||
|
|
var tryNextFrame = scratchArray;
|
||
|
|
tryNextFrame.length = 0;
|
||
|
|
var tilesToUpdateHeights = primitive._tileToUpdateHeights;
|
||
|
|
var terrainProvider = primitive._tileProvider.terrainProvider;
|
||
|
|
|
||
|
|
var startTime = getTimestamp();
|
||
|
|
var timeSlice = primitive._updateHeightsTimeSlice;
|
||
|
|
var endTime = startTime + timeSlice;
|
||
|
|
|
||
|
|
var mode = frameState.mode;
|
||
|
|
var projection = frameState.mapProjection;
|
||
|
|
var ellipsoid = projection.ellipsoid;
|
||
|
|
var i;
|
||
|
|
|
||
|
|
while (tilesToUpdateHeights.length > 0) {
|
||
|
|
var tile = tilesToUpdateHeights[0];
|
||
|
|
if (tile.state !== QuadtreeTileLoadState.DONE) {
|
||
|
|
tryNextFrame.push(tile);
|
||
|
|
tilesToUpdateHeights.shift();
|
||
|
|
primitive._lastTileIndex = 0;
|
||
|
|
continue;
|
||
|
|
}
|
||
|
|
var customData = tile.customData;
|
||
|
|
var customDataLength = customData.length;
|
||
|
|
|
||
|
|
var timeSliceMax = false;
|
||
|
|
for (i = primitive._lastTileIndex; i < customDataLength; ++i) {
|
||
|
|
var data = customData[i];
|
||
|
|
|
||
|
|
if (tile.level > data.level) {
|
||
|
|
if (!defined(data.positionOnEllipsoidSurface)) {
|
||
|
|
// cartesian has to be on the ellipsoid surface for `ellipsoid.geodeticSurfaceNormal`
|
||
|
|
data.positionOnEllipsoidSurface = Cartesian3.fromRadians(data.positionCartographic.longitude, data.positionCartographic.latitude, 0.0, ellipsoid);
|
||
|
|
}
|
||
|
|
|
||
|
|
if (mode === SceneMode.SCENE3D) {
|
||
|
|
var surfaceNormal = ellipsoid.geodeticSurfaceNormal(data.positionOnEllipsoidSurface, scratchRay.direction);
|
||
|
|
|
||
|
|
// compute origin point
|
||
|
|
|
||
|
|
// Try to find the intersection point between the surface normal and z-axis.
|
||
|
|
// minimum height (-11500.0) for the terrain set, need to get this information from the terrain provider
|
||
|
|
var rayOrigin = ellipsoid.getSurfaceNormalIntersectionWithZAxis(data.positionOnEllipsoidSurface, 11500.0, scratchRay.origin);
|
||
|
|
|
||
|
|
// Theoretically, not with Earth datums, the intersection point can be outside the ellipsoid
|
||
|
|
if (!defined(rayOrigin)) {
|
||
|
|
// intersection point is outside the ellipsoid, try other value
|
||
|
|
// minimum height (-11500.0) for the terrain set, need to get this information from the terrain provider
|
||
|
|
var magnitude = Math.min(defaultValue(tile.data.minimumHeight, 0.0),-11500.0);
|
||
|
|
|
||
|
|
// multiply by the *positive* value of the magnitude
|
||
|
|
var vectorToMinimumPoint = Cartesian3.multiplyByScalar(surfaceNormal, Math.abs(magnitude) + 1, scratchPosition);
|
||
|
|
Cartesian3.subtract(data.positionOnEllipsoidSurface, vectorToMinimumPoint, scratchRay.origin);
|
||
|
|
}
|
||
|
|
} else {
|
||
|
|
Cartographic.clone(data.positionCartographic, scratchCartographic);
|
||
|
|
|
||
|
|
// minimum height for the terrain set, need to get this information from the terrain provider
|
||
|
|
scratchCartographic.height = -11500.0;
|
||
|
|
projection.project(scratchCartographic, scratchPosition);
|
||
|
|
Cartesian3.fromElements(scratchPosition.z, scratchPosition.x, scratchPosition.y, scratchPosition);
|
||
|
|
Cartesian3.clone(scratchPosition, scratchRay.origin);
|
||
|
|
Cartesian3.clone(Cartesian3.UNIT_X, scratchRay.direction);
|
||
|
|
}
|
||
|
|
|
||
|
|
var position = tile.data.pick(scratchRay, mode, projection, false, scratchPosition);
|
||
|
|
if (defined(position)) {
|
||
|
|
data.callback(position);
|
||
|
|
data.level = tile.level;
|
||
|
|
}
|
||
|
|
} else if (tile.level === data.level) {
|
||
|
|
var children = tile.children;
|
||
|
|
var childrenLength = children.length;
|
||
|
|
|
||
|
|
var child;
|
||
|
|
for (var j = 0; j < childrenLength; ++j) {
|
||
|
|
child = children[j];
|
||
|
|
if (Rectangle.contains(child.rectangle, data.positionCartographic)) {
|
||
|
|
break;
|
||
|
|
}
|
||
|
|
}
|
||
|
|
|
||
|
|
var tileDataAvailable = terrainProvider.getTileDataAvailable(child.x, child.y, child.level);
|
||
|
|
var parentTile = tile.parent;
|
||
|
|
if ((defined(tileDataAvailable) && !tileDataAvailable) ||
|
||
|
|
(defined(parentTile) && defined(parentTile.data) && defined(parentTile.data.terrainData) &&
|
||
|
|
!parentTile.data.terrainData.isChildAvailable(parentTile.x, parentTile.y, child.x, child.y))) {
|
||
|
|
data.removeFunc();
|
||
|
|
}
|
||
|
|
}
|
||
|
|
|
||
|
|
if (getTimestamp() >= endTime) {
|
||
|
|
timeSliceMax = true;
|
||
|
|
break;
|
||
|
|
}
|
||
|
|
}
|
||
|
|
|
||
|
|
if (timeSliceMax) {
|
||
|
|
primitive._lastTileIndex = i;
|
||
|
|
break;
|
||
|
|
} else {
|
||
|
|
primitive._lastTileIndex = 0;
|
||
|
|
tilesToUpdateHeights.shift();
|
||
|
|
}
|
||
|
|
}
|
||
|
|
for (i = 0; i < tryNextFrame.length; i++) {
|
||
|
|
tilesToUpdateHeights.push(tryNextFrame[i]);
|
||
|
|
}
|
||
|
|
}
|
||
|
|
|
||
|
|
function createRenderCommandsForSelectedTiles(primitive, frameState) {
|
||
|
|
var tileProvider = primitive._tileProvider;
|
||
|
|
var tilesToRender = primitive._tilesToRender;
|
||
|
|
var tilesToUpdateHeights = primitive._tileToUpdateHeights;
|
||
|
|
|
||
|
|
for (var i = 0, len = tilesToRender.length; i < len; ++i) {
|
||
|
|
var tile = tilesToRender[i];
|
||
|
|
tileProvider.showTileThisFrame(tile, frameState);
|
||
|
|
|
||
|
|
if (tile._frameRendered !== frameState.frameNumber - 1) {
|
||
|
|
tilesToUpdateHeights.push(tile);
|
||
|
|
}
|
||
|
|
tile._frameRendered = frameState.frameNumber;
|
||
|
|
}
|
||
|
|
}
|
||
|
|
|
||
|
|
return QuadtreePrimitive;
|
||
|
|
});
|