From 20ac82c398b591e77b6c293bc1a84bfb60c0e514 Mon Sep 17 00:00:00 2001 From: Jeshurun Hembd Date: Wed, 22 Jan 2025 15:02:22 -0500 Subject: [PATCH] Move glTF handling from Cesium3DTilesVoxelProvider to VoxelContent --- .../Scene/Cesium3DTilesVoxelProvider.js | 67 +----- packages/engine/Source/Scene/KeyframeNode.js | 13 +- packages/engine/Source/Scene/SpatialNode.js | 5 +- packages/engine/Source/Scene/VoxelContent.js | 194 +++++++++++++++++- packages/engine/Source/Scene/VoxelProvider.js | 4 +- .../engine/Source/Scene/VoxelTraversal.js | 16 +- 6 files changed, 205 insertions(+), 94 deletions(-) diff --git a/packages/engine/Source/Scene/Cesium3DTilesVoxelProvider.js b/packages/engine/Source/Scene/Cesium3DTilesVoxelProvider.js index fe10eab066cf..49b3fd8496f9 100644 --- a/packages/engine/Source/Scene/Cesium3DTilesVoxelProvider.js +++ b/packages/engine/Source/Scene/Cesium3DTilesVoxelProvider.js @@ -4,7 +4,6 @@ import Check from "../Core/Check.js"; import defaultValue from "../Core/defaultValue.js"; import defined from "../Core/defined.js"; import Ellipsoid from "../Core/Ellipsoid.js"; -import GltfLoader from "./GltfLoader.js"; import hasExtension from "./hasExtension.js"; import ImplicitSubtree from "./ImplicitSubtree.js"; import ImplicitSubtreeCache from "./ImplicitSubtreeCache.js"; @@ -21,8 +20,7 @@ import RuntimeError from "../Core/RuntimeError.js"; import VoxelBoxShape from "./VoxelBoxShape.js"; import VoxelCylinderShape from "./VoxelCylinderShape.js"; import VoxelShapeType from "./VoxelShapeType.js"; -import MetadataComponentType from "./MetadataComponentType.js"; -import ComponentDatatype from "../Core/ComponentDatatype.js"; +import VoxelContent from "./VoxelContent.js"; /** * A {@link VoxelProvider} that fetches voxel data from a 3D Tiles tileset. @@ -474,7 +472,7 @@ async function getSubtree(provider, subtreeCoord) { } /** - * Requests the data for a given tile. The data is a flattened 3D array ordered by X, then Y, then Z. + * Requests the data for a given tile. * * @private * @@ -483,9 +481,8 @@ async function getSubtree(provider, subtreeCoord) { * @param {number} [options.tileX=0] The tile's X coordinate. * @param {number} [options.tileY=0] The tile's Y coordinate. * @param {number} [options.tileZ=0] The tile's Z coordinate. - * @param {FrameState} options.frameState The frame state * @privateparam {number} [options.keyframe=0] The requested keyframe. - * @returns {Promise|undefined} A promise to an array of typed arrays containing the requested voxel data or undefined if there was a problem loading the data. + * @returns {Promise} A promise resolving to a VoxelContent containing the data for the tile. */ Cesium3DTilesVoxelProvider.prototype.requestData = async function (options) { options = defaultValue(options, defaultValue.EMPTY_OBJECT); @@ -498,10 +495,6 @@ Cesium3DTilesVoxelProvider.prototype.requestData = async function (options) { frameState, } = options; - //>>includeStart('debug', pragmas.debug); - Check.typeOf.object("options.frameState", frameState); - //>>includeEnd('debug'); - if (keyframe !== 0) { return Promise.reject( `3D Tiles currently doesn't support time-dynamic data.`, @@ -554,59 +547,7 @@ Cesium3DTilesVoxelProvider.prototype.requestData = async function (options) { url: gltfRelative.url, }); - const gltfLoader = new GltfLoader({ - gltfResource: gltfResource, - releaseGltfJson: false, - loadAttributesAsTypedArray: true, - }); - - await gltfLoader.load(); - gltfLoader.process(frameState); - await gltfLoader._loadResourcesPromise; - gltfLoader.process(frameState); - - const { attributes } = gltfLoader.components.scene.nodes[0].primitives[0]; - return processAttributes(attributes, that); + return VoxelContent.fromGltf(gltfResource, this, frameState); }; -/** - * Processes the attributes from the glTF loader, reordering them into the order expected by the primitive. - * TODO: use a MetadataTable? - * - * @param {ModelComponents.Attribute[]} attributes The attributes to process. - * @param {VoxelProvider} provider The provider from which these attributes were received. - * @returns {TypedArray[]} An array of typed arrays containing the attribute values. - * @private - */ -function processAttributes(attributes, provider) { - const { names, types, componentTypes } = provider; - const data = new Array(attributes.length); - - for (let i = 0; i < attributes.length; i++) { - // The attributes array from GltfLoader is not in the same order as - // names, types, etc. from the provider. - // Find the appropriate glTF attribute based on its name. - // Note: glTF custom attribute names are prefixed with "_" - const name = `_${names[i]}`; - const attribute = attributes.find((a) => a.name === name); - if (!defined(attribute)) { - continue; - } - - const componentDatatype = MetadataComponentType.toComponentDatatype( - componentTypes[i], - ); - const componentCount = MetadataType.getComponentCount(types[i]); - const totalCount = attribute.count * componentCount; - data[i] = ComponentDatatype.createArrayBufferView( - componentDatatype, - attribute.typedArray.buffer, - attribute.typedArray.byteOffset + attribute.byteOffset, - totalCount, - ); - } - - return data; -} - export default Cesium3DTilesVoxelProvider; diff --git a/packages/engine/Source/Scene/KeyframeNode.js b/packages/engine/Source/Scene/KeyframeNode.js index 607c591fe35f..2c255500ed50 100644 --- a/packages/engine/Source/Scene/KeyframeNode.js +++ b/packages/engine/Source/Scene/KeyframeNode.js @@ -13,7 +13,6 @@ const LoadState = Object.freeze({ * * @param {SpatialNode} spatialNode * @param {number} keyframe - * // TODO: add contentResource param? Or does that go in VoxelContent? * * @private */ @@ -21,27 +20,19 @@ function KeyframeNode(spatialNode, keyframe) { this.spatialNode = spatialNode; this.keyframe = keyframe; this.state = LoadState.UNLOADED; - // TODO: switch to .content - this.metadata = []; - //this.content = undefined; - //this.contentResource = contentResource; + this.content = undefined; this.megatextureIndex = -1; this.priority = -Number.MAX_VALUE; this.highPriorityFrameNumber = -1; } -//KeyframeNode.prototype.requestContent = function() { -//}; - /** * Frees the resources used by this object. * TODO: replace with a destroy method? * @private */ KeyframeNode.prototype.unload = function () { - // TODO: switch to .content - //this.content = this.content && this.content.destroy(); - this.metadata = []; + this.content = this.content && this.content.destroy(); this.spatialNode = undefined; this.state = LoadState.UNLOADED; diff --git a/packages/engine/Source/Scene/SpatialNode.js b/packages/engine/Source/Scene/SpatialNode.js index 57f29ee6d650..099777d264c6 100644 --- a/packages/engine/Source/Scene/SpatialNode.js +++ b/packages/engine/Source/Scene/SpatialNode.js @@ -333,14 +333,15 @@ SpatialNode.prototype.addKeyframeNodeToMegatextures = function ( ) { if ( keyframeNode.megatextureIndex !== -1 || - keyframeNode.metadata.length !== megatextures.length + keyframeNode.content.metadata.length !== megatextures.length ) { throw new DeveloperError("Keyframe node cannot be added to megatexture"); } + const { metadata } = keyframeNode.content; for (let i = 0; i < megatextures.length; i++) { const megatexture = megatextures[i]; - keyframeNode.megatextureIndex = megatexture.add(keyframeNode.metadata[i]); + keyframeNode.megatextureIndex = megatexture.add(metadata[i]); } const renderableKeyframeNodes = this.renderableKeyframeNodes; diff --git a/packages/engine/Source/Scene/VoxelContent.js b/packages/engine/Source/Scene/VoxelContent.js index 451be03d2493..a4516b961c6b 100644 --- a/packages/engine/Source/Scene/VoxelContent.js +++ b/packages/engine/Source/Scene/VoxelContent.js @@ -1,44 +1,197 @@ +import { destroyObject } from "@cesium/engine"; import Check from "../Core/Check.js"; +import ComponentDatatype from "../Core/ComponentDatatype.js"; import DeveloperError from "../Core/DeveloperError.js"; import defined from "../Core/defined.js"; import getJsonFromTypedArray from "../Core/getJsonFromTypedArray.js"; +import GltfLoader from "./GltfLoader.js"; +import MetadataComponentType from "./MetadataComponentType.js"; import MetadataTable from "./MetadataTable.js"; +import MetadataType from "./MetadataType.js"; /** + *
+ * To construct a VoxelContent, call {@link VoxelContent.fromGltf}. Do not call the constructor directly. + *
* An object representing voxel content for a {@link Cesium3DTilesVoxelProvider}. * * @alias VoxelContent * @constructor * - * @param {Resource} resource The resource for this voxel content. This is used for fetching external buffers as needed. + * @param {object} options An object with the following properties: + * @param {Resource} [options.resource] The resource for this voxel content. This is used for fetching external buffers as needed. + * @param {ResourceLoader} [options.loader] The loader used to load the voxel content. + * @param {TypedArray[]} [options.metadata] The metadata for this voxel content. + * + * @exception {DeveloperError} One of loader and metadata must be defined. + * @exception {DeveloperError} metadata must be an array of TypedArrays. * * @private * @experimental This feature is not final and is subject to change without Cesium's standard deprecation policy. */ -function VoxelContent(resource) { +function VoxelContent(options) { + const { resource, loader, metadata } = options; //>>includeStart('debug', pragmas.debug); - Check.typeOf.object("resource", resource); + Check.typeOf.object("options", options); + if (!defined(loader)) { + if (!defined(metadata)) { + throw new DeveloperError("One of loader and metadata must be defined."); + } + if (!Array.isArray(metadata)) { + throw new DeveloperError("metadata must be an array of TypedArrays."); + } + } //>>includeEnd('debug'); + // TODO: do we need resource? this._resource = resource; - this._metadataTable = undefined; + this._loader = loader; + + this._metadata = metadata; + this._resourcesLoaded = false; + this._ready = false; } Object.defineProperties(VoxelContent.prototype, { /** - * The {@link MetadataTable} storing voxel property values. + * Returns true when the content is ready to render; otherwise false + * + * @memberof VoxelContent.prototype * - * @type {MetadataTable} + * @type {boolean} * @readonly * @private */ - metadataTable: { + ready: { get: function () { - return this._metadataTable; + return this._ready; + }, + }, + + /** + * The metadata for this voxel content. + * The metadata is an array of typed arrays, one for each field. + * The data for one field is a flattened 3D array ordered by X, then Y, then Z. + * TODO: use a MetadataTable + * @type {TypedArray[]} + * @readonly + */ + metadata: { + get: function () { + return this._metadata; }, }, }); +VoxelContent.fromGltf = async function (resource, provider, frameState) { + //>>includeStart('debug', pragmas.debug); + Check.typeOf.object("resource", resource); + //>>includeEnd('debug'); + + // Construct the glTF loader + const loader = new GltfLoader({ + gltfResource: resource, + releaseGltfJson: false, + loadAttributesAsTypedArray: true, + }); + + try { + // This loads the gltf JSON and ensures the gltf is valid + // Further resource loading is handled synchronously in loader.process() + // via voxelContent.update() as the frameState is needed + await loader.load(); + } catch (error) { + loader.destroy(); + throw error; + } + + await loader.load(); + loader.process(frameState); + await loader._loadResourcesPromise; + loader.process(frameState); + + const metadata = processAttributes( + loader.components.scene.nodes[0].primitives[0].attributes, + provider, + ); + return new VoxelContent({ resource, loader, metadata }); +}; + +/** + * Updates the content until all resources are ready for rendering. + * @param {FrameState} frameState The frame state + * @private + */ +VoxelContent.prototype.update = function (primitive, frameState) { + const loader = this._loader; + + if (this._ready) { + // Nothing to do + return; + } + + // Ensures frames continue to render in requestRender mode while resources are processing + frameState.afterRender.push(() => true); + + if (!defined(loader)) { + this._ready = true; + return; + } + + if (this._resourcesLoaded) { + // TODO: load to megatexture? + const { attributes } = loader.components.scene.nodes[0].primitives[0]; + this._metadata = processAttributes(attributes, primitive); + this._ready = true; + return; + } + + // TODO: handle errors from GltfLoader.prototype.process + this._resourcesLoaded = loader.process(frameState); +}; + +/** + * Processes the attributes from the glTF loader, reordering them into the order expected by the primitive. + * TODO: use a MetadataTable? + * + * @param {ModelComponents.Attribute[]} attributes The attributes to process + * @param {VoxelPrimitive} primitive The primitive for which this voxel content will be used. + * @returns {TypedArray[]} An array of typed arrays containing the attribute values + * @private + */ +function processAttributes(attributes, provider) { + //function processAttributes(attributes, primitive) { + //const { names, types, componentTypes } = primitive.provider; + const { names, types, componentTypes } = provider; + const data = new Array(attributes.length); + + for (let i = 0; i < attributes.length; i++) { + // The attributes array from GltfLoader is not in the same order as + // names, types, etc. from the provider. + // Find the appropriate glTF attribute based on its name. + // Note: glTF custom attribute names are prefixed with "_" + const name = `_${names[i]}`; + const attribute = attributes.find((a) => a.name === name); + if (!defined(attribute)) { + continue; + } + + const componentDatatype = MetadataComponentType.toComponentDatatype( + componentTypes[i], + ); + const componentCount = MetadataType.getComponentCount(types[i]); + const totalCount = attribute.count * componentCount; + data[i] = ComponentDatatype.createArrayBufferView( + componentDatatype, + attribute.typedArray.buffer, + attribute.typedArray.byteOffset + attribute.byteOffset, + totalCount, + ); + } + + return data; +} + /** * Creates an object representing voxel content for a {@link Cesium3DTilesVoxelProvider}. * @@ -164,4 +317,29 @@ function parseVoxelChunks(binaryView) { }; } +/** + * Returns true if this object was destroyed; otherwise, false. + *

+ * If this object was destroyed, it should not be used; calling any function other than + * isDestroyed will result in a {@link DeveloperError} exception. + * + * @returns {boolean} true if this object was destroyed; otherwise, false. + * + * @see VoxelContent#destroy + * + * @private + */ +VoxelContent.prototype.isDestroyed = function () { + return false; +}; + +/** + * Frees the resources used by this object. + * @private + */ +VoxelContent.prototype.destroy = function () { + this._loader = this._loader && this._loader.destroy(); + return destroyObject(this); +}; + export default VoxelContent; diff --git a/packages/engine/Source/Scene/VoxelProvider.js b/packages/engine/Source/Scene/VoxelProvider.js index 8a7c80ea03b6..d2f667f7493f 100644 --- a/packages/engine/Source/Scene/VoxelProvider.js +++ b/packages/engine/Source/Scene/VoxelProvider.js @@ -204,7 +204,6 @@ Object.defineProperties(VoxelProvider.prototype, { /** * Requests the data for a given tile. - * The data is a flattened 3D array ordered by X, then Y, then Z. * * @private * @@ -213,9 +212,8 @@ Object.defineProperties(VoxelProvider.prototype, { * @param {number} [options.tileX=0] The tile's X coordinate. * @param {number} [options.tileY=0] The tile's Y coordinate. * @param {number} [options.tileZ=0] The tile's Z coordinate. - * @param {FrameState} options.frameState The frame state * @privateparam {number} [options.keyframe=0] The requested keyframe. - * @returns {Promise|undefined} A promise to an array of typed arrays containing the requested voxel data or undefined if there was a problem loading the data. + * @returns {Promise} A promise resolving to a VoxelContent containing the data for the tile. */ VoxelProvider.prototype.requestData = DeveloperError.throwInstantiationError; diff --git a/packages/engine/Source/Scene/VoxelTraversal.js b/packages/engine/Source/Scene/VoxelTraversal.js index 5d9da05ed359..7a71464e7f18 100644 --- a/packages/engine/Source/Scene/VoxelTraversal.js +++ b/packages/engine/Source/Scene/VoxelTraversal.js @@ -434,24 +434,26 @@ function requestData(that, keyframeNode, frameState) { if (!defined(result)) { keyframeNode.state = KeyframeNode.LoadState.UNAVAILABLE; - } else if (!Array.isArray(result) || result.length !== length) { + //} else if (!Array.isArray(result) || result.length !== length) { // TODO should this throw runtime error? - keyframeNode.state = KeyframeNode.LoadState.FAILED; + // TODO what if result is a VoxelContent? How do we check the metadata? + // keyframeNode.state = KeyframeNode.LoadState.FAILED; } else { const megatextures = that.megatextures; + keyframeNode.content = result; + keyframeNode.state = KeyframeNode.LoadState.RECEIVED; + const { metadata } = result; for (let i = 0; i < length; i++) { const { voxelCountPerTile, channelCount } = megatextures[i]; const { x, y, z } = voxelCountPerTile; const tileVoxelCount = x * y * z; - const data = result[i]; + const data = metadata[i]; const expectedLength = tileVoxelCount * channelCount; - if (data.length === expectedLength) { - keyframeNode.metadata[i] = data; + if (data.length !== expectedLength) { // State is received only when all metadata requests have been received - keyframeNode.state = KeyframeNode.LoadState.RECEIVED; - } else { keyframeNode.state = KeyframeNode.LoadState.FAILED; + keyframeNode.content = undefined; break; } }