import Check from "../Core/Check.js"; import Frozen from "../Core/Frozen.js"; import defined from "../Core/defined.js"; import PropertyTable from "./PropertyTable.js"; import PropertyTexture from "./PropertyTexture.js"; import PropertyAttribute from "./PropertyAttribute.js"; import StructuralMetadata from "./StructuralMetadata.js"; import MetadataTable from "./MetadataTable.js"; import Sampler from "../Renderer/Sampler.js"; import Texture from "../Renderer/Texture.js"; import PixelFormat from "../Core/PixelFormat.js"; import PixelDatatype from "../Renderer/PixelDatatype.js"; import RuntimeError from "../Core/RuntimeError.js"; import oneTimeWarning from "../Core/oneTimeWarning.js"; import TextureWrap from "../Renderer/TextureWrap.js"; import TextureMagnificationFilter from "../Renderer/TextureMagnificationFilter.js"; import TextureMinificationFilter from "../Renderer/TextureMinificationFilter.js"; import ContextLimits from "../Renderer/ContextLimits.js"; import MetadataComponentType from "./MetadataComponentType.js"; import MetadataType from "./MetadataType.js"; /** * Parse the EXT_structural_metadata glTF extension to create a * structural metadata object. * * @param {object} options Object with the following properties: * @param {object} options.extension The extension JSON object. * @param {MetadataSchema} options.schema The parsed schema. * @param {Object} [options.bufferViews] An object mapping bufferView IDs to Uint8Array objects. * @param {Object} [options.textures] An object mapping texture IDs to {@link Texture} objects. * @param {Context} [options.context] The current rendering context. * @return {StructuralMetadata} A structural metadata object * @private * @experimental This feature is using part of the 3D Tiles spec that is not final and is subject to change without Cesium's standard deprecation policy. */ function parseStructuralMetadata(options) { options = options ?? Frozen.EMPTY_OBJECT; const extension = options.extension; // The calling code is responsible for loading the schema. // This keeps metadata parsing synchronous. const schema = options.schema; //>>includeStart('debug', pragmas.debug); Check.typeOf.object("options.extension", extension); Check.typeOf.object("options.schema", schema); //>>includeEnd('debug'); const propertyTables = []; if (defined(extension.propertyTables)) { for (let i = 0; i < extension.propertyTables.length; i++) { const propertyTable = extension.propertyTables[i]; const classDefinition = schema.classes[propertyTable.class]; const propertyTableTexture = createTextureForPropertyTable( propertyTable, options.bufferViews, classDefinition, options.context, ); const metadataTable = new MetadataTable({ count: propertyTable.count, properties: propertyTable.properties, class: classDefinition, bufferViews: options.bufferViews, }); propertyTables.push( new PropertyTable({ id: i, name: propertyTable.name, count: propertyTable.count, metadataTable: metadataTable, extras: propertyTable.extras, extensions: propertyTable.extensions, texture: propertyTableTexture, }), ); } } const propertyTextures = []; if (defined(extension.propertyTextures)) { for (let i = 0; i < extension.propertyTextures.length; i++) { const propertyTexture = extension.propertyTextures[i]; propertyTextures.push( new PropertyTexture({ id: i, name: propertyTexture.name, propertyTexture: propertyTexture, class: schema.classes[propertyTexture.class], textures: options.textures, }), ); } } const propertyAttributes = []; if (defined(extension.propertyAttributes)) { for (let i = 0; i < extension.propertyAttributes.length; i++) { const propertyAttribute = extension.propertyAttributes[i]; propertyAttributes.push( new PropertyAttribute({ id: i, name: propertyAttribute.name, class: schema.classes[propertyAttribute.class], propertyAttribute: propertyAttribute, }), ); } } return new StructuralMetadata({ schema: schema, propertyTables: propertyTables, propertyTextures: propertyTextures, propertyAttributes: propertyAttributes, statistics: extension.statistics, extras: extension.extras, extensions: extension.extensions, }); } // Always use four channels for property table textures. const NUM_CHANNELS = 4; /** * Creates a texture from a set of property table properties (those which are GPU compatible). * Each row of the texture is a property, with each column corresponding to a given feature. * * @param {PropertyTable} propertyTable The property table. * @param {Object} bufferViews An object mapping bufferView IDs to Uint8Array objects for the given property table. * @param {MetadataClass} classDefinition Class defined in the schema * @param {Context} context The rendering context. * @returns {Texture|undefined} The created texture, or undefined if no properties are GPU compatible. * * @private */ function createTextureForPropertyTable( propertyTable, bufferViews, classDefinition, context, ) { const properties = propertyTable.properties; if (!defined(properties)) { return undefined; } const numFeatures = propertyTable.count; let gpuCompatiblePropertyInfo; try { gpuCompatiblePropertyInfo = collectGpuCompatiblePropertyInfo( properties, bufferViews, classDefinition, numFeatures, ); } catch (error) { console.warn( `Failed to create texture for property table "${propertyTable.name}": ${error.message}`, ); return undefined; } const numGpuCompatibleProperties = gpuCompatiblePropertyInfo.length; if (numGpuCompatibleProperties === 0) { return undefined; } // In the future, we could use multiple textures if we would exceed the maximum texture size. if ( numFeatures > ContextLimits.maximumTextureSize || numGpuCompatibleProperties > ContextLimits.maximumTextureSize ) { oneTimeWarning( "PropertyTableTextureExceedsMaximumSize", `Cannot create a texture for the property table "${propertyTable.name}" because it exceeds the maximum texture size of ${ContextLimits.maximumTextureSize}.`, ); return undefined; } const packedBufferView = packPropertyTablePropertiesIntoRGBA8( gpuCompatiblePropertyInfo, numFeatures, ); // Create a sampler fit for sampling raw data without mipmapping / filtering etc. const sampler = new Sampler({ wrapS: TextureWrap.CLAMP_TO_EDGE, wrapT: TextureWrap.CLAMP_TO_EDGE, minificationFilter: TextureMinificationFilter.NEAREST, magnificationFilter: TextureMagnificationFilter.NEAREST, }); return Texture.create({ context: context, pixelFormat: PixelFormat.RGBA, pixelDatatype: PixelDatatype.UNSIGNED_BYTE, sampler: sampler, flipY: false, source: { width: numFeatures, height: numGpuCompatibleProperties, arrayBufferView: packedBufferView, }, }); } function collectGpuCompatiblePropertyInfo( properties, bufferViews, classDefinition, numFeatures, ) { const propertyInfos = []; const classProperties = classDefinition.properties; // It's possible for a primitive in a tileset to only use a subset of the class properties defined in the schema. // For instance, the Design Tiler merges classes together by default, and omits properties in primitives that aren't used. // To make the default values available to the GPU, and to avoid compiling different versions of the shader for each primitive, // we iterate over _all_ class properties here - not just the properties in the property table. for (const [propertyId, classProperty] of Object.entries(classProperties)) { // Certain properties like strings, dynamic-sized arrays, and 64-bit types cannot be represented natively on the GPU. if (!classProperty.isGpuCompatible(NUM_CHANNELS)) { continue; } const property = properties[propertyId]; const bufferView = defined(property) ? bufferViews[property.values] : createNoDataBufferView(classProperty, numFeatures); const bufferViewLength = bufferView.length; const bytesPerElement = classProperty.cpuBytesPerElement(); const numBufferElements = bufferViewLength / bytesPerElement; if (numBufferElements !== numFeatures) { throw new RuntimeError( `Property with ID: "${propertyId}" has (${numBufferElements}), which does not match number of features in the property table: (${numFeatures}).`, ); } propertyInfos.push({ view: bufferView, classProperty: classProperty, }); } return propertyInfos; } /** * When a property is part of a tileset class schema but not used in a property table, * we create a buffer view filled with the property's noData value. * * @param {MetadataClassProperty} classProperty The class property definition. * @param {number} numFeatures The number of features in the property table. * * @returns {Uint8Array} A buffer view filled with the property's noData value. * * @private */ function createNoDataBufferView(classProperty, numFeatures) { // noData can be a number, an array of numbers (e.g. for vecN types), or a nested array of numbers (e.g. for arrays of vecN types). let noData = classProperty.noData; const metadataComponentCount = MetadataType.getComponentCount( classProperty.type, ); const metadataArrayLength = classProperty.isArray ? classProperty.arrayLength : 1; // Special case: noData enum values are specified as strings, so we need to convert them to numbers here. if (classProperty.type === MetadataType.ENUM) { const enumDefinition = classProperty.enumType; noData = enumDefinition.valuesByName[noData]; } // Wrap noData in an array (up to two times) so we can treat it uniformly in the loop below. if (metadataComponentCount === 1) { noData = [noData]; } if (metadataArrayLength === 1) { noData = [noData]; } const bytesPerElement = classProperty.cpuBytesPerElement(); const bytesPerComponent = MetadataComponentType.getSizeInBytes( classProperty.valueType, ); const buffer = new ArrayBuffer(bytesPerElement * numFeatures); const view = new DataView(buffer); const accessors = MetadataComponentType.getDataViewAccessors( view, classProperty.valueType, ); for (let i = 0; i < numFeatures; i++) { for (let j = 0; j < metadataArrayLength; j++) { for (let k = 0; k < metadataComponentCount; k++) { const componentIdx = j * metadataComponentCount + k; accessors.set( bytesPerElement * i + componentIdx * bytesPerComponent, noData[j][k], ); } } } return new Uint8Array(buffer); } // Make one big buffer view to load into the texture // Since each texel is always 4 bytes (RGBA8 format), elements less than 4 bytes need to be padded (respecting little-endian order). // Exception: single-component 64-bit types can be downcast to 32-bit for GPU compatibility (with potential loss of precision / range). function packPropertyTablePropertiesIntoRGBA8(propertyInfos, numFeatures) { const numGpuCompatibleProperties = propertyInfos.length; const packedBufferView = new Uint8Array( numGpuCompatibleProperties * numFeatures * NUM_CHANNELS, ); const packedDataView = new DataView( packedBufferView.buffer, packedBufferView.byteOffset, packedBufferView.byteLength, ); for ( let propertyIndex = 0; propertyIndex < numGpuCompatibleProperties; propertyIndex++ ) { const propertyInfo = propertyInfos[propertyIndex]; const classProperty = propertyInfo.classProperty; const rowOffset = propertyIndex * numFeatures * NUM_CHANNELS; const sourceType = classProperty.valueType; const packedType = MetadataComponentType.gpuComponentType(sourceType); // E.g. When the source component type is INT64, we first downcast each element to INT32 before packing into the GPU buffer. if (sourceType !== packedType) { downcastAndPackProperty(propertyInfo, packedDataView, rowOffset); continue; } packProperty(propertyInfo, packedBufferView, rowOffset); } return packedBufferView; } function packProperty(propertyInfo, packedBufferView, rowOffset) { const bufferView = propertyInfo.view; const bytesPerElement = propertyInfo.classProperty.cpuBytesPerElement(); const numElements = bufferView.length / bytesPerElement; for (let elementIndex = 0; elementIndex < numElements; elementIndex++) { const sourceOffset = elementIndex * bytesPerElement; const destinationOffset = rowOffset + elementIndex * NUM_CHANNELS; packedBufferView.set( bufferView.subarray(sourceOffset, sourceOffset + bytesPerElement), destinationOffset, ); } } // This is the slow path - rather than doing a straight copy, we need to interpret and downcast each element before packing it into the GPU buffer. // Note: this function does not handle properties with multiple components per element (or arrays). While not complete, this is OK because // multi-component properties with 64-bit types - even when downcast to 32 bits - cannot fit into a single RGBA8 texel. function downcastAndPackProperty(propertyInfo, packedDataView, rowOffset) { const classProperty = propertyInfo.classProperty; const bufferView = propertyInfo.view; const sourceType = classProperty.valueType; const packedType = MetadataComponentType.gpuComponentType(sourceType); const bytesPerElement = classProperty.cpuBytesPerElement(); const numElements = bufferView.length / bytesPerElement; const sourceDataView = new DataView( bufferView.buffer, bufferView.byteOffset, bufferView.byteLength, ); const sourceAccessors = MetadataComponentType.getDataViewAccessors( sourceDataView, sourceType, ); const packedAccessors = MetadataComponentType.getDataViewAccessors( packedDataView, packedType, ); const downcastFunction = MetadataComponentType.downcastFunction(sourceType); for (let elementIndex = 0; elementIndex < numElements; elementIndex++) { const sourceElementOffset = elementIndex * bytesPerElement; const destinationElementOffset = rowOffset + elementIndex * NUM_CHANNELS; const value = sourceAccessors.get(sourceElementOffset); packedAccessors.set(destinationElementOffset, downcastFunction(value)); } } export default parseStructuralMetadata;