| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406 |
- 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 <code>EXT_structural_metadata</code> 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<string, Uint8Array>} [options.bufferViews] An object mapping bufferView IDs to Uint8Array objects.
- * @param {Object<string, Texture>} [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<string, Uint8Array>} 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 <code>undefined</code> 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;
|