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;