UNPKG

@loaders.gl/gltf

Version:

Framework-independent loader for the glTF format

656 lines 28.3 kB
import { GLTFScenegraph } from "../api/gltf-scenegraph.js"; import { convertRawBufferToMetadataArray, getPrimitiveTextureData, primitivePropertyDataToAttributes, getArrayElementByteSize, getOffsetsForProperty, parseVariableLengthArrayNumeric, parseFixedLengthArrayNumeric, getPropertyDataString } from "./utils/3d-tiles-utils.js"; import { ensureArrayBuffer } from '@loaders.gl/loader-utils'; const EXT_STRUCTURAL_METADATA_NAME = 'EXT_structural_metadata'; export const name = EXT_STRUCTURAL_METADATA_NAME; export async function decode(gltfData, options) { const scenegraph = new GLTFScenegraph(gltfData); decodeExtStructuralMetadata(scenegraph, options); } export function encode(gltfData, options) { const scenegraph = new GLTFScenegraph(gltfData); encodeExtStructuralMetadata(scenegraph, options); scenegraph.createBinaryChunk(); return scenegraph.gltf; } /* // Example of the extension. // See more info at https://github.com/CesiumGS/glTF/tree/3d-tiles-next/extensions/2.0/Vendor/EXT_structural_metadata const extensions = { "extensions": { "EXT_structural_metadata": { "schema": { "classes": { "tree": { "name": "Tree", "description": "Woody, perennial plant.", "properties": { "species": { "description": "Type of tree.", "type": "ENUM", "enumType": "speciesEnum", "required": true }, "age": { "description": "The age of the tree, in years", "type": "SCALAR", "componentType": "UINT8", "required": true } } } }, "enums": { "speciesEnum": { "name": "Species", "description": "An example enum for tree species.", // valueType is not defined here. Default is "UINT16" "values": [ { "name": "Unspecified", "value": 0 }, { "name": "Oak", "value": 1 } ] } } }, "propertyTables": [{ "name": "tree_survey_2021-09-29", "class": "tree", "count": 10, // The number of elements in each property array (in `species`, in `age`). "properties": { "species": { "values": 0, // It's an index of the buffer view containing property values. }, "age": { "values": 1 } } }] } } } */ /** * Decodes feature metadata from extension. * @param scenegraph - Instance of the class for structured access to GLTF data. * @param options - GLTFLoader options. */ function decodeExtStructuralMetadata(scenegraph, options) { // Decoding metadata involves buffers processing. // So, if buffers have not been loaded, there is no reason to process metadata. if (!options.gltf?.loadBuffers) { return; } const extension = scenegraph.getExtension(EXT_STRUCTURAL_METADATA_NAME); if (!extension) { return; } if (options.gltf?.loadImages) { decodePropertyTextures(scenegraph, extension); } decodePropertyTables(scenegraph, extension); } /** * Processes the data stored in the textures * @param scenegraph - Instance of the class for structured access to GLTF data. * @param extension - Top-level extension. */ function decodePropertyTextures(scenegraph, extension) { const propertyTextures = extension.propertyTextures; const json = scenegraph.gltf.json; if (propertyTextures && json.meshes) { // Iterate through all meshes/primitives. for (const mesh of json.meshes) { for (const primitive of mesh.primitives) { processPrimitivePropertyTextures(scenegraph, propertyTextures, primitive, extension); } } } } /** * Processes the data stored in the property tables. * @param scenegraph - Instance of the class for structured access to GLTF data. * @param extension - Top-level extension. */ function decodePropertyTables(scenegraph, extension) { const schema = extension.schema; if (!schema) { return; } const schemaClasses = schema.classes; const propertyTables = extension.propertyTables; if (schemaClasses && propertyTables) { for (const schemaName in schemaClasses) { const propertyTable = findPropertyTableByClass(propertyTables, schemaName); if (propertyTable) { processPropertyTable(scenegraph, schema, propertyTable); } } } } /** * Finds the property table by class name. * @param propertyTables - propertyTable definition taken from the top-level extension. * @param schemaClassName - class name in the extension schema. */ function findPropertyTableByClass(propertyTables, schemaClassName) { for (const propertyTable of propertyTables) { if (propertyTable.class === schemaClassName) { return propertyTable; } } return null; } /** * Takes data from property textures reffered by the primitive. * @param scenegraph - Instance of the class for structured access to GLTF data. * @param propertyTextures - propertyTexture definition taken from the top-level extention. * @param primitive - Primitive object. * @param extension - Top-level extension. */ function processPrimitivePropertyTextures(scenegraph, propertyTextures, primitive, extension) { if (!propertyTextures) { return; } const primitiveExtension = primitive.extensions?.[EXT_STRUCTURAL_METADATA_NAME]; const primitivePropertyTextureIndices = primitiveExtension?.propertyTextures; if (!primitivePropertyTextureIndices) { return; } for (const primitivePropertyTextureIndex of primitivePropertyTextureIndices) { const propertyTexture = propertyTextures[primitivePropertyTextureIndex]; processPrimitivePropertyTexture(scenegraph, propertyTexture, primitive, extension); } } /** * Takes property data from the texture pointed by the primitive and appends them to `exension.data`. * @param scenegraph - Instance of the class for structured access to GLTF data. * @param propertyTexture - propertyTexture definition taken from the top-level extension. * @param primitive - Primitive object. * @param extension - Top-level extension. */ function processPrimitivePropertyTexture(scenegraph, propertyTexture, primitive, extension) { if (!propertyTexture.properties) { return; } if (!extension.dataAttributeNames) { extension.dataAttributeNames = []; } /* Iterate through all properties defined in propertyTexture, e.g. "speed" and "direction": { "class": "wind", "properties": { "speed": { "index": 0, "texCoord": 0, "channels": [0] }, "direction": { "index": 0, "texCoord": 0, "channels": [1, 2] } } } */ const className = propertyTexture.class; for (const propertyName in propertyTexture.properties) { // propertyName has values like "speed", "direction" // Make attributeName as a combination of the class name and the propertyName like "wind_speed" or "wind_direction" const attributeName = `${className}_${propertyName}`; const textureInfoTopLevel = propertyTexture.properties?.[propertyName]; if (!textureInfoTopLevel) { // eslint-disable-next-line no-continue continue; } // The data taken from all meshes/primitives (the same property, e.g. "speed" or "direction") will be combined into one array and saved in textureInfoTopLevel.data // Initially textureInfoTopLevel.data will be initialized with an empty array. if (!textureInfoTopLevel.data) { textureInfoTopLevel.data = []; } const featureTextureTable = textureInfoTopLevel.data; const propertyData = getPrimitiveTextureData(scenegraph, textureInfoTopLevel, primitive); if (propertyData === null) { // eslint-disable-next-line no-continue continue; } primitivePropertyDataToAttributes(scenegraph, attributeName, propertyData, featureTextureTable, primitive); textureInfoTopLevel.data = featureTextureTable; extension.dataAttributeNames.push(attributeName); } } /** * Navigates through all properies in the property table, gets properties data, * and put the data to `propertyTable.data` as an array. * @param scenegraph - Instance of the class for structured access to GLTF data. * @param schema - schema object. * @param propertyTable - propertyTable definition taken from the top-level extension. */ function processPropertyTable(scenegraph, schema, propertyTable) { const schemaClass = schema.classes?.[propertyTable.class]; if (!schemaClass) { throw new Error(`Incorrect data in the EXT_structural_metadata extension: no schema class with name ${propertyTable.class}`); } const numberOfElements = propertyTable.count; // `propertyTable.count` is a number of elements in each property array. for (const propertyName in schemaClass.properties) { const classProperty = schemaClass.properties[propertyName]; const propertyTableProperty = propertyTable.properties?.[propertyName]; if (propertyTableProperty) { // Getting all elements (`numberOfElements`) of the array in the `propertyTableProperty` const data = getPropertyDataFromBinarySource(scenegraph, schema, classProperty, numberOfElements, propertyTableProperty); propertyTableProperty.data = data; } } } /** * Decodes a propertyTable column from binary source based on property type. * @param scenegraph - Instance of the class for structured access to GLTF data. * @param schema - Schema object. * @param classProperty - class property object. * @param numberOfElements - The number of elements in each property array that propertyTableProperty contains. It's a number of rows in the table. * @param propertyTableProperty - propertyTable's property metadata. * @returns {string[] | number[] | string[][] | number[][]} */ function getPropertyDataFromBinarySource(scenegraph, schema, classProperty, numberOfElements, propertyTableProperty) { let data = []; const valuesBufferView = propertyTableProperty.values; const valuesDataBytes = scenegraph.getTypedArrayForBufferView(valuesBufferView); const arrayOffsets = getArrayOffsetsForProperty(scenegraph, classProperty, propertyTableProperty, numberOfElements); const stringOffsets = getStringOffsetsForProperty(scenegraph, propertyTableProperty, numberOfElements); switch (classProperty.type) { case 'SCALAR': case 'VEC2': case 'VEC3': case 'VEC4': case 'MAT2': case 'MAT3': case 'MAT4': { data = getPropertyDataNumeric(classProperty, numberOfElements, valuesDataBytes, arrayOffsets); break; } case 'BOOLEAN': { // TODO: implement it as soon as we have the corresponding tileset throw new Error(`Not implemented - classProperty.type=${classProperty.type}`); } case 'STRING': { data = getPropertyDataString(numberOfElements, valuesDataBytes, arrayOffsets, stringOffsets); break; } case 'ENUM': { data = getPropertyDataENUM(schema, classProperty, numberOfElements, valuesDataBytes, arrayOffsets); break; } default: throw new Error(`Unknown classProperty type ${classProperty.type}`); } return data; } /** * Parses propertyTable.property.arrayOffsets that are offsets of sub-arrays in a flatten array of values. * @param scenegraph - Instance of the class for structured access to GLTF data. * @param classProperty - class property object. * @param propertyTableProperty - propertyTable's property metadata. * @param numberOfElements - The number of elements in each property array that propertyTableProperty contains. It's a number of rows in the table. * @returns Typed array with offset values. * @see https://github.com/CesiumGS/glTF/blob/2976f1183343a47a29e4059a70961371cd2fcee8/extensions/2.0/Vendor/EXT_structural_metadata/schema/propertyTable.property.schema.json#L21 */ function getArrayOffsetsForProperty(scenegraph, classProperty, propertyTableProperty, numberOfElements) { if (classProperty.array && // `count` is a number of array elements. May only be defined when `array` is true. // If `count` is NOT defined, it's a VARIABLE-length array typeof classProperty.count === 'undefined' && // `arrayOffsets` is an index of the buffer view containing offsets for variable-length arrays. typeof propertyTableProperty.arrayOffsets !== 'undefined') { // Data are in a VARIABLE-length array return getOffsetsForProperty(scenegraph, propertyTableProperty.arrayOffsets, propertyTableProperty.arrayOffsetType || 'UINT32', numberOfElements); } return null; } /** * Parses propertyTable.property.stringOffsets. * @param scenegraph - Instance of the class for structured access to GLTF data. * @param propertyTableProperty - propertyTable's property metadata. * @param numberOfElements - The number of elements in each property array that propertyTableProperty contains. It's a number of rows in the table. * @returns Typed array with offset values. * @see https://github.com/CesiumGS/glTF/blob/2976f1183343a47a29e4059a70961371cd2fcee8/extensions/2.0/Vendor/EXT_structural_metadata/schema/propertyTable.property.schema.json#L29C10-L29C23 */ function getStringOffsetsForProperty(scenegraph, propertyTableProperty, numberOfElements) { if (typeof propertyTableProperty.stringOffsets !== 'undefined' // `stringOffsets` is an index of the buffer view containing offsets for strings. ) { // Data are in a FIXED-length array return getOffsetsForProperty(scenegraph, propertyTableProperty.stringOffsets, propertyTableProperty.stringOffsetType || 'UINT32', numberOfElements); } return null; } /** * Decodes properties of SCALAR, VEC-N, MAT-N types from binary sourse. * @param classProperty - class property object. * @param numberOfElements - The number of elements in each property array that propertyTableProperty contains. It's a number of rows in the table. * @param valuesDataBytes - Data taken from values property of the property table property. * @param arrayOffsets - Offsets for variable-length arrays. It's null for fixed-length arrays or scalar types. * @returns Property values in a typed array or in an array of typed arrays. */ function getPropertyDataNumeric(classProperty, numberOfElements, valuesDataBytes, arrayOffsets) { const isArray = classProperty.array; const arrayCount = classProperty.count; const elementSize = getArrayElementByteSize(classProperty.type, classProperty.componentType); const elementCount = valuesDataBytes.byteLength / elementSize; let valuesData; if (classProperty.componentType) { valuesData = convertRawBufferToMetadataArray(valuesDataBytes, classProperty.type, // The datatype of the element's components. Only applicable to `SCALAR`, `VECN`, and `MATN` types. classProperty.componentType, elementCount); } else { // The spec doesn't provide any info what to do if componentType is not set. valuesData = valuesDataBytes; } if (isArray) { if (arrayOffsets) { // VARIABLE-length array return parseVariableLengthArrayNumeric(valuesData, numberOfElements, arrayOffsets, valuesDataBytes.length, elementSize); } if (arrayCount) { // FIXED-length array return parseFixedLengthArrayNumeric(valuesData, numberOfElements, arrayCount); } return []; } return valuesData; } /** * Decodes properties of enum type from binary source. * @param schema - Schema object. * @param classProperty - Class property object. * @param numberOfElements - The number of elements in each property array that propertyTableProperty contains. It's a number of rows in the table. * @param valuesDataBytes - Data taken from values property of the property table property. * @param arrayOffsets - Offsets for variable-length arrays. It's null for fixed-length arrays or scalar types. * @returns Strings array of nested strings array. */ function getPropertyDataENUM(schema, classProperty, numberOfElements, valuesDataBytes, arrayOffsets) { const enumType = classProperty.enumType; // Enum ID as declared in the `enums` dictionary. Required when `type` is `ENUM`. if (!enumType) { throw new Error('Incorrect data in the EXT_structural_metadata extension: classProperty.enumType is not set for type ENUM'); } const enumEntry = schema.enums?.[enumType]; if (!enumEntry) { throw new Error(`Incorrect data in the EXT_structural_metadata extension: schema.enums does't contain ${enumType}`); } const enumValueType = enumEntry.valueType || 'UINT16'; const elementSize = getArrayElementByteSize(classProperty.type, enumValueType); const elementCount = valuesDataBytes.byteLength / elementSize; let valuesData = convertRawBufferToMetadataArray(valuesDataBytes, classProperty.type, enumValueType, elementCount); if (!valuesData) { valuesData = valuesDataBytes; } if (classProperty.array) { if (arrayOffsets) { // VARIABLE-length array return parseVariableLengthArrayENUM({ valuesData, numberOfElements, arrayOffsets, valuesDataBytesLength: valuesDataBytes.length, elementSize, enumEntry }); } const arrayCount = classProperty.count; if (arrayCount) { // FIXED-length array return parseFixedLengthArrayENUM(valuesData, numberOfElements, arrayCount, enumEntry); } return []; } // Single value (not an array) return getEnumsArray(valuesData, 0, numberOfElements, enumEntry); } /** * Parses variable length nested ENUM arrays. * @param params.valuesData - Values in a flat typed array. * @param params.numberOfElements - The number of elements in each property array that propertyTableProperty contains. It's a number of rows in the table. * @param params.arrayOffsets - Offsets for variable-length arrays. It's null for fixed-length arrays or scalar types. * @param params.valuesDataBytesLength - Byte length of values array. * @param params.elementSize - Single element byte size. * @param params.enumEntry - Enums dictionary. * @returns Nested strings array. */ function parseVariableLengthArrayENUM(params) { const { valuesData, numberOfElements, arrayOffsets, valuesDataBytesLength, elementSize, enumEntry } = params; const attributeValueArray = []; for (let index = 0; index < numberOfElements; index++) { const arrayOffset = arrayOffsets[index]; const arrayByteSize = arrayOffsets[index + 1] - arrayOffsets[index]; if (arrayByteSize + arrayOffset > valuesDataBytesLength) { break; } const typedArrayOffset = arrayOffset / elementSize; const elementCount = arrayByteSize / elementSize; const array = getEnumsArray(valuesData, typedArrayOffset, elementCount, enumEntry); attributeValueArray.push(array); } return attributeValueArray; } /** * Parses fixed length ENUM arrays. * @param valuesData - Values in a flat typed array. * @param numberOfElements - The number of elements in each property array that propertyTableProperty contains. It's a number of rows in the table. * @param arrayCount - Nested arrays length. * @param enumEntry - Enums dictionary. * @returns Nested strings array. */ function parseFixedLengthArrayENUM(valuesData, numberOfElements, arrayCount, enumEntry) { const attributeValueArray = []; for (let index = 0; index < numberOfElements; index++) { const elementOffset = arrayCount * index; const array = getEnumsArray(valuesData, elementOffset, arrayCount, enumEntry); attributeValueArray.push(array); } return attributeValueArray; } /** * Parses ENUM values into a string array. * @param valuesData - Values in a flat typed array. * @param offset - Offset to start parse from. * @param count - Values length to parse. * @param enumEntry - Enums dictionary. * @returns Array of strings with parsed ENUM names. */ function getEnumsArray(valuesData, offset, count, enumEntry) { const array = []; for (let i = 0; i < count; i++) { // At the moment we don't support BigInt. It requires additional calculations logic // and might be an issue in Safari if (valuesData instanceof BigInt64Array || valuesData instanceof BigUint64Array) { array.push(''); } else { const value = valuesData[offset + i]; const enumObject = getEnumByValue(enumEntry, value); if (enumObject) { array.push(enumObject.name); } else { array.push(''); } } } return array; } /** * Looks up ENUM whose `value` property matches the specified number in the parameter `value`. * @param {GLTF_EXT_structural_metadata_Enum} enumEntry - ENUM entry containing the array of possible enums. * @param {number} value - The value of the ENUM to locate. * @returns {GLTF_EXT_structural_metadata_EnumValue | null} ENUM matcihng the specified value or null of no ENUM object was found. */ function getEnumByValue(enumEntry, value) { for (const enumValue of enumEntry.values) { if (enumValue.value === value) { return enumValue; } } return null; } const SCHEMA_CLASS_ID_DEFAULT = 'schemaClassId'; function encodeExtStructuralMetadata(scenegraph, options) { const extension = scenegraph.getExtension(EXT_STRUCTURAL_METADATA_NAME); if (!extension) { return; } if (extension.propertyTables) { for (const table of extension.propertyTables) { const classId = table.class; const schemaClass = extension.schema?.classes?.[classId]; if (table.properties && schemaClass) { encodeProperties(table, schemaClass, scenegraph); } } } } function encodeProperties(table, schemaClass, scenegraph) { for (const propertyName in table.properties) { const data = table.properties[propertyName].data; if (data) { const classProperty = schemaClass.properties[propertyName]; if (classProperty) { const tableProperty = createPropertyTableProperty(data, classProperty, scenegraph); // Override table property that came with "data" table.properties[propertyName] = tableProperty; } } } } /** * Creates ExtStructuralMetadata, creates the schema and creates a property table containing feature data provided. * @param scenegraph - Instance of the class for structured access to GLTF data. * @param propertyAttributes - property attributes * @param classId - classId to use for encoding metadata. * @returns Index of the table created. */ export function createExtStructuralMetadata(scenegraph, propertyAttributes, classId = SCHEMA_CLASS_ID_DEFAULT) { let extension = scenegraph.getExtension(EXT_STRUCTURAL_METADATA_NAME); if (!extension) { extension = scenegraph.addExtension(EXT_STRUCTURAL_METADATA_NAME); } extension.schema = createSchema(propertyAttributes, classId, extension.schema); const table = createPropertyTable(propertyAttributes, classId, extension.schema); if (!extension.propertyTables) { extension.propertyTables = []; } return extension.propertyTables.push(table) - 1; // index of the table } function createSchema(propertyAttributes, classId, schemaToUpdate) { const schema = schemaToUpdate ?? { id: 'schema_id' }; const schemaClass = { properties: {} }; for (const attribute of propertyAttributes) { const classProperty = { type: attribute.elementType, componentType: attribute.componentType }; schemaClass.properties[attribute.name] = classProperty; } schema.classes = {}; schema.classes[classId] = schemaClass; return schema; } function createPropertyTable(propertyAttributes, classId, schema) { const table = { class: classId, count: 0 }; // count is a number of rows in the table let count = 0; const schemaClass = schema.classes?.[classId]; for (const attribute of propertyAttributes) { if (count === 0) { count = attribute.values.length; } // The number of elements in all propertyAttributes must be the same if (count !== attribute.values.length && attribute.values.length) { throw new Error('Illegal values in attributes'); } const classProperty = schemaClass?.properties[attribute.name]; if (classProperty) { // const tableProperty = createPropertyTableProperty(attribute, classProperty, scenegraph); if (!table.properties) { table.properties = {}; } // values is a required field. Its real value will be set while encoding data table.properties[attribute.name] = { values: 0, data: attribute.values }; } } table.count = count; return table; } function createPropertyTableProperty( // attribute: PropertyAttribute, values, classProperty, scenegraph) { const prop = { values: 0 }; if (classProperty.type === 'STRING') { const { stringData, stringOffsets } = createPropertyDataString(values); prop.stringOffsets = createBufferView(stringOffsets, scenegraph); prop.values = createBufferView(stringData, scenegraph); } else if (classProperty.type === 'SCALAR' && classProperty.componentType) { const data = createPropertyDataScalar(values, classProperty.componentType); prop.values = createBufferView(data, scenegraph); } return prop; } const COMPONENT_TYPE_TO_ARRAY_CONSTRUCTOR = { INT8: Int8Array, UINT8: Uint8Array, INT16: Int16Array, UINT16: Uint16Array, INT32: Int32Array, UINT32: Uint32Array, INT64: Int32Array, UINT64: Uint32Array, FLOAT32: Float32Array, FLOAT64: Float64Array }; function createPropertyDataScalar(array, componentType) { const numberArray = []; for (const value of array) { numberArray.push(Number(value)); } const Construct = COMPONENT_TYPE_TO_ARRAY_CONSTRUCTOR[componentType]; if (!Construct) { throw new Error('Illegal component type'); } return new Construct(numberArray); } function createPropertyDataString(strings) { const utf8Encode = new TextEncoder(); const arr = []; let len = 0; for (const str of strings) { const uint8Array = utf8Encode.encode(str); len += uint8Array.length; arr.push(uint8Array); } const strArray = new Uint8Array(len); const strOffsets = []; let offset = 0; for (const str of arr) { strArray.set(str, offset); strOffsets.push(offset); offset += str.length; } strOffsets.push(offset); // The last offset represents the byte offset after the last string. const stringOffsetsTypedArray = new Uint32Array(strOffsets); // Its length = len+1 return { stringData: strArray, stringOffsets: stringOffsetsTypedArray }; } function createBufferView(typedArray, scenegraph) { scenegraph.gltf.buffers.push({ arrayBuffer: ensureArrayBuffer(typedArray.buffer), byteOffset: typedArray.byteOffset, byteLength: typedArray.byteLength }); return scenegraph.addBufferView(typedArray); } //# sourceMappingURL=EXT_structural_metadata.js.map