Add existing to tracked
This commit is contained in:
+406
@@ -0,0 +1,406 @@
|
||||
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;
|
||||
Reference in New Issue
Block a user