Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add tile loading events for Voxels #12430

Merged
merged 14 commits into from
Jan 27, 2025
1 change: 1 addition & 0 deletions CHANGES.md
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,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
137 changes: 137 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,136 @@ function VoxelPrimitive(options) {
}
}

/**
* The event fired to indicate that a tile's content was loaded.
* <p>
* The loaded tile is passed to the event listener.
* </p>
lukemckinstry marked this conversation as resolved.
Show resolved Hide resolved
* <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}
* @default new Event()
lukemckinstry marked this conversation as resolved.
Show resolved Hide resolved
*
* @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}
* @default new Event()
lukemckinstry marked this conversation as resolved.
Show resolved Hide resolved
*
* @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.
* <p>
* If there are no event listeners, error messages will be logged to the console.
* </p>
lukemckinstry marked this conversation as resolved.
Show resolved Hide resolved
*
* @type {Event}
* @default new Event()
lukemckinstry marked this conversation as resolved.
Show resolved Hide resolved
*
* @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}
* @default new Event()
lukemckinstry marked this conversation as resolved.
Show resolved Hide resolved
*
* @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}
* @default new Event()
lukemckinstry marked this conversation as resolved.
Show resolved Hide resolved
*
* @example
* voxelPrimitive.loadProgress.addEventListener(function(numberOfPendingRequests, numberOfTilesProcessing) {
* if ((numberOfPendingRequests === 0) && (numberOfTilesProcessing === 0)) {
* console.log('Stopped loading');
lukemckinstry marked this conversation as resolved.
Show resolved Hide resolved
* 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}
* @default new Event()
lukemckinstry marked this conversation as resolved.
Show resolved Hide resolved
*
* @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}
* @default new Event()
lukemckinstry marked this conversation as resolved.
Show resolved Hide resolved
*
* @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();
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This may be a question beyond the scope of this PR, but when a provider returns undefined instead of a promise, is that actually a failure state? Typically when undefined is returned from a function like this, it signals that the request couldn't be scheduled this frame and will be tried again next frame.

CC @jjhembd

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

For both Cesium3DTilesVoxelProvider and the procedural providers in the Sandcastles, an undefined return happened when the tile didn't exist. I agree that this is not really expected behavior.

In #12432 I changed the providers to return a rejected Promise if the tile doesn't exist.

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I added a few comments in #12432 that mentioned undefined may still be a valid return value that needs to be accounted for. But, yes, it should never be a failure state.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

In #12432 I changed the providers to return a rejected Promise if the tile doesn't exist.

This change here as described above

function postRequestSuccess(result) {

This simplifies the logic on whether to raise the tileLoad or tileFailed event
Something like this at the end of the loadTileSuccess would be all that is needed.

    if (keyframeNode.state === KeyframeNode.LoadState.PROCESSING) {
      that._primitive.tileLoad.raiseEvent();
    }
    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
Loading