diff --git a/CHANGES.md b/CHANGES.md index 5c745014715..80837d33416 100644 --- a/CHANGES.md +++ b/CHANGES.md @@ -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: diff --git a/packages/engine/Source/Scene/VoxelPrimitive.js b/packages/engine/Source/Scene/VoxelPrimitive.js index 0942fbb817a..fe52ee7e6a9 100644 --- a/packages/engine/Source/Scene/VoxelPrimitive.js +++ b/packages/engine/Source/Scene/VoxelPrimitive.js @@ -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}. @@ -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. * @@ -450,6 +457,136 @@ function VoxelPrimitive(options) { } } + /** + * The event fired to indicate that a tile's content was loaded. + *

+ * The loaded tile is passed to the event listener. + *

+ *

+ * 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. + *

+ * + * @type {Event} + * @default new 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. + *

+ * This event is fired during the traversal while the frame is being rendered. + * + * @type {Event} + * @default new 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. + *

+ * If there are no event listeners, error messages will be logged to the console. + *

+ * + * @type {Event} + * @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() + * + * @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. + *

+ * The number of pending tile requests, numberOfPendingRequests, and number of tiles + * processing, numberOfTilesProcessing are passed to the event listener. + *

+ *

+ * This event is fired at the end of the frame after the scene is rendered. + *

+ * + * @type {Event} + * @default new Event() + * + * @example + * voxelPrimitive.loadProgress.addEventListener(function(numberOfPendingRequests, numberOfTilesProcessing) { + * if ((numberOfPendingRequests === 0) && (numberOfTilesProcessing === 0)) { + * console.log('Stopped 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. + *

+ * This event is fired at the end of the frame after the scene is rendered. + *

+ * + * @type {Event} + * @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. + *

+ * This event is fired at the end of the frame after the scene is rendered. + *

+ * + * @type {Event} + * @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); diff --git a/packages/engine/Source/Scene/VoxelTraversal.js b/packages/engine/Source/Scene/VoxelTraversal.js index b7e02d06f1d..b5ba12db606 100644 --- a/packages/engine/Source/Scene/VoxelTraversal.js +++ b/packages/engine/Source/Scene/VoxelTraversal.js @@ -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; @@ -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, @@ -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--; @@ -443,27 +465,25 @@ 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++; @@ -471,6 +491,7 @@ function requestData(that, keyframeNode) { promise.then(postRequestSuccess).catch(postRequestFailure); } else { keyframeNode.state = KeyframeNode.LoadState.FAILED; + that._primitive.tileFailed.raiseEvent(); } } @@ -645,6 +666,7 @@ function loadAndUnload(that, frameState) { destroyedCount++; const discardNode = keyframeNodesInMegatexture[addNodeIndex]; + that._primitive.tileUnload.raiseEvent(); discardNode.spatialNode.destroyKeyframeNode( discardNode, that.megatextures, @@ -703,8 +725,9 @@ function keyframePriority(previousKeyframe, keyframe, nextKeyframe, traversal) { * * @private */ -function printDebugInformation( +function postPassesUpdate( that, + frameState, loadAndUnloadTimeMs, generateOctreeTimeMs, totalTimeMs, @@ -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] }`; @@ -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; diff --git a/packages/engine/Specs/Scene/VoxelPrimitiveSpec.js b/packages/engine/Specs/Scene/VoxelPrimitiveSpec.js index 3c55516d198..bdd25a24c20 100644 --- a/packages/engine/Specs/Scene/VoxelPrimitiveSpec.js +++ b/packages/engine/Specs/Scene/VoxelPrimitiveSpec.js @@ -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); diff --git a/packages/engine/Specs/Scene/VoxelTraversalSpec.js b/packages/engine/Specs/Scene/VoxelTraversalSpec.js index c70e0dc1b73..233b5981ec2 100644 --- a/packages/engine/Specs/Scene/VoxelTraversalSpec.js +++ b/packages/engine/Specs/Scene/VoxelTraversalSpec.js @@ -166,6 +166,30 @@ describe( expect(megatexture.occupiedCount).toBe(1); }); + it("tile failed event is raised", async function () { + const keyFrameLocation = 0; + const recomputeBoundingVolumes = true; + const pauseUpdate = false; + const spyFailed = jasmine.createSpy("listener"); + traversal._primitive.tileFailed.addEventListener(spyFailed); + spyOn(traversal._primitive._provider, "requestData").and.callFake(() => { + return Promise.reject(); + }); + let counter = 0; + const target = 3; + await pollToPromise(function () { + traversal.update( + scene.frameState, + keyFrameLocation, + recomputeBoundingVolumes, + pauseUpdate, + ); + counter++; + return counter === target; + }); + expect(spyFailed.calls.count()).toBeGreaterThan(1); + }); + it("finds keyframe node with expected metadata values", async function () { const keyFrameLocation = 0; const recomputeBoundingVolumes = true; @@ -203,7 +227,7 @@ describe( pauseUpdate, ); } - for (let i = 0; i < 10; i++) { + for (let i = 0; i < 15; i++) { updateTraversal(); } }