/*global define*/
define([
'../Core/defaultValue',
'../Core/defined',
'../Core/defineProperties',
'../Core/DeveloperError',
'../Core/getTimestamp',
'../Core/Queue',
'../Core/Visibility',
'./QuadtreeOccluders',
'./QuadtreeTile',
'./QuadtreeTileLoadState',
'./SceneMode',
'./TileReplacementQueue'
], function(
defaultValue,
defined,
defineProperties,
DeveloperError,
getTimestamp,
Queue,
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.
*/
var QuadtreePrimitive = 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._tileTraversalQueue = new Queue();
this._tileLoadQueue = [];
this._tileReplacementQueue = new TileReplacementQueue();
this._levelZeroTiles = undefined;
this._levelZeroTilesReady = false;
this._loadQueueTimeSlice = 5.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
});
};
defineProperties(QuadtreePrimitive.prototype, {
/**
* Gets the provider of {@link QuadtreeTile} instances for this quadtree.
* @type {QuadtreeTile}
* @memberof QuadtreePrimitive.prototype
*/
tileProvider : {
get : function() {
return this._tileProvider;
}
}
});
/**
* 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() {
// Clear the replacement queue
var replacementQueue = this._tileReplacementQueue;
replacementQueue.head = undefined;
replacementQueue.tail = undefined;
replacementQueue.count = 0;
// Free and recreate the level zero tiles.
var levelZeroTiles = this._levelZeroTiles;
if (defined(levelZeroTiles)) {
for (var i = 0; i < levelZeroTiles.length; ++i) {
levelZeroTiles[i].freeResources();
}
}
this._levelZeroTiles = undefined;
};
/**
* 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]);
}
};
/**
* Updates the primitive.
*
* @param {Context} context The rendering context to use.
* @param {FrameState} frameState The state of the current frame.
* @param {DrawCommand[]} commandList The list of draw commands. The primitive will usually add
* commands to this array during the update call.
*/
QuadtreePrimitive.prototype.update = function(context, frameState, commandList) {
this._tileProvider.beginUpdate(context, frameState, commandList);
selectTilesForRendering(this, context, frameState);
processTileLoadQueue(this, context, frameState);
createRenderCommandsForSelectedTiles(this, context, frameState, commandList);
this._tileProvider.endUpdate(context, frameState, commandList);
};
/**
* Returns true if this object was destroyed; otherwise, false.
*
* If this object was destroyed, it should not be used; calling any function other than
* isDestroyed
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.
*
* Once an object is destroyed, it should not be used; calling any function other than
* isDestroyed
will result in a {@link DeveloperError} exception. Therefore,
* assign the return value (undefined
) to the object as done in the example.
*
* @memberof QuadtreePrimitive
*
* @returns {undefined}
*
* @exception {DeveloperError} This object was destroyed, i.e., destroy() was called.
*
* @see QuadtreePrimitive#isDestroyed
*
* @example
* primitive = primitive && primitive.destroy();
*/
QuadtreePrimitive.prototype.destroy = function() {
this._tileProvider = this._tileProvider && this._tileProvider.destroy();
};
function selectTilesForRendering(primitive, context, frameState) {
var debug = primitive._debug;
if (debug.suspendLodUpdate) {
return;
}
var i;
var len;
// Clear the render list.
var tilesToRender = primitive._tilesToRender;
tilesToRender.length = 0;
var traversalQueue = primitive._tileTraversalQueue;
traversalQueue.clear();
debug.maxDepth = 0;
debug.tilesVisited = 0;
debug.tilesCulled = 0;
debug.tilesRendered = 0;
debug.tilesWaitingForChildren = 0;
primitive._tileLoadQueue.length = 0;
primitive._tileReplacementQueue.markStartOfRenderFrame();
// We can't render anything before the level zero tiles exist.
if (!defined(primitive._levelZeroTiles)) {
if (primitive._tileProvider.ready) {
var terrainTilingScheme = primitive._tileProvider.tilingScheme;
primitive._levelZeroTiles = QuadtreeTile.createLevelZeroTiles(terrainTilingScheme);
} else {
// Nothing to do until the provider is ready.
return;
}
}
primitive._occluders.ellipsoid.cameraPosition = frameState.camera.positionWC;
var tileProvider = primitive._tileProvider;
var occluders = primitive._occluders;
var tile;
// Enqueue the root tiles that are renderable and visible.
var levelZeroTiles = primitive._levelZeroTiles;
for (i = 0, len = levelZeroTiles.length; i < len; ++i) {
tile = levelZeroTiles[i];
primitive._tileReplacementQueue.markTileRendered(tile);
if (tile.needsLoading) {
queueTileLoad(primitive, tile);
}
if (tile.renderable && tileProvider.computeTileVisibility(tile, frameState, occluders) !== Visibility.NONE) {
traversalQueue.enqueue(tile);
} else {
++debug.tilesCulled;
if (!tile.renderable) {
++debug.tilesWaitingForChildren;
}
}
}
// Traverse the tiles in breadth-first order.
// This ordering allows us to load bigger, lower-detail tiles before smaller, higher-detail ones.
// This maximizes the average detail across the scene and results in fewer sharp transitions
// between very different LODs.
while (defined((tile = traversalQueue.dequeue()))) {
++debug.tilesVisited;
primitive._tileReplacementQueue.markTileRendered(tile);
if (tile.level > debug.maxDepth) {
debug.maxDepth = tile.level;
}
// There are a few different algorithms we could use here.
// This one doesn't load children unless we refine to them.
// We may want to revisit this in the future.
if (screenSpaceError(primitive, context, frameState, tile) < primitive.maximumScreenSpaceError) {
// This tile meets SSE requirements, so render it.
addTileToRenderList(primitive, tile);
} else if (queueChildrenLoadAndDetermineIfChildrenAreAllRenderable(primitive, tile)) {
// SSE is not good enough and children are loaded, so refine.
var children = tile.children;
// PERFORMANCE_IDEA: traverse children front-to-back so we can avoid sorting by distance later.
for (i = 0, len = children.length; i < len; ++i) {
if (tileProvider.computeTileVisibility(children[i], frameState, occluders) !== Visibility.NONE) {
traversalQueue.enqueue(children[i]);
} else {
++debug.tilesCulled;
}
}
} else {
++debug.tilesWaitingForChildren;
// SSE is not good enough but not all children are loaded, so render this tile anyway.
addTileToRenderList(primitive, tile);
}
}
if (debug.enableDebugOutput) {
if (debug.tilesVisited !== debug.lastTilesVisited ||
debug.tilesRendered !== debug.lastTilesRendered ||
debug.tilesCulled !== debug.lastTilesCulled ||
debug.maxDepth !== debug.lastMaxDepth ||
debug.tilesWaitingForChildren !== debug.lastTilesWaitingForChildren) {
/*global console*/
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;
}
}
}
function screenSpaceError(primitive, context, frameState, tile) {
if (frameState.mode === SceneMode.SCENE2D) {
return screenSpaceError2D(primitive, context, frameState, tile);
}
var maxGeometricError = primitive._tileProvider.getLevelMaximumGeometricError(tile.level);
var distance = primitive._tileProvider.computeDistanceToTile(tile, frameState);
tile._distance = distance;
var height = context.drawingBufferHeight;
var camera = frameState.camera;
var frustum = camera.frustum;
var fovy = frustum.fovy;
// PERFORMANCE_IDEA: factor out stuff that's constant across tiles.
return (maxGeometricError * height) / (2 * distance * Math.tan(0.5 * fovy));
}
function screenSpaceError2D(primitive, context, frameState, tile) {
var camera = frameState.camera;
var frustum = camera.frustum;
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);
return maxGeometricError / pixelSize;
}
function addTileToRenderList(primitive, tile) {
primitive._tilesToRender.push(tile);
++primitive._debug.tilesRendered;
}
function queueChildrenLoadAndDetermineIfChildrenAreAllRenderable(primitive, tile) {
var allRenderable = true;
var allUpsampledOnly = true;
var children = tile.children;
for (var i = 0, len = children.length; i < len; ++i) {
var child = children[i];
primitive._tileReplacementQueue.markTileRendered(child);
allUpsampledOnly = allUpsampledOnly && child.upsampledFromParent;
allRenderable = allRenderable && child.renderable;
if (child.needsLoading) {
queueTileLoad(primitive, child);
}
}
if (!allRenderable) {
++primitive._debug.tilesWaitingForChildren;
}
// If all children are upsampled from this tile, we just render this tile instead of its children.
return allRenderable && !allUpsampledOnly;
}
function queueTileLoad(primitive, tile) {
primitive._tileLoadQueue.push(tile);
}
function processTileLoadQueue(primitive, context, frameState) {
var tileLoadQueue = primitive._tileLoadQueue;
var tileProvider = primitive._tileProvider;
if (tileLoadQueue.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 startTime = getTimestamp();
var timeSlice = primitive._loadQueueTimeSlice;
var endTime = startTime + timeSlice;
for (var len = tileLoadQueue.length - 1, i = len; i >= 0; --i) {
var tile = tileLoadQueue[i];
primitive._tileReplacementQueue.markTileRendered(tile);
tileProvider.loadTile(context, frameState, tile);
if (getTimestamp() >= endTime) {
break;
}
}
}
function tileDistanceSortFunction(a, b) {
return a._distance - b._distance;
}
function createRenderCommandsForSelectedTiles(primitive, context, frameState, commandList) {
var tileProvider = primitive._tileProvider;
var tilesToRender = primitive._tilesToRender;
tilesToRender.sort(tileDistanceSortFunction);
for (var i = 0, len = tilesToRender.length; i < len; ++i) {
tileProvider.showTileThisFrame(tilesToRender[i], context, frameState, commandList);
}
}
return QuadtreePrimitive;
});