407 lines
14 KiB
JavaScript
407 lines
14 KiB
JavaScript
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;
|