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

Open
wants to merge 12 commits into
base: main
Choose a base branch
from
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>
Comment on lines +462 to +464
Copy link
Contributor

Choose a reason for hiding this comment

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

Suggested change
* <p>
* The loaded tile is passed to the event listener.
* </p>

This is raised with no arguments since the nodes are still private API, correct?

* <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()
Copy link
Contributor

Choose a reason for hiding this comment

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

Suggested change
* @default new Event()

I don't think this should be needed. Strictly speaking, we should probably prevent the public API from reassigning this at all by providing only a public getter. But this can remain consistent with what we have over in Cesium3DTileset.

*
* @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()
Copy link
Contributor

Choose a reason for hiding this comment

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

Suggested change
* @default new Event()

Same as above.

*
* @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>
Comment on lines +499 to +501
Copy link
Contributor

Choose a reason for hiding this comment

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

Is this accurate? Please remove this line if not.

*
* @type {Event}
* @default new Event()
Copy link
Contributor

Choose a reason for hiding this comment

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

Suggested change
* @default new 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}
* @default new Event()
Copy link
Contributor

Choose a reason for hiding this comment

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

Suggested change
* @default new 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}
* @default new Event()
Copy link
Contributor

Choose a reason for hiding this comment

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

Suggested change
* @default new Event()

*
* @example
* voxelPrimitive.loadProgress.addEventListener(function(numberOfPendingRequests, numberOfTilesProcessing) {
* if ((numberOfPendingRequests === 0) && (numberOfTilesProcessing === 0)) {
* console.log('Stopped loading');
Copy link
Contributor

Choose a reason for hiding this comment

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

Nitpick:

Suggested change
* console.log('Stopped loading');
* 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}
* @default new Event()
Copy link
Contributor

Choose a reason for hiding this comment

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

Suggested change
* @default new 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}
* @default new Event()
Copy link
Contributor

Choose a reason for hiding this comment

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

Suggested change
* @default new 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();
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

}
}

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