Merge pull request #12907 from CesiumGS/quadtree-performance

Quadtree performance
This commit is contained in:
Matt Schwartz 2025-10-13 19:58:42 +00:00 committed by GitHub
commit 3418fa936e
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
6 changed files with 154 additions and 78 deletions

View File

@ -14,6 +14,7 @@
- Fix issues with label background when updating properties while `label.show` is `false`. [#12138](https://github.com/CesiumGS/cesium/issues/12138)
- Improved performance of `scene.drillPick`. [#12916](https://github.com/CesiumGS/cesium/pull/12916)
- Improved performance when removing primitives. [#3018](https://github.com/CesiumGS/cesium/pull/3018)
- Improved performance of terrain Quadtree handling of custom data [#12907](https://github.com/CesiumGS/cesium/pull/12907)
## 1.134.1 - 2025-10-10

View File

@ -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;
}
}

View File

@ -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();
}
}

View File

@ -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,10 @@ function QuadtreeTile(options) {
this._distance = 0.0;
this._loadPriority = 0.0;
this._customData = [];
this._frameUpdated = undefined;
this._customData = new Set();
this._customDataIterator = undefined;
this._addedCustomData = [];
this._removedCustomData = [];
this._lastSelectionResult = TileSelectionResult.NONE;
this._lastSelectionResultFrame = undefined;
this._loadedCallbacks = {};
@ -311,52 +314,69 @@ 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);
}
}
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);
}
}
this._frameUpdated = parent._frameUpdated;
}
QuadtreeTile.prototype.updateCustomData = function () {
const added = this._addedCustomData;
const removed = this._removedCustomData;
if (added.length === 0 && removed.length === 0) {
return;
}
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;
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 splitPointScratch = 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) {
// Can't assume that a given tiling scheme divides a parent into four tiles at its rectangle's center.
// But we can safely take any child tile's rectangle and take its center-facing corner as the parent's split point.
const nwChildRectangle = tile.northwestChild.rectangle;
const tileSplitPoint = Rectangle.southeast(
nwChildRectangle,
splitPointScratch,
);
const x = positionCartographic.longitude >= tileSplitPoint.longitude ? 1 : 0;
const y = positionCartographic.latitude < tileSplitPoint.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 +542,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 () {

View File

@ -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);

View File

@ -458,4 +458,53 @@ describe("Scene/QuadtreeTile", function () {
expect(cachedData).toEqual(dummyPosition);
});
});
describe("updateCustomData", function () {
function addAndRemoveCustomData(tilingScheme) {
const tile = new QuadtreeTile({
level: 0,
x: 0,
y: 0,
tilingScheme: tilingScheme,
});
const child = tile.northwestChild;
const centerCartographic = Rectangle.center(child.rectangle);
const data = {
positionCartographic: centerCartographic,
};
tile._addedCustomData.push(data);
tile.updateCustomData();
expect(tile.customData.has(data)).toBe(true);
expect(tile._addedCustomData.length).toBe(0);
expect(child._addedCustomData.length).toBe(1);
expect(child._addedCustomData[0]).toBe(data);
child.updateCustomData();
expect(child.customData.has(data)).toBe(true);
// Now remove the data from the parent tile.
tile._removedCustomData.push(data);
tile.updateCustomData();
expect(tile.customData.has(data)).toBe(false);
expect(tile._removedCustomData.length).toBe(0);
expect(child._removedCustomData.length).toBe(1);
expect(child._removedCustomData[0]).toBe(data);
child.updateCustomData();
expect(child.customData.has(data)).toBe(false);
}
it("can add and remove custom data when tiling scheme is GeographicTilingScheme", function () {
addAndRemoveCustomData(new GeographicTilingScheme());
});
it("can add and remove custom data when tiling scheme is WebMercatorTilingScheme", function () {
addAndRemoveCustomData(new WebMercatorTilingScheme());
});
});
});