diff --git a/CHANGES.md b/CHANGES.md index ed244a0b0c4a..6d3b39acd5a7 100644 --- a/CHANGES.md +++ b/CHANGES.md @@ -12,6 +12,7 @@ - Improved performance and reduced memory usage of `Event` class. [#12896](https://github.com/CesiumGS/cesium/pull/12896) - Fixes vertical misalignment of glyphs in labels with small fonts [#8474](https://github.com/CesiumGS/cesium/issues/8474) - Prevent runtime errors for certain forms of invalid PNTS files [#12872](https://github.com/CesiumGS/cesium/issues/12872) +- Improved performance of terrain Quadtree handling of custom data [#12907](https://github.com/CesiumGS/cesium/pull/12907) #### Additions :tada: diff --git a/packages/engine/Source/Scene/GlobeSurfaceTile.js b/packages/engine/Source/Scene/GlobeSurfaceTile.js index 0571b9e363ee..039295b4a58f 100644 --- a/packages/engine/Source/Scene/GlobeSurfaceTile.js +++ b/packages/engine/Source/Scene/GlobeSurfaceTile.js @@ -493,10 +493,8 @@ GlobeSurfaceTile.prototype.updateExaggeration = function ( if (quadtree !== undefined) { quadtree._tileToUpdateHeights.push(tile); const customData = tile.customData; - const customDataLength = customData.length; - for (let i = 0; i < customDataLength; i++) { + for (const data of customData) { // Restart the level so that a height update is triggered - const data = customData[i]; data.level = -1; } } diff --git a/packages/engine/Source/Scene/QuadtreePrimitive.js b/packages/engine/Source/Scene/QuadtreePrimitive.js index fa8930c0baa1..7b1d56b211bf 100644 --- a/packages/engine/Source/Scene/QuadtreePrimitive.js +++ b/packages/engine/Source/Scene/QuadtreePrimitive.js @@ -90,7 +90,6 @@ function QuadtreePrimitive(options) { this._removeHeightCallbacks = []; this._tileToUpdateHeights = []; - this._lastTileIndex = 0; this._updateHeightsTimeSlice = 2.0; // If a culled tile contains _cameraPositionCartographic or _cameraReferenceFrameOriginCartographic, it will be marked @@ -217,10 +216,8 @@ function invalidateAllTiles(primitive) { for (let i = 0; i < levelZeroTiles.length; ++i) { const tile = levelZeroTiles[i]; const customData = tile.customData; - const customDataLength = customData.length; - for (let j = 0; j < customDataLength; ++j) { - const data = customData[j]; + for (const data of customData) { data.level = 0; primitive._addHeightCallbacks.push(data); } @@ -513,7 +510,6 @@ function selectTilesForRendering(primitive, frameState) { tilesToRender.length = 0; // We can't render anything before the level zero tiles exist. - let i; const tileProvider = primitive._tileProvider; if (!defined(primitive._levelZeroTiles)) { const tilingScheme = tileProvider.tilingScheme; @@ -524,7 +520,7 @@ function selectTilesForRendering(primitive, frameState) { const numberOfRootTiles = primitive._levelZeroTiles.length; if (rootTraversalDetails.length < numberOfRootTiles) { rootTraversalDetails = new Array(numberOfRootTiles); - for (i = 0; i < numberOfRootTiles; ++i) { + for (let i = 0; i < numberOfRootTiles; ++i) { if (rootTraversalDetails[i] === undefined) { rootTraversalDetails[i] = new TraversalDetails(); } @@ -537,7 +533,6 @@ function selectTilesForRendering(primitive, frameState) { primitive._occluders.ellipsoid.cameraPosition = frameState.camera.positionWC; - let tile; const levelZeroTiles = primitive._levelZeroTiles; const occluders = levelZeroTiles.length > 1 ? primitive._occluders : undefined; @@ -550,18 +545,28 @@ function selectTilesForRendering(primitive, frameState) { const customDataAdded = primitive._addHeightCallbacks; const customDataRemoved = primitive._removeHeightCallbacks; - const frameNumber = frameState.frameNumber; - let 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.forEach((data) => { + const tile = levelZeroTiles.find((tile) => + Rectangle.contains(tile.rectangle, data.positionCartographic), + ); + if (tile) { + tile._addedCustomData.push(data); } + }); - customDataAdded.length = 0; - customDataRemoved.length = 0; - } + customDataRemoved.forEach((data) => { + const tile = levelZeroTiles.find((tile) => + Rectangle.contains(tile.rectangle, data.positionCartographic), + ); + if (tile) { + tile._removedCustomData.push(data); + } + }); + + levelZeroTiles.forEach((tile) => tile._updateCustomData()); + customDataAdded.length = 0; + customDataRemoved.length = 0; const camera = frameState.camera; @@ -577,8 +582,8 @@ function selectTilesForRendering(primitive, frameState) { ); // Traverse in depth-first, near-to-far order. - for (i = 0, len = levelZeroTiles.length; i < len; ++i) { - tile = levelZeroTiles[i]; + for (let i = 0; i < levelZeroTiles.length; ++i) { + const tile = levelZeroTiles[i]; primitive._tileReplacementQueue.markTileRendered(tile); if (!tile.renderable) { queueTileLoad(primitive, primitive._tileLoadQueueHigh, tile, frameState); @@ -596,7 +601,7 @@ function selectTilesForRendering(primitive, frameState) { } } - primitive._lastSelectionFrameNumber = frameNumber; + primitive._lastSelectionFrameNumber = frameState.frameNumber; } function queueTileLoad(primitive, queue, tile, frameState) { @@ -716,7 +721,7 @@ function visitTile( ++debug.tilesVisited; primitive._tileReplacementQueue.markTileRendered(tile); - tile._updateCustomData(frameState.frameNumber); + tile._updateCustomData(); if (tile.level > debug.maxDepthVisited) { debug.maxDepthVisited = tile.level; @@ -1417,15 +1422,18 @@ function updateHeights(primitive, frameState) { // Ensure stale position cache is cleared tile.clearPositionCache(); tilesToUpdateHeights.shift(); - primitive._lastTileIndex = 0; continue; } const customData = tile.customData; - const customDataLength = customData.length; + if (!defined(tile._customDataIterator)) { + tile._customDataIterator = customData.values(); + } + const customDataIterator = tile._customDataIterator; let timeSliceMax = false; - for (i = primitive._lastTileIndex; i < customDataLength; ++i) { - const data = customData[i]; + let nextData; + while (!(nextData = customDataIterator.next()).done) { + const data = nextData.value; // No need to run this code when the tile is upsampled, because the height will be the same as its parent. const terrainData = tile.data.terrainData; @@ -1543,10 +1551,10 @@ function updateHeights(primitive, frameState) { } if (timeSliceMax) { - primitive._lastTileIndex = i; + tile._customDataIterator = customDataIterator; break; } else { - primitive._lastTileIndex = 0; + tile._customDataIterator = undefined; tilesToUpdateHeights.shift(); } } diff --git a/packages/engine/Source/Scene/QuadtreeTile.js b/packages/engine/Source/Scene/QuadtreeTile.js index 8caea12b52fd..ee7b64a64f74 100644 --- a/packages/engine/Source/Scene/QuadtreeTile.js +++ b/packages/engine/Source/Scene/QuadtreeTile.js @@ -1,6 +1,7 @@ import defined from "../Core/defined.js"; import DeveloperError from "../Core/DeveloperError.js"; import Rectangle from "../Core/Rectangle.js"; +import Cartographic from "../Core/Cartographic.js"; import QuadtreeTileLoadState from "./QuadtreeTileLoadState.js"; import TileSelectionResult from "./TileSelectionResult.js"; @@ -107,8 +108,9 @@ function QuadtreeTile(options) { this._distance = 0.0; this._loadPriority = 0.0; - this._customData = []; - this._frameUpdated = undefined; + this._customData = new Set(); + this._addedCustomData = []; + this._removedCustomData = []; this._lastSelectionResult = TileSelectionResult.NONE; this._lastSelectionResultFrame = undefined; this._loadedCallbacks = {}; @@ -311,52 +313,62 @@ QuadtreeTile.prototype.clearPositionCache = function () { } }; -QuadtreeTile.prototype._updateCustomData = function ( - frameNumber, - added, - removed, -) { - let customData = this.customData; - - let i; - let data; - let rectangle; - - if (defined(added) && defined(removed)) { - customData = customData.filter(function (value) { - return removed.indexOf(value) === -1; - }); - this._customData = customData; - - rectangle = this._rectangle; - for (i = 0; i < added.length; ++i) { - data = added[i]; - if (Rectangle.contains(rectangle, data.positionCartographic)) { - customData.push(data); - } - } +QuadtreeTile.prototype._updateCustomData = function () { + const added = this._addedCustomData; + const removed = this._removedCustomData; + if (added.length === 0 && removed.length === 0) { + return; + } - this._frameUpdated = frameNumber; - } else { - // interior or leaf tile, update from parent - const parent = this._parent; - if (defined(parent) && this._frameUpdated !== parent._frameUpdated) { - customData.length = 0; - - rectangle = this._rectangle; - const parentCustomData = parent.customData; - for (i = 0; i < parentCustomData.length; ++i) { - data = parentCustomData[i]; - if (Rectangle.contains(rectangle, data.positionCartographic)) { - customData.push(data); - } - } + const customData = this.customData; + for (let i = 0; i < added.length; ++i) { + const data = added[i]; + customData.add(data); + + const child = childTileAtPosition(this, data.positionCartographic); + child._addedCustomData.push(data); + } + this._addedCustomData.length = 0; - this._frameUpdated = parent._frameUpdated; + for (let i = 0; i < removed.length; ++i) { + const data = removed[i]; + if (customData.has(data)) { + customData.delete(data); } + + const child = childTileAtPosition(this, data.positionCartographic); + child._removedCustomData.push(data); } + this._removedCustomData.length = 0; }; +const centerScratch = new Cartographic(); + +/** + * Determines which child tile that contains the specified position. Assumes the position is within + * the bounds of the parent tile. + * @private + * @param {QuadtreeTile} tile - The parent tile. + * @param {Cartographic} positionCartographic - The cartographic position. + * @returns {QuadtreeTile} The child tile that contains the position. + */ +function childTileAtPosition(tile, positionCartographic) { + const center = Rectangle.center(tile.rectangle, centerScratch); + const x = positionCartographic.longitude >= center.longitude ? 1 : 0; + const y = positionCartographic.latitude < center.latitude ? 1 : 0; + + switch (y * 2 + x) { + case 0: + return tile.northwestChild; + case 1: + return tile.northeastChild; + case 2: + return tile.southwestChild; + default: + return tile.southeastChild; + } +} + Object.defineProperties(QuadtreeTile.prototype, { /** * Gets the tiling scheme used to tile the surface. @@ -522,9 +534,9 @@ Object.defineProperties(QuadtreeTile.prototype, { }, /** - * An array of objects associated with this tile. + * A set of objects associated with this tile. * @memberof QuadtreeTile.prototype - * @type {Array} + * @type {Set} */ customData: { get: function () { diff --git a/packages/engine/Specs/Scene/QuadtreePrimitiveSpec.js b/packages/engine/Specs/Scene/QuadtreePrimitiveSpec.js index 9dd4e5336c54..b6a5806ac6a0 100644 --- a/packages/engine/Specs/Scene/QuadtreePrimitiveSpec.js +++ b/packages/engine/Specs/Scene/QuadtreePrimitiveSpec.js @@ -899,7 +899,7 @@ describe("Scene/QuadtreePrimitive", function () { let addedCallback = false; quadtree.forEachLoadedTile(function (tile) { - addedCallback = addedCallback || tile.customData.length > 0; + addedCallback = addedCallback || tile.customData.size > 0; }); expect(addedCallback).toEqual(true); @@ -915,7 +915,7 @@ describe("Scene/QuadtreePrimitive", function () { let removedCallback = true; quadtree.forEachLoadedTile(function (tile) { - removedCallback = removedCallback && tile.customData.length === 0; + removedCallback = removedCallback && tile.customData.size === 0; }); expect(removedCallback).toEqual(true);