Skip to content

Commit

Permalink
Merge pull request #12430 from CesiumGS/issue-12297-voxel-stats
Browse files Browse the repository at this point in the history
Add tile loading events for Voxels
  • Loading branch information
ggetz authored Jan 27, 2025
2 parents 3c5afc8 + f3149a3 commit 4e6dde5
Show file tree
Hide file tree
Showing 5 changed files with 268 additions and 13 deletions.
1 change: 1 addition & 0 deletions CHANGES.md
Original file line number Diff line number Diff line change
Expand Up @@ -26,6 +26,7 @@

- Expanded integration with the [iTwin Platform](https://developer.bentley.com/) to load GeoJSON and KML data from the Reality Management API. Use `ITwinData.createDataSourceForRealityDataId` to load data as either GeoJSON or KML`. [#12344](https://github.com/CesiumGS/cesium/pull/12344)
- Added `environmentMapOptions` to `ModelGraphics`. For performance reasons by default, the environment map will not update if the entity position change. If environment map updates based on entity position are desired, provide an appropriate `environmentMapOptions.maximumPositionEpsilon` value. [#12358](https://github.com/CesiumGS/cesium/pull/12358)
- Added events to `VoxelPrimitive` to match `Cesium3DTileset`, including `allTilesLoaded`, `initialTilesLoaded`, `loadProgress`, `tileFailed`, `tileLoad`, `tileVisible`, `tileUnload`.

#### Fixes :wrench:

Expand Down
124 changes: 124 additions & 0 deletions packages/engine/Source/Scene/VoxelPrimitive.js
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,7 @@ import CustomShader from "./Model/CustomShader.js";
import Cartographic from "../Core/Cartographic.js";
import Ellipsoid from "../Core/Ellipsoid.js";
import VerticalExaggeration from "../Core/VerticalExaggeration.js";
import Cesium3DTilesetStatistics from "./Cesium3DTilesetStatistics.js";

/**
* A primitive that renders voxel data from a {@link VoxelProvider}.
Expand Down Expand Up @@ -70,6 +71,12 @@ function VoxelPrimitive(options) {
*/
this._traversal = undefined;

/**
* @type {Cesium3DTilesetStatistics}
* @private
*/
this._statistics = new Cesium3DTilesetStatistics();

/**
* This member is not created until the provider is ready.
*
Expand Down Expand Up @@ -450,6 +457,123 @@ function VoxelPrimitive(options) {
}
}

/**
* The event fired to indicate that a tile's content was loaded.
* <p>
* This event is fired during the tileset traversal while the frame is being rendered
* so that updates to the tile take effect in the same frame. Do not create or modify
* Cesium entities or primitives during the event listener.
* </p>
*
* @type {Event}
*
* @example
* voxelPrimitive.tileLoad.addEventListener(function() {
* console.log('A tile was loaded.');
* });
*/
this.tileLoad = new Event();

/**
* This event fires once for each visible tile in a frame.
* <p>
* This event is fired during the traversal while the frame is being rendered.
*
* @type {Event}
*
* @example
* voxelPrimitive.tileVisible.addEventListener(function() {
* console.log('A tile is visible.');
* });
*
*/
this.tileVisible = new Event();

/**
* The event fired to indicate that a tile's content failed to load.
*
* @type {Event}
*
* @example
* voxelPrimitive.tileFailed.addEventListener(function() {
* console.log('An error occurred loading tile.');
* });
*/
this.tileFailed = new Event();

/**
* The event fired to indicate that a tile's content was unloaded.
*
* @type {Event}
*
* @example
* voxelPrimitive.tileUnload.addEventListener(function() {
* console.log('A tile was unloaded from the cache.');
* });
*
*/
this.tileUnload = new Event();

/**
* The event fired to indicate progress of loading new tiles. This event is fired when a new tile
* is requested, when a requested tile is finished downloading, and when a downloaded tile has been
* processed and is ready to render.
* <p>
* The number of pending tile requests, <code>numberOfPendingRequests</code>, and number of tiles
* processing, <code>numberOfTilesProcessing</code> are passed to the event listener.
* </p>
* <p>
* This event is fired at the end of the frame after the scene is rendered.
* </p>
*
* @type {Event}
*
* @example
* voxelPrimitive.loadProgress.addEventListener(function(numberOfPendingRequests, numberOfTilesProcessing) {
* if ((numberOfPendingRequests === 0) && (numberOfTilesProcessing === 0)) {
* console.log('Finished loading');
* return;
* }
*
* console.log(`Loading: requests: ${numberOfPendingRequests}, processing: ${numberOfTilesProcessing}`);
* });
*/
this.loadProgress = new Event();

/**
* The event fired to indicate that all tiles that meet the screen space error this frame are loaded. The voxel
* primitive is completely loaded for this view.
* <p>
* This event is fired at the end of the frame after the scene is rendered.
* </p>
*
* @type {Event}
*
* @example
* voxelPrimitive.allTilesLoaded.addEventListener(function() {
* console.log('All tiles are loaded');
* });
*/
this.allTilesLoaded = new Event();

/**
* The event fired to indicate that all tiles that meet the screen space error this frame are loaded. This event
* is fired once when all tiles in the initial view are loaded.
* <p>
* This event is fired at the end of the frame after the scene is rendered.
* </p>
*
* @type {Event}
*
* @example
* voxelPrimitive.initialTilesLoaded.addEventListener(function() {
* console.log('Initial tiles are loaded');
* });
*
* @see Cesium3DTileset#allTilesLoaded
*/
this.initialTilesLoaded = new Event();

// If the provider fails to initialize the primitive will fail too.
const provider = this._provider;
initialize(this, provider);
Expand Down
97 changes: 85 additions & 12 deletions packages/engine/Source/Scene/VoxelTraversal.js
Original file line number Diff line number Diff line change
Expand Up @@ -143,6 +143,12 @@ function VoxelTraversal(
*/
this._binaryTreeKeyframeWeighting = new Array(keyframeCount);

/**
* @type {boolean}
* @private
*/
this._initialTilesLoaded = false;

const binaryTreeKeyframeWeighting = this._binaryTreeKeyframeWeighting;
binaryTreeKeyframeWeighting[0] = 0;
binaryTreeKeyframeWeighting[keyframeCount - 1] = 0;
Expand Down Expand Up @@ -316,13 +322,17 @@ VoxelTraversal.prototype.update = function (
const timestamp1 = getTimestamp();
generateOctree(this, sampleCount, levelBlendFactor);
const timestamp2 = getTimestamp();

if (this._debugPrint) {
const checkEventListeners =
primitive.loadProgress.numberOfListeners > 0 ||
primitive.allTilesLoaded.numberOfListeners > 0 ||
primitive.initialTilesLoaded.numberOfListeners > 0;
if (this._debugPrint || checkEventListeners) {
const loadAndUnloadTimeMs = timestamp1 - timestamp0;
const generateOctreeTimeMs = timestamp2 - timestamp1;
const totalTimeMs = timestamp2 - timestamp0;
printDebugInformation(
postPassesUpdate(
this,
frameState,
loadAndUnloadTimeMs,
generateOctreeTimeMs,
totalTimeMs,
Expand Down Expand Up @@ -418,6 +428,18 @@ function requestData(that, keyframeNode) {
}

const provider = that._primitive._provider;
const { keyframe, spatialNode } = keyframeNode;
if (spatialNode.level >= provider._implicitTileset.availableLevels) {
return;
}

const requestOptions = {
tileLevel: spatialNode.level,
tileX: spatialNode.x,
tileY: spatialNode.y,
tileZ: spatialNode.z,
keyframe: keyframe,
};

function postRequestSuccess(result) {
that._simultaneousRequestCount--;
Expand All @@ -443,34 +465,33 @@ function requestData(that, keyframeNode) {
keyframeNode.metadata[i] = data;
// State is received only when all metadata requests have been received
keyframeNode.state = KeyframeNode.LoadState.RECEIVED;
that._primitive.tileLoad.raiseEvent();
} else {
keyframeNode.state = KeyframeNode.LoadState.FAILED;
break;
}
}
}
if (keyframeNode.state === KeyframeNode.LoadState.FAILED) {
that._primitive.tileFailed.raiseEvent();
}
}

function postRequestFailure() {
that._simultaneousRequestCount--;
keyframeNode.state = KeyframeNode.LoadState.FAILED;
that._primitive.tileFailed.raiseEvent();
}

const { keyframe, spatialNode } = keyframeNode;
const promise = provider.requestData({
tileLevel: spatialNode.level,
tileX: spatialNode.x,
tileY: spatialNode.y,
tileZ: spatialNode.z,
keyframe: keyframe,
});
const promise = provider.requestData(requestOptions);

if (defined(promise)) {
that._simultaneousRequestCount++;
keyframeNode.state = KeyframeNode.LoadState.RECEIVING;
promise.then(postRequestSuccess).catch(postRequestFailure);
} else {
keyframeNode.state = KeyframeNode.LoadState.FAILED;
that._primitive.tileFailed.raiseEvent();
}
}

Expand Down Expand Up @@ -645,6 +666,7 @@ function loadAndUnload(that, frameState) {
destroyedCount++;

const discardNode = keyframeNodesInMegatexture[addNodeIndex];
that._primitive.tileUnload.raiseEvent();
discardNode.spatialNode.destroyKeyframeNode(
discardNode,
that.megatextures,
Expand Down Expand Up @@ -703,8 +725,9 @@ function keyframePriority(previousKeyframe, keyframe, nextKeyframe, traversal) {
*
* @private
*/
function printDebugInformation(
function postPassesUpdate(
that,
frameState,
loadAndUnloadTimeMs,
generateOctreeTimeMs,
totalTimeMs,
Expand Down Expand Up @@ -758,6 +781,55 @@ function printDebugInformation(
}
traverseRecursive(rootNode);

const numberOfPendingRequests =
loadStateByCount[KeyframeNode.LoadState.RECEIVING];
const numberOfTilesProcessing =
loadStateByCount[KeyframeNode.LoadState.RECEIVED];

const progressChanged =
numberOfPendingRequests !==
that._primitive._statistics.numberOfPendingRequests ||
numberOfTilesProcessing !==
that._primitive._statistics.numberOfTilesProcessing;

if (progressChanged) {
frameState.afterRender.push(function () {
that._primitive.loadProgress.raiseEvent(
numberOfPendingRequests,
numberOfTilesProcessing,
);

return true;
});
}

that._primitive._statistics.numberOfPendingRequests = numberOfPendingRequests;
that._primitive._statistics.numberOfTilesProcessing = numberOfTilesProcessing;

const tilesLoaded =
numberOfPendingRequests === 0 && numberOfTilesProcessing === 0;

// Events are raised (added to the afterRender queue) here since promises
// may resolve outside of the update loop that then raise events, e.g.,
// model's readyEvent
if (progressChanged && tilesLoaded) {
frameState.afterRender.push(function () {
that._primitive.allTilesLoaded.raiseEvent();
return true;
});
if (!that._initialTilesLoaded) {
that._initialTilesLoaded = true;
frameState.afterRender.push(function () {
that._primitive.initialTilesLoaded.raiseEvent();
return true;
});
}
}

if (!that._debugPrint) {
return;
}

const loadedKeyframeStatistics = `KEYFRAMES: ${
loadStatesByKeyframe[KeyframeNode.LoadState.LOADED]
}`;
Expand Down Expand Up @@ -892,6 +964,7 @@ function generateOctree(that, sampleCount, levelBlendFactor) {
} else {
// Store the leaf node information instead
// Recursion stops here because there are no renderable children
that._primitive.tileVisible.raiseEvent();
if (useLeafNodes) {
const baseIdx = leafNodeCount * 5;
const keyframeNode = node.renderableKeyframeNodePrevious;
Expand Down
33 changes: 33 additions & 0 deletions packages/engine/Specs/Scene/VoxelPrimitiveSpec.js
Original file line number Diff line number Diff line change
Expand Up @@ -58,6 +58,39 @@ describe(
expect(primitive.maximumValues).toBe(provider.maximumValues);
});

it("initial tiles loaded and all tiles loaded events are raised", async function () {
const spyUpdate1 = jasmine.createSpy("listener");
const spyUpdate2 = jasmine.createSpy("listener");
const primitive = new VoxelPrimitive({ provider });
primitive.allTilesLoaded.addEventListener(spyUpdate1);
primitive.initialTilesLoaded.addEventListener(spyUpdate2);
scene.primitives.add(primitive);
await pollToPromise(() => {
scene.renderForSpecs();
return primitive._traversal._initialTilesLoaded;
});
expect(spyUpdate1.calls.count()).toEqual(1);
expect(spyUpdate2.calls.count()).toEqual(1);
});

it("tile load, load progress and tile visible events are raised", async function () {
const spyUpdate1 = jasmine.createSpy("listener");
const spyUpdate2 = jasmine.createSpy("listener");
const spyUpdate3 = jasmine.createSpy("listener");
const primitive = new VoxelPrimitive({ provider });
primitive.tileLoad.addEventListener(spyUpdate1);
primitive.loadProgress.addEventListener(spyUpdate2);
primitive.tileVisible.addEventListener(spyUpdate3);
scene.primitives.add(primitive);
await pollToPromise(() => {
scene.renderForSpecs();
return primitive._traversal._initialTilesLoaded;
});
expect(spyUpdate1.calls.count()).toEqual(1);
expect(spyUpdate2.calls.count()).toBeGreaterThan(0);
expect(spyUpdate3.calls.count()).toEqual(1);
});

it("toggles render options that require shader rebuilds", async function () {
const primitive = new VoxelPrimitive({ provider });
scene.primitives.add(primitive);
Expand Down
Loading

0 comments on commit 4e6dde5

Please sign in to comment.