Skip to content

Commit

Permalink
tiles: Tile traversal optimizations (#1183)
Browse files Browse the repository at this point in the history
  • Loading branch information
Avnerus authored Jun 16, 2021
1 parent 0c5cd92 commit 5e4d910
Show file tree
Hide file tree
Showing 7 changed files with 91 additions and 78 deletions.
2 changes: 1 addition & 1 deletion examples/website/i3s/app.js
Original file line number Diff line number Diff line change
Expand Up @@ -144,7 +144,7 @@ export default class App extends PureComponent {

_renderLayers() {
const {tilesetUrl, token} = this.state;
const loadOptions = {throttleRequests: true};
const loadOptions = {};
if (token) {
loadOptions.token = token;
}
Expand Down
2 changes: 1 addition & 1 deletion modules/tiles/docs/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -38,7 +38,7 @@
- `options`: Options object, but not limited to
Parameters:
- `modelMatrix`=`Matrix4.IDENTITY` (`Matrix4`) - A 4x4 transformation matrix that transforms the tileset's root tile.
- `maximumMemoryUsage`=`512`] (`Number`) - The maximum amount of memory in MB that can be used by the tileset.
- `maximumMemoryUsage`=`512` (`Number`) - The maximum amount of memory in MB that can be used by the tileset.
- `ellipsoid`=`Ellipsoid.WGS84` ([`Ellipsoid`](https://math.gl/modules/geospatial/docs/api-reference/ellipsoid)) - The ellipsoid determining the size and shape of the globe.
Callbacks:
- `onTileLoad` (`(tileHeader : Tile3DHeader) : void`) - callback when a tile node is fully loaded during the tileset traversal.
Expand Down
4 changes: 2 additions & 2 deletions modules/tiles/docs/api-reference/tile-3d.md
Original file line number Diff line number Diff line change
Expand Up @@ -42,9 +42,9 @@ One of
- `render`: has content to render
- `tileset`: tileset tile

###### `depth` (Number)
##### `_selectionDepth` (Number)

The depth of the tile in the tileset tree.
The depth of the tile in the traversal tree.

###### `content` (Object)

Expand Down
3 changes: 2 additions & 1 deletion modules/tiles/docs/api-reference/tileset-3d.md
Original file line number Diff line number Diff line change
Expand Up @@ -69,7 +69,8 @@ Parameters:
- `json`: loaded tileset json object, should follow the format [tiles format](https://loaders.gl/docs/specifications/category-3d-tiles)
- `options`:
- `options.ellipsoid`=`Ellipsoid.WGS84` (`Ellipsoid`) - The ellipsoid determining the size and shape of the globe.
- `options.throttleRequests`=`true` (`Boolean`) - Determines whether or not to throttle tile fetching requests.
- `options.throttleRequests`=`true` (`Boolean`) - Determines whether or not to throttle tile fetching requests. Throttled requests are prioritized according to tile visibility.
- `options.maxRequests`=`64` (`Number`) - When throttling tile fetching, the maximum number of simultaneous requests.
- `options.modelMatrix`=`Matrix4.IDENTITY` (`Matrix4`) - A 4x4 transformation matrix this transforms the entire tileset.
- `options.maximumMemoryUsage`=`512`] (`Number`) - The maximum amount of memory in MB that can be used by the tileset.
- `options.fetchOptions` - fetchOptions, i.e. headers, used to load tiles from tiling server
Expand Down
59 changes: 42 additions & 17 deletions modules/tiles/src/tileset/tile-3d.ts
Original file line number Diff line number Diff line change
Expand Up @@ -50,7 +50,7 @@ export default class TileHeader {
id: string;
url: string;
parent: TileHeader;
refine: string;
refine: number;
type: string;
contentUrl: string;
lodMetricType: string;
Expand Down Expand Up @@ -86,7 +86,7 @@ export default class TileHeader {
private _centerZDepth: number;
private _screenSpaceError: number;
private _visibilityPlaneMask: any;
private _visible: boolean;
private _visible?: boolean;
private _inRequestVolume: boolean;

private _stackLength: number;
Expand Down Expand Up @@ -178,7 +178,7 @@ export default class TileHeader {
this._shouldRefine = false;
this._distanceToCamera = 0;
this._centerZDepth = 0;
this._visible = false;
this._visible = undefined;
this._inRequestVolume = false;
this._stackLength = 0;
this._selectionDepth = 0;
Expand Down Expand Up @@ -289,6 +289,44 @@ export default class TileHeader {
}
}

/*
* If skipLevelOfDetail is off try to load child tiles as soon as possible so that their parent can refine sooner.
* Tiles are prioritized by screen space error.
*/
_getPriority() {
const traverser = this.tileset._traverser;
const {skipLevelOfDetail} = traverser.options;

/*
* Tiles that are outside of the camera's frustum could be skipped if we are in 'ADD' mode
* or if we are using 'Skip Traversal' in 'REPLACE' mode.
* In 'REPLACE' and 'Base Traversal' mode, all child tiles have to be loaded and displayed,
* including ones outide of the camera frustum, so that we can hide the parent tile.
*/
const maySkipTile = this.refine === TILE_REFINEMENT.ADD || skipLevelOfDetail;

// Check if any reason to abort
if (maySkipTile && !this.isVisible && this._visible !== undefined) {
return -1;
}
if (this.contentState === TILE_CONTENT_STATE.UNLOADED) {
return -1;
}

// Based on the priority function `getPriorityReverseScreenSpaceError` in CesiumJS. Scheduling priority is based on the parent's screen space error when possible.
const parent = this.parent;
const useParentScreenSpaceError =
parent && (!maySkipTile || this._screenSpaceError === 0.0 || parent.hasTilesetContent);
const screenSpaceError = useParentScreenSpaceError
? parent._screenSpaceError
: this._screenSpaceError;

const rootScreenSpaceError = traverser.root ? traverser.root._screenSpaceError : 0.0;

// Map higher SSE to lower values (e.g. root tile is highest priority)
return Math.max(rootScreenSpaceError - screenSpaceError, 0);
}

/**
* Requests the tile's content.
* The request may not be made if the Request Scheduler can't prioritize it.
Expand Down Expand Up @@ -394,7 +432,6 @@ export default class TileHeader {
this._visible = this._visibilityPlaneMask !== CullingVolume.MASK_OUTSIDE;
this._inRequestVolume = this.insideViewerRequestVolume(frameState);

this._priority = this.lodMetricValue;
this._frameNumber = frameState.frameNumber;
this.viewportIds = viewportIds;
}
Expand Down Expand Up @@ -594,7 +631,7 @@ export default class TileHeader {
this._centerZDepth = 0;
this._screenSpaceError = 0;
this._visibilityPlaneMask = CullingVolume.MASK_INDETERMINATE;
this._visible = false;
this._visible = undefined;
this._inRequestVolume = false;

this._stackLength = 0;
Expand All @@ -609,18 +646,6 @@ export default class TileHeader {
this._priority = 0.0;
}

_getPriority() {
// Check if any reason to abort
if (!this.isVisible) {
return -1;
}
if (this.contentState === TILE_CONTENT_STATE.UNLOADED) {
return -1;
}

return Math.max(1e7 - this._priority, 0) || 0;
}

_getRefine(refine) {
// Inherit from parent tile if omitted.
return refine || (this.parent && this.parent.refine) || TILE_REFINEMENT.REPLACE;
Expand Down
16 changes: 11 additions & 5 deletions modules/tiles/src/tileset/tileset-3d.ts
Original file line number Diff line number Diff line change
Expand Up @@ -65,6 +65,7 @@ export type Tileset3DProps = {
token?: string;
attributions?: string[];
headers?: any;
maxRequests?: number;
loadTiles?: boolean;
fetchOptions?: {[key: string]: any};
basePath?: string;
Expand All @@ -84,6 +85,7 @@ type Props = {
token: string;
attributions: string[];
headers: any;
maxRequests: number;
loadTiles: boolean;
fetchOptions: {[key: string]: any};
basePath: string;
Expand All @@ -97,8 +99,11 @@ const DEFAULT_PROPS: Props = {
// A 4x4 transformation matrix this transforms the entire tileset.
modelMatrix: new Matrix4(),

// Set to true to enable experimental request throttling, for improved performance
throttleRequests: false,
// Set to false to disable network request throttling
throttleRequests: true,

// Number of simultaneous requsts, if throttleRequests is true
maxRequests: 64,

maximumMemoryUsage: 32,

Expand Down Expand Up @@ -260,7 +265,8 @@ export default class Tileset3D {
this._traverser = this._initializeTraverser();
this._cache = new TilesetCache();
this._requestScheduler = new RequestScheduler({
throttleRequests: this.options.throttleRequests
throttleRequests: this.options.throttleRequests,
maxRequests: this.options.maxRequests
});
// update tracker
// increase in each update cycle
Expand Down Expand Up @@ -506,7 +512,7 @@ export default class Tileset3D {
let tilesRenderable = 0;
let pointsRenderable = 0;
for (const tile of this.selectedTiles) {
if (tile.contentAvailable) {
if (tile.contentAvailable && tile.content) {
tilesRenderable++;
if (tile.content.pointCount) {
pointsRenderable += tile.content.pointCount;
Expand Down Expand Up @@ -685,7 +691,7 @@ export default class Tileset3D {
}

_unloadTile(tile) {
this.gpuMemoryUsageInBytes -= tile.content.byteLength || 0;
this.gpuMemoryUsageInBytes -= (tile.content && tile.content.byteLength) || 0;

this.stats.get(TILES_IN_MEMORY).decrementCount();
this.stats.get(TILES_UNLOADED).incrementCount();
Expand Down
83 changes: 32 additions & 51 deletions modules/tiles/src/tileset/traversers/tileset-traverser.ts
Original file line number Diff line number Diff line change
Expand Up @@ -99,6 +99,7 @@ export default class TilesetTraverser {
// stack to store traversed tiles, only visible tiles should be added to stack
// visible: visible in the current view frustum
const stack = this._traversalStack;
root._selectionDepth = 1;

stack.push(root);
while (stack.length > 0) {
Expand All @@ -109,7 +110,12 @@ export default class TilesetTraverser {
let shouldRefine = false;
if (this.canTraverse(tile, frameState)) {
this.updateChildTiles(tile, frameState);
shouldRefine = this.updateAndPushChildren(tile, frameState, stack);
shouldRefine = this.updateAndPushChildren(
tile,
frameState,
stack,
tile.hasRenderContent ? tile._selectionDepth + 1 : tile._selectionDepth
);
}

// 3. decide if should render (select) this tile
Expand Down Expand Up @@ -161,7 +167,7 @@ export default class TilesetTraverser {
}

/* eslint-disable complexity, max-statements */
updateAndPushChildren(tile, frameState, stack) {
updateAndPushChildren(tile, frameState, stack, depth) {
const {loadSiblings, skipLevelOfDetail} = this.options;

const children = tile.children;
Expand All @@ -172,10 +178,13 @@ export default class TilesetTraverser {
// For traditional replacement refinement only refine if all children are loaded.
// Empty tiles are exempt since it looks better if children stream in as they are loaded to fill the empty space.
const checkRefines =
!skipLevelOfDetail && tile.refine === TILE_REFINEMENT.REPLACE && tile.hasRenderContent;
tile.refine === TILE_REFINEMENT.REPLACE && tile.hasRenderContent && !skipLevelOfDetail;

let hasVisibleChild = false;
let refines = true;

for (const child of children) {
child._selectionDepth = depth;
if (child.isVisibleAndInRequestVolume) {
if (stack.find(child)) {
stack.delete(child);
Expand All @@ -198,14 +207,18 @@ export default class TilesetTraverser {
} else {
childRefines = child.contentAvailable;
}
refines = refines && childRefines;

if (!childRefines) {
return childRefines;
if (!refines) {
return false;
}
}
}

return hasVisibleChild;
if (!hasVisibleChild) {
refines = false;
}
return refines;
}
/* eslint-enable complexity, max-statements */

Expand All @@ -226,7 +239,7 @@ export default class TilesetTraverser {
loadTile(tile, frameState) {
if (this.shouldLoadTile(tile, frameState)) {
tile._requestedFrame = frameState.frameNumber;
tile._priority = this.getPriority(tile);
tile._priority = tile._getPriority();
this.requestedTiles[tile.id] = tile;
}
}
Expand All @@ -241,10 +254,6 @@ export default class TilesetTraverser {
// tile should have children
// tile LoD (level of detail) is not sufficient under current viewport
canTraverse(tile, frameState, useParentMetric = false, ignoreVisibility = false) {
if (!ignoreVisibility && !tile.isVisibleAndInRequestVolume) {
return false;
}

if (!tile.hasChildren) {
return false;
}
Expand All @@ -256,6 +265,10 @@ export default class TilesetTraverser {
return !tile.contentExpired;
}

if (!ignoreVisibility && !tile.isVisibleAndInRequestVolume) {
return false;
}

return this.shouldRefine(tile, frameState, useParentMetric);
}

Expand Down Expand Up @@ -302,36 +315,6 @@ export default class TilesetTraverser {
return b._distanceToCamera - a._distanceToCamera;
}

// If skipLevelOfDetail is off try to load child tiles as soon as possible so that their parent can refine sooner.
// Additive tiles are prioritized by distance because it subjectively looks better.
// Replacement tiles are prioritized by screen space error.
// A tileset that has both additive and replacement tiles may not prioritize tiles as effectively since SSE and distance
// are different types of values. Maybe all priorities need to be normalized to 0-1 range.
// TODO move to tile-3d-header
getPriority(tile) {
const {options} = this;
switch (tile.refine) {
case TILE_REFINEMENT.ADD:
return tile._distanceToCamera;

case TILE_REFINEMENT.REPLACE:
const {parent} = tile;
const useParentScreenSpaceError =
parent &&
(!options.skipLevelOfDetail ||
tile._screenSpaceError === 0.0 ||
parent.hasTilesetContent);
const screenSpaceError = useParentScreenSpaceError
? parent._screenSpaceError
: tile._screenSpaceError;
const rootScreenSpaceError = this.root._screenSpaceError;
return rootScreenSpaceError - screenSpaceError; // Map higher SSE to lower values (e.g. root tile is highest priority)

default:
return assert(false);
}
}

anyChildrenVisible(tile, frameState) {
let anyVisible = false;
for (const child of tile.children) {
Expand All @@ -341,42 +324,40 @@ export default class TilesetTraverser {
return anyVisible;
}

// TODO revisit this empty traversal logic
// Depth-first traversal that checks if all nearest descendants with content are loaded.
// Ignores visibility.
executeEmptyTraversal(root, frameState) {
let allDescendantsLoaded = true;
const stack = this._emptyTraversalStack;

while (stack.length > 0) {
stack.push(root);

while (stack.length > 0 && allDescendantsLoaded) {
const tile = stack.pop();

this.updateTile(tile, frameState);

if (!tile.isVisibleAndInRequestVolume) {
// Load tiles that aren't visible since they are still needed for the parent to refine
this.loadTile(tile, frameState);
this.touchTile(tile, frameState);
}

this.touchTile(tile, frameState);

// Only traverse if the tile is empty - traversal stop at descendants with content
const traverse = !tile.hasRenderContent && this.canTraverse(tile, frameState, false, true);

// Traversal stops but the tile does not have content yet.
// There will be holes if the parent tries to refine to its children, so don't refine.
if (!traverse && !tile.contentAvailable) {
allDescendantsLoaded = false;
}

if (traverse) {
const children = tile.children.filter((c) => c);
const children = tile.children;
for (const child of children) {
// eslint-disable-next-line max-depth
if (stack.find(child)) {
stack.delete(child);
}
stack.push(child);
}
} else if (!tile.contentAvailable) {
allDescendantsLoaded = false;
}
}

Expand Down

0 comments on commit 5e4d910

Please sign in to comment.