1853 lines
42 KiB
JavaScript
1853 lines
42 KiB
JavaScript
const textDecoder = new TextDecoder();
|
||
|
||
// Pre-computed half-float exponent lookup table for fast conversion
|
||
// Math.pow(2, exp - 15) for exp = 0..31
|
||
const HALF_EXPONENT_TABLE = new Float32Array( 32 );
|
||
for ( let i = 0; i < 32; i ++ ) {
|
||
|
||
HALF_EXPONENT_TABLE[ i ] = Math.pow( 2, i - 15 );
|
||
|
||
}
|
||
|
||
// Pre-computed constant for denormalized half-floats: 2^-14
|
||
const HALF_DENORM_SCALE = Math.pow( 2, - 14 );
|
||
|
||
// Type enum values from crateDataTypes.h
|
||
const TypeEnum = {
|
||
Invalid: 0,
|
||
Bool: 1,
|
||
UChar: 2,
|
||
Int: 3,
|
||
UInt: 4,
|
||
Int64: 5,
|
||
UInt64: 6,
|
||
Half: 7,
|
||
Float: 8,
|
||
Double: 9,
|
||
String: 10,
|
||
Token: 11,
|
||
AssetPath: 12,
|
||
Matrix2d: 13,
|
||
Matrix3d: 14,
|
||
Matrix4d: 15,
|
||
Quatd: 16,
|
||
Quatf: 17,
|
||
Quath: 18,
|
||
Vec2d: 19,
|
||
Vec2f: 20,
|
||
Vec2h: 21,
|
||
Vec2i: 22,
|
||
Vec3d: 23,
|
||
Vec3f: 24,
|
||
Vec3h: 25,
|
||
Vec3i: 26,
|
||
Vec4d: 27,
|
||
Vec4f: 28,
|
||
Vec4h: 29,
|
||
Vec4i: 30,
|
||
Dictionary: 31,
|
||
TokenListOp: 32,
|
||
StringListOp: 33,
|
||
PathListOp: 34,
|
||
ReferenceListOp: 35,
|
||
IntListOp: 36,
|
||
Int64ListOp: 37,
|
||
UIntListOp: 38,
|
||
UInt64ListOp: 39,
|
||
PathVector: 40,
|
||
TokenVector: 41,
|
||
Specifier: 42,
|
||
Permission: 43,
|
||
Variability: 44,
|
||
VariantSelectionMap: 45,
|
||
TimeSamples: 46,
|
||
Payload: 47,
|
||
DoubleVector: 48,
|
||
LayerOffsetVector: 49,
|
||
StringVector: 50,
|
||
ValueBlock: 51,
|
||
Value: 52,
|
||
UnregisteredValue: 53,
|
||
UnregisteredValueListOp: 54,
|
||
PayloadListOp: 55,
|
||
TimeCode: 56,
|
||
PathExpression: 57,
|
||
Relocates: 58,
|
||
Spline: 59,
|
||
AnimationBlock: 60
|
||
};
|
||
|
||
// Field set terminator marker
|
||
const FIELD_SET_TERMINATOR = 0xFFFFFFFF;
|
||
|
||
// Float compression type codes
|
||
const FLOAT_COMPRESSION_INT = 0x69; // 'i' - compressed as integers
|
||
const FLOAT_COMPRESSION_LUT = 0x74; // 't' - lookup table
|
||
|
||
// ============================================================================
|
||
// LZ4 Decompression (minimal implementation for USD)
|
||
// Based on LZ4 block format specification
|
||
// ============================================================================
|
||
|
||
function lz4DecompressBlock( input, inputOffset, inputEnd, output, outputOffset, outputEnd ) {
|
||
|
||
while ( inputOffset < inputEnd ) {
|
||
|
||
// Read token
|
||
const token = input[ inputOffset ++ ];
|
||
if ( inputOffset > inputEnd ) break;
|
||
|
||
// Literal length
|
||
let literalLength = token >> 4;
|
||
if ( literalLength === 15 ) {
|
||
|
||
let b;
|
||
do {
|
||
|
||
if ( inputOffset >= inputEnd ) break;
|
||
b = input[ inputOffset ++ ];
|
||
literalLength += b;
|
||
|
||
} while ( b === 255 && inputOffset < inputEnd );
|
||
|
||
}
|
||
|
||
// Copy literals
|
||
if ( literalLength > 0 ) {
|
||
|
||
if ( inputOffset + literalLength > inputEnd ) {
|
||
|
||
literalLength = inputEnd - inputOffset;
|
||
|
||
}
|
||
|
||
for ( let i = 0; i < literalLength; i ++ ) {
|
||
|
||
if ( outputOffset >= outputEnd ) break;
|
||
output[ outputOffset ++ ] = input[ inputOffset ++ ];
|
||
|
||
}
|
||
|
||
}
|
||
|
||
// Check if we're at the end (last sequence has no match)
|
||
if ( inputOffset >= inputEnd ) break;
|
||
|
||
// Read match offset (little-endian 16-bit)
|
||
if ( inputOffset + 2 > inputEnd ) break;
|
||
const matchOffset = input[ inputOffset ++ ] | ( input[ inputOffset ++ ] << 8 );
|
||
|
||
if ( matchOffset === 0 ) {
|
||
|
||
// Invalid offset
|
||
break;
|
||
|
||
}
|
||
|
||
// Match length
|
||
let matchLength = ( token & 0x0F ) + 4;
|
||
if ( matchLength === 19 ) {
|
||
|
||
let b;
|
||
do {
|
||
|
||
if ( inputOffset >= inputEnd ) break;
|
||
b = input[ inputOffset ++ ];
|
||
matchLength += b;
|
||
|
||
} while ( b === 255 && inputOffset < inputEnd );
|
||
|
||
}
|
||
|
||
// Copy match (byte-by-byte to handle overlapping)
|
||
const matchPos = outputOffset - matchOffset;
|
||
if ( matchPos < 0 ) {
|
||
|
||
// Invalid match position
|
||
break;
|
||
|
||
}
|
||
|
||
for ( let i = 0; i < matchLength; i ++ ) {
|
||
|
||
if ( outputOffset >= outputEnd ) break;
|
||
output[ outputOffset ++ ] = output[ matchPos + i ];
|
||
|
||
}
|
||
|
||
}
|
||
|
||
return outputOffset;
|
||
|
||
}
|
||
|
||
// USD uses TfFastCompression which wraps LZ4 with chunk headers
|
||
function decompressLZ4( input, uncompressedSize ) {
|
||
|
||
// TfFastCompression format (used by OpenUSD):
|
||
// Single chunk (byte 0 == 0): [0] + LZ4 data
|
||
// Multi chunk (byte 0 > 0): [numChunks] + [compressedSizes...] + [chunkData...]
|
||
|
||
const output = new Uint8Array( uncompressedSize );
|
||
const numChunks = input[ 0 ];
|
||
|
||
if ( numChunks === 0 ) {
|
||
|
||
// Single chunk - all remaining bytes are LZ4 compressed
|
||
lz4DecompressBlock( input, 1, input.length, output, 0, uncompressedSize );
|
||
return output;
|
||
|
||
} else {
|
||
|
||
// Multiple chunks - each chunk decompresses to max 65536 bytes
|
||
const CHUNK_SIZE = 65536;
|
||
|
||
// First, read all chunk sizes
|
||
let headerOffset = 1;
|
||
const compressedSizes = [];
|
||
|
||
for ( let i = 0; i < numChunks; i ++ ) {
|
||
|
||
const size = ( input[ headerOffset ] |
|
||
( input[ headerOffset + 1 ] << 8 ) |
|
||
( input[ headerOffset + 2 ] << 16 ) |
|
||
( input[ headerOffset + 3 ] << 24 ) ) >>> 0;
|
||
compressedSizes.push( size );
|
||
headerOffset += 4;
|
||
|
||
}
|
||
|
||
// Decompress each chunk
|
||
let inputOffset = headerOffset;
|
||
let outputOffset = 0;
|
||
|
||
for ( let i = 0; i < numChunks; i ++ ) {
|
||
|
||
const chunkCompressedSize = compressedSizes[ i ];
|
||
const chunkOutputSize = Math.min( CHUNK_SIZE, uncompressedSize - outputOffset );
|
||
|
||
lz4DecompressBlock(
|
||
input, inputOffset, inputOffset + chunkCompressedSize,
|
||
output, outputOffset, outputOffset + chunkOutputSize
|
||
);
|
||
|
||
inputOffset += chunkCompressedSize;
|
||
outputOffset += chunkOutputSize;
|
||
|
||
}
|
||
|
||
return output;
|
||
|
||
}
|
||
|
||
}
|
||
|
||
// ============================================================================
|
||
// Integer Decompression (USD-specific delta + variable-width encoding)
|
||
// ============================================================================
|
||
|
||
function decompressIntegers32( compressedData, numInts ) {
|
||
|
||
// First decompress with LZ4
|
||
const encodedSize = numInts * 4 + ( ( numInts * 2 + 7 ) >> 3 ) + 4;
|
||
const encoded = decompressLZ4( new Uint8Array( compressedData ), encodedSize );
|
||
|
||
// Then decode
|
||
return decodeIntegers32( encoded, numInts );
|
||
|
||
}
|
||
|
||
function decodeIntegers32( data, numInts ) {
|
||
|
||
const view = new DataView( data.buffer, data.byteOffset, data.byteLength );
|
||
let offset = 0;
|
||
|
||
// Read common value (signed 32-bit)
|
||
const commonValue = view.getInt32( offset, true );
|
||
offset += 4;
|
||
|
||
const numCodesBytes = ( numInts * 2 + 7 ) >> 3;
|
||
const codesStart = offset;
|
||
const vintsStart = offset + numCodesBytes;
|
||
|
||
const result = new Int32Array( numInts );
|
||
let prevVal = 0;
|
||
let codesOffset = codesStart;
|
||
let vintsOffset = vintsStart;
|
||
|
||
for ( let i = 0; i < numInts; ) {
|
||
|
||
const codeByte = data[ codesOffset ++ ];
|
||
|
||
for ( let j = 0; j < 4 && i < numInts; j ++, i ++ ) {
|
||
|
||
const code = ( codeByte >> ( j * 2 ) ) & 3;
|
||
let delta = 0;
|
||
|
||
switch ( code ) {
|
||
|
||
case 0: // Common value
|
||
delta = commonValue;
|
||
break;
|
||
case 1: // 8-bit signed
|
||
delta = view.getInt8( vintsOffset );
|
||
vintsOffset += 1;
|
||
break;
|
||
case 2: // 16-bit signed
|
||
delta = view.getInt16( vintsOffset, true );
|
||
vintsOffset += 2;
|
||
break;
|
||
case 3: // 32-bit signed
|
||
delta = view.getInt32( vintsOffset, true );
|
||
vintsOffset += 4;
|
||
break;
|
||
|
||
}
|
||
|
||
prevVal += delta;
|
||
result[ i ] = prevVal;
|
||
|
||
}
|
||
|
||
}
|
||
|
||
return result;
|
||
|
||
}
|
||
|
||
// ============================================================================
|
||
// Binary Reader Helper
|
||
// ============================================================================
|
||
|
||
class BinaryReader {
|
||
|
||
constructor( buffer ) {
|
||
|
||
this.buffer = buffer;
|
||
this.view = new DataView( buffer );
|
||
this.offset = 0;
|
||
|
||
}
|
||
|
||
seek( offset ) {
|
||
|
||
this.offset = offset;
|
||
|
||
}
|
||
|
||
tell() {
|
||
|
||
return this.offset;
|
||
|
||
}
|
||
|
||
readUint8() {
|
||
|
||
const value = this.view.getUint8( this.offset );
|
||
this.offset += 1;
|
||
return value;
|
||
|
||
}
|
||
|
||
readInt8() {
|
||
|
||
const value = this.view.getInt8( this.offset );
|
||
this.offset += 1;
|
||
return value;
|
||
|
||
}
|
||
|
||
readUint16() {
|
||
|
||
const value = this.view.getUint16( this.offset, true );
|
||
this.offset += 2;
|
||
return value;
|
||
|
||
}
|
||
|
||
readInt16() {
|
||
|
||
const value = this.view.getInt16( this.offset, true );
|
||
this.offset += 2;
|
||
return value;
|
||
|
||
}
|
||
|
||
readUint32() {
|
||
|
||
const value = this.view.getUint32( this.offset, true );
|
||
this.offset += 4;
|
||
return value;
|
||
|
||
}
|
||
|
||
readInt32() {
|
||
|
||
const value = this.view.getInt32( this.offset, true );
|
||
this.offset += 4;
|
||
return value;
|
||
|
||
}
|
||
|
||
readUint64() {
|
||
|
||
const lo = this.view.getUint32( this.offset, true );
|
||
const hi = this.view.getUint32( this.offset + 4, true );
|
||
this.offset += 8;
|
||
// For values that fit in Number, this is safe
|
||
return hi * 0x100000000 + lo;
|
||
|
||
}
|
||
|
||
readInt64() {
|
||
|
||
const lo = this.view.getUint32( this.offset, true );
|
||
const hi = this.view.getInt32( this.offset + 4, true );
|
||
this.offset += 8;
|
||
return hi * 0x100000000 + lo;
|
||
|
||
}
|
||
|
||
readFloat32() {
|
||
|
||
const value = this.view.getFloat32( this.offset, true );
|
||
this.offset += 4;
|
||
return value;
|
||
|
||
}
|
||
|
||
readFloat64() {
|
||
|
||
const value = this.view.getFloat64( this.offset, true );
|
||
this.offset += 8;
|
||
return value;
|
||
|
||
}
|
||
|
||
readBytes( length ) {
|
||
|
||
const bytes = new Uint8Array( this.buffer, this.offset, length );
|
||
this.offset += length;
|
||
return bytes;
|
||
|
||
}
|
||
|
||
readString( length ) {
|
||
|
||
const bytes = this.readBytes( length );
|
||
let end = 0;
|
||
while ( end < length && bytes[ end ] !== 0 ) end ++;
|
||
return textDecoder.decode( bytes.subarray( 0, end ) );
|
||
|
||
}
|
||
|
||
}
|
||
|
||
// ============================================================================
|
||
// ValueRep - 64-bit packed value representation
|
||
// ============================================================================
|
||
|
||
class ValueRep {
|
||
|
||
constructor( lo, hi ) {
|
||
|
||
this.lo = lo; // Lower 32 bits
|
||
this.hi = hi; // Upper 32 bits
|
||
|
||
}
|
||
|
||
get isArray() {
|
||
|
||
return ( this.hi & 0x80000000 ) !== 0;
|
||
|
||
}
|
||
|
||
get isInlined() {
|
||
|
||
return ( this.hi & 0x40000000 ) !== 0;
|
||
|
||
}
|
||
|
||
get isCompressed() {
|
||
|
||
return ( this.hi & 0x20000000 ) !== 0;
|
||
|
||
}
|
||
|
||
get typeEnum() {
|
||
|
||
return ( this.hi >> 16 ) & 0xFF;
|
||
|
||
}
|
||
|
||
get payload() {
|
||
|
||
// 48-bit payload: lo (32 bits) + hi lower 16 bits
|
||
// Note: JavaScript numbers are IEEE 754 doubles with 53 bits of integer precision,
|
||
// so 48-bit values are represented exactly without loss of precision.
|
||
return this.lo + ( ( this.hi & 0xFFFF ) * 0x100000000 );
|
||
|
||
}
|
||
|
||
getInlinedValue() {
|
||
|
||
// For inlined scalars, the value is in the lower 32 bits
|
||
return this.lo;
|
||
|
||
}
|
||
|
||
}
|
||
|
||
// ============================================================================
|
||
// USDC Parser
|
||
// ============================================================================
|
||
|
||
class USDCParser {
|
||
|
||
/**
|
||
* Parse USDC file and return raw spec data without building Three.js scene.
|
||
* Used by USDComposer for unified scene composition.
|
||
*/
|
||
parseData( buffer ) {
|
||
|
||
this.buffer = buffer instanceof ArrayBuffer ? buffer : buffer.buffer;
|
||
this.reader = new BinaryReader( this.buffer );
|
||
this.version = { major: 0, minor: 0, patch: 0 };
|
||
|
||
this._conversionBuffer = new ArrayBuffer( 4 );
|
||
this._conversionView = new DataView( this._conversionBuffer );
|
||
|
||
this._readBootstrap();
|
||
this._readTOC();
|
||
this._readTokens();
|
||
this._readStrings();
|
||
this._readFields();
|
||
this._readFieldSets();
|
||
this._readPaths();
|
||
this._readSpecs();
|
||
|
||
// Build specsByPath without building scene
|
||
this.specsByPath = {};
|
||
|
||
for ( const spec of this.specs ) {
|
||
|
||
const path = this.paths[ spec.pathIndex ];
|
||
if ( ! path ) continue;
|
||
|
||
const fields = this._getFieldsForSpec( spec );
|
||
this.specsByPath[ path ] = { specType: spec.specType, fields };
|
||
|
||
}
|
||
|
||
return { specsByPath: this.specsByPath };
|
||
|
||
}
|
||
|
||
_readBootstrap() {
|
||
|
||
const reader = this.reader;
|
||
reader.seek( 0 );
|
||
|
||
// Read magic "PXR-USDC"
|
||
const magic = reader.readString( 8 );
|
||
if ( magic !== 'PXR-USDC' ) {
|
||
|
||
throw new Error( 'Not a valid USDC file' );
|
||
|
||
}
|
||
|
||
// Read version
|
||
this.version.major = reader.readUint8();
|
||
this.version.minor = reader.readUint8();
|
||
this.version.patch = reader.readUint8();
|
||
reader.readBytes( 5 ); // Skip remaining version bytes
|
||
|
||
// Read TOC offset
|
||
this.tocOffset = reader.readUint64();
|
||
|
||
// Skip reserved bytes (rest of 128-byte header)
|
||
// Already at offset 24, skip to end of bootstrap (88 bytes total for bootstrap struct)
|
||
|
||
}
|
||
|
||
_readTOC() {
|
||
|
||
const reader = this.reader;
|
||
reader.seek( this.tocOffset );
|
||
|
||
// Read number of sections
|
||
const numSections = reader.readUint64();
|
||
this.sections = {};
|
||
|
||
for ( let i = 0; i < numSections; i ++ ) {
|
||
|
||
const name = reader.readString( 16 );
|
||
const start = reader.readUint64();
|
||
const size = reader.readUint64();
|
||
|
||
this.sections[ name ] = { start, size };
|
||
|
||
}
|
||
|
||
}
|
||
|
||
_readTokens() {
|
||
|
||
const section = this.sections[ 'TOKENS' ];
|
||
if ( ! section ) return;
|
||
|
||
const reader = this.reader;
|
||
reader.seek( section.start );
|
||
|
||
const numTokens = reader.readUint64();
|
||
this.tokens = [];
|
||
|
||
if ( this.version.major === 0 && this.version.minor < 4 ) {
|
||
|
||
// Uncompressed tokens (version < 0.4.0)
|
||
const tokensNumBytes = reader.readUint64();
|
||
const tokensData = reader.readBytes( tokensNumBytes );
|
||
|
||
let strStart = 0;
|
||
for ( let i = 0; i < numTokens; i ++ ) {
|
||
|
||
let strEnd = strStart;
|
||
while ( strEnd < tokensData.length && tokensData[ strEnd ] !== 0 ) strEnd ++;
|
||
|
||
this.tokens.push( textDecoder.decode( tokensData.subarray( strStart, strEnd ) ) );
|
||
strStart = strEnd + 1;
|
||
|
||
}
|
||
|
||
} else {
|
||
|
||
// Compressed tokens (version >= 0.4.0)
|
||
const uncompressedSize = reader.readUint64();
|
||
const compressedSize = reader.readUint64();
|
||
const compressedData = reader.readBytes( compressedSize );
|
||
|
||
const tokensData = decompressLZ4( compressedData, uncompressedSize );
|
||
|
||
let strStart = 0;
|
||
for ( let i = 0; i < numTokens; i ++ ) {
|
||
|
||
let strEnd = strStart;
|
||
while ( strEnd < tokensData.length && tokensData[ strEnd ] !== 0 ) strEnd ++;
|
||
|
||
this.tokens.push( textDecoder.decode( tokensData.subarray( strStart, strEnd ) ) );
|
||
strStart = strEnd + 1;
|
||
|
||
}
|
||
|
||
}
|
||
|
||
}
|
||
|
||
_readStrings() {
|
||
|
||
const section = this.sections[ 'STRINGS' ];
|
||
if ( ! section ) {
|
||
|
||
this.strings = [];
|
||
return;
|
||
|
||
}
|
||
|
||
const reader = this.reader;
|
||
reader.seek( section.start );
|
||
|
||
// Strings section has an 8-byte count prefix, but string indices stored
|
||
// elsewhere in the file are relative to the section start (not the data).
|
||
// So we read the entire section as uint32 values to maintain correct indexing.
|
||
const numStrings = Math.floor( section.size / 4 );
|
||
this.strings = [];
|
||
|
||
for ( let i = 0; i < numStrings; i ++ ) {
|
||
|
||
this.strings.push( reader.readUint32() );
|
||
|
||
}
|
||
|
||
}
|
||
|
||
_readFields() {
|
||
|
||
const section = this.sections[ 'FIELDS' ];
|
||
if ( ! section ) return;
|
||
|
||
const reader = this.reader;
|
||
reader.seek( section.start );
|
||
|
||
this.fields = [];
|
||
|
||
if ( this.version.major === 0 && this.version.minor < 4 ) {
|
||
|
||
// Uncompressed fields
|
||
const numFields = Math.floor( section.size / 12 ); // 4 bytes token index + 8 bytes value rep
|
||
|
||
for ( let i = 0; i < numFields; i ++ ) {
|
||
|
||
const tokenIndex = reader.readUint32();
|
||
const repLo = reader.readUint32();
|
||
const repHi = reader.readUint32();
|
||
|
||
this.fields.push( {
|
||
tokenIndex,
|
||
valueRep: new ValueRep( repLo, repHi )
|
||
} );
|
||
|
||
}
|
||
|
||
} else {
|
||
|
||
// Compressed fields (version >= 0.4.0)
|
||
const numFields = reader.readUint64();
|
||
|
||
// Read compressed token indices
|
||
const tokenIndicesCompressedSize = reader.readUint64();
|
||
const tokenIndicesCompressed = reader.readBytes( tokenIndicesCompressedSize );
|
||
const tokenIndices = decompressIntegers32(
|
||
tokenIndicesCompressed.buffer.slice(
|
||
tokenIndicesCompressed.byteOffset,
|
||
tokenIndicesCompressed.byteOffset + tokenIndicesCompressedSize
|
||
),
|
||
numFields
|
||
);
|
||
|
||
// Read compressed value reps (LZ4 only, no integer encoding)
|
||
const repsCompressedSize = reader.readUint64();
|
||
const repsCompressed = reader.readBytes( repsCompressedSize );
|
||
const repsData = decompressLZ4( repsCompressed, numFields * 8 );
|
||
const repsView = new DataView( repsData.buffer, repsData.byteOffset, repsData.byteLength );
|
||
|
||
for ( let i = 0; i < numFields; i ++ ) {
|
||
|
||
const repLo = repsView.getUint32( i * 8, true );
|
||
const repHi = repsView.getUint32( i * 8 + 4, true );
|
||
|
||
this.fields.push( {
|
||
tokenIndex: tokenIndices[ i ],
|
||
valueRep: new ValueRep( repLo, repHi )
|
||
} );
|
||
|
||
}
|
||
|
||
}
|
||
|
||
}
|
||
|
||
_readFieldSets() {
|
||
|
||
const section = this.sections[ 'FIELDSETS' ];
|
||
if ( ! section ) return;
|
||
|
||
const reader = this.reader;
|
||
reader.seek( section.start );
|
||
|
||
this.fieldSets = [];
|
||
|
||
if ( this.version.major === 0 && this.version.minor < 4 ) {
|
||
|
||
// Uncompressed field sets
|
||
const numFieldSets = Math.floor( section.size / 4 );
|
||
|
||
for ( let i = 0; i < numFieldSets; i ++ ) {
|
||
|
||
this.fieldSets.push( reader.readUint32() );
|
||
|
||
}
|
||
|
||
} else {
|
||
|
||
// Compressed field sets
|
||
const numFieldSets = reader.readUint64();
|
||
const compressedSize = reader.readUint64();
|
||
const compressed = reader.readBytes( compressedSize );
|
||
|
||
const indices = decompressIntegers32(
|
||
compressed.buffer.slice(
|
||
compressed.byteOffset,
|
||
compressed.byteOffset + compressedSize
|
||
),
|
||
numFieldSets
|
||
);
|
||
|
||
for ( let i = 0; i < numFieldSets; i ++ ) {
|
||
|
||
this.fieldSets.push( indices[ i ] );
|
||
|
||
}
|
||
|
||
}
|
||
|
||
}
|
||
|
||
_readPaths() {
|
||
|
||
const section = this.sections[ 'PATHS' ];
|
||
if ( ! section ) return;
|
||
|
||
const reader = this.reader;
|
||
reader.seek( section.start );
|
||
|
||
const numPaths = reader.readUint64();
|
||
this.paths = new Array( numPaths ).fill( '' );
|
||
|
||
if ( this.version.major === 0 && this.version.minor < 4 ) {
|
||
|
||
// Uncompressed paths - recursive tree structure
|
||
this._readPathsRecursive( '' );
|
||
|
||
} else {
|
||
|
||
// Compressed paths (version >= 0.4.0)
|
||
// Note: numPaths is stored twice - once for array sizing, once in compressed paths section
|
||
reader.readUint64(); // Read duplicate numPaths value (matches numPaths above)
|
||
|
||
const compressedSize1 = reader.readUint64();
|
||
const pathIndicesCompressed = reader.readBytes( compressedSize1 );
|
||
const pathIndices = decompressIntegers32(
|
||
pathIndicesCompressed.buffer.slice(
|
||
pathIndicesCompressed.byteOffset,
|
||
pathIndicesCompressed.byteOffset + compressedSize1
|
||
),
|
||
numPaths
|
||
);
|
||
|
||
const compressedSize2 = reader.readUint64();
|
||
const elementTokenIndicesCompressed = reader.readBytes( compressedSize2 );
|
||
const elementTokenIndices = decompressIntegers32(
|
||
elementTokenIndicesCompressed.buffer.slice(
|
||
elementTokenIndicesCompressed.byteOffset,
|
||
elementTokenIndicesCompressed.byteOffset + compressedSize2
|
||
),
|
||
numPaths
|
||
);
|
||
|
||
const compressedSize3 = reader.readUint64();
|
||
const jumpsCompressed = reader.readBytes( compressedSize3 );
|
||
const jumps = decompressIntegers32(
|
||
jumpsCompressed.buffer.slice(
|
||
jumpsCompressed.byteOffset,
|
||
jumpsCompressed.byteOffset + compressedSize3
|
||
),
|
||
numPaths
|
||
);
|
||
|
||
// Build paths from compressed data
|
||
this._buildPathsFromCompressed( pathIndices, elementTokenIndices, jumps );
|
||
|
||
}
|
||
|
||
}
|
||
|
||
_readPathsRecursive( parentPath, depth = 0 ) {
|
||
|
||
const reader = this.reader;
|
||
|
||
// Prevent infinite recursion
|
||
if ( depth > 1000 ) return;
|
||
|
||
// Read path item header
|
||
const index = reader.readUint32();
|
||
const elementTokenIndex = reader.readUint32();
|
||
const bits = reader.readUint8();
|
||
|
||
const hasChild = ( bits & 1 ) !== 0;
|
||
const hasSibling = ( bits & 2 ) !== 0;
|
||
const isPrimProperty = ( bits & 4 ) !== 0;
|
||
|
||
// Build path
|
||
let path;
|
||
if ( parentPath === '' ) {
|
||
|
||
path = '/';
|
||
|
||
} else {
|
||
|
||
const elemToken = this.tokens[ elementTokenIndex ] || '';
|
||
if ( isPrimProperty ) {
|
||
|
||
path = parentPath + '.' + elemToken;
|
||
|
||
} else {
|
||
|
||
path = parentPath === '/' ? '/' + elemToken : parentPath + '/' + elemToken;
|
||
|
||
}
|
||
|
||
}
|
||
|
||
this.paths[ index ] = path;
|
||
|
||
// Process children and siblings
|
||
if ( hasChild && hasSibling ) {
|
||
|
||
// Read sibling offset
|
||
const siblingOffset = reader.readUint64();
|
||
|
||
// Read child
|
||
this._readPathsRecursive( path, depth + 1 );
|
||
|
||
// Read sibling
|
||
reader.seek( siblingOffset );
|
||
this._readPathsRecursive( parentPath, depth + 1 );
|
||
|
||
} else if ( hasChild ) {
|
||
|
||
this._readPathsRecursive( path, depth + 1 );
|
||
|
||
} else if ( hasSibling ) {
|
||
|
||
this._readPathsRecursive( parentPath, depth + 1 );
|
||
|
||
}
|
||
|
||
}
|
||
|
||
_buildPathsFromCompressed( pathIndices, elementTokenIndices, jumps ) {
|
||
|
||
// Jump encoding from USD:
|
||
// 0 = only sibling (no child), next entry is sibling
|
||
// -1 = only child (no sibling), next entry is child
|
||
// -2 = leaf (no child, no sibling)
|
||
// >0 = has both child and sibling, value is offset to sibling
|
||
|
||
const buildPaths = ( startIndex, parentPath ) => {
|
||
|
||
let curIndex = startIndex;
|
||
|
||
while ( curIndex < pathIndices.length ) {
|
||
|
||
const thisIndex = curIndex ++;
|
||
const pathIndex = pathIndices[ thisIndex ];
|
||
const elementTokenIndex = elementTokenIndices[ thisIndex ];
|
||
const jump = jumps[ thisIndex ];
|
||
|
||
// Build path
|
||
let path;
|
||
if ( parentPath === '' ) {
|
||
|
||
path = '/';
|
||
parentPath = path;
|
||
|
||
} else {
|
||
|
||
const elemToken = this.tokens[ Math.abs( elementTokenIndex ) ] || '';
|
||
const isPrimProperty = elementTokenIndex < 0;
|
||
|
||
if ( isPrimProperty ) {
|
||
|
||
path = parentPath + '.' + elemToken;
|
||
|
||
} else {
|
||
|
||
path = parentPath === '/' ? '/' + elemToken : parentPath + '/' + elemToken;
|
||
|
||
}
|
||
|
||
}
|
||
|
||
this.paths[ pathIndex ] = path;
|
||
|
||
// Determine children and siblings
|
||
const hasChild = jump > 0 || jump === - 1;
|
||
const hasSibling = jump >= 0;
|
||
|
||
if ( hasChild ) {
|
||
|
||
if ( hasSibling ) {
|
||
|
||
// Has both child and sibling
|
||
// Recursively process sibling subtree
|
||
const siblingIndex = thisIndex + jump;
|
||
buildPaths( siblingIndex, parentPath );
|
||
|
||
}
|
||
|
||
// Child is next entry, continue with new parent path
|
||
parentPath = path;
|
||
|
||
} else if ( hasSibling ) {
|
||
|
||
// Only sibling, next entry is sibling with same parent
|
||
// Just continue loop with curIndex and same parentPath
|
||
|
||
} else {
|
||
|
||
// Leaf node, exit loop
|
||
break;
|
||
|
||
}
|
||
|
||
}
|
||
|
||
};
|
||
|
||
buildPaths( 0, '' );
|
||
|
||
}
|
||
|
||
_readSpecs() {
|
||
|
||
const section = this.sections[ 'SPECS' ];
|
||
if ( ! section ) return;
|
||
|
||
const reader = this.reader;
|
||
reader.seek( section.start );
|
||
|
||
this.specs = [];
|
||
|
||
if ( this.version.major === 0 && this.version.minor < 4 ) {
|
||
|
||
// Uncompressed specs
|
||
// Each spec: pathIndex (4), fieldSetIndex (4), specType (4) = 12 bytes
|
||
// For version 0.0.1 there may be different padding
|
||
const specSize = ( this.version.minor === 0 && this.version.patch === 1 ) ? 16 : 12;
|
||
const numSpecs = Math.floor( section.size / specSize );
|
||
|
||
for ( let i = 0; i < numSpecs; i ++ ) {
|
||
|
||
const pathIndex = reader.readUint32();
|
||
const fieldSetIndex = reader.readUint32();
|
||
const specType = reader.readUint32();
|
||
|
||
if ( specSize === 16 ) reader.readUint32(); // padding
|
||
|
||
this.specs.push( { pathIndex, fieldSetIndex, specType } );
|
||
|
||
}
|
||
|
||
} else {
|
||
|
||
// Compressed specs
|
||
const numSpecs = reader.readUint64();
|
||
|
||
const compressedSize1 = reader.readUint64();
|
||
const pathIndicesCompressed = reader.readBytes( compressedSize1 );
|
||
const pathIndices = decompressIntegers32(
|
||
pathIndicesCompressed.buffer.slice(
|
||
pathIndicesCompressed.byteOffset,
|
||
pathIndicesCompressed.byteOffset + compressedSize1
|
||
),
|
||
numSpecs
|
||
);
|
||
|
||
const compressedSize2 = reader.readUint64();
|
||
const fieldSetIndicesCompressed = reader.readBytes( compressedSize2 );
|
||
const fieldSetIndices = decompressIntegers32(
|
||
fieldSetIndicesCompressed.buffer.slice(
|
||
fieldSetIndicesCompressed.byteOffset,
|
||
fieldSetIndicesCompressed.byteOffset + compressedSize2
|
||
),
|
||
numSpecs
|
||
);
|
||
|
||
const compressedSize3 = reader.readUint64();
|
||
const specTypesCompressed = reader.readBytes( compressedSize3 );
|
||
const specTypes = decompressIntegers32(
|
||
specTypesCompressed.buffer.slice(
|
||
specTypesCompressed.byteOffset,
|
||
specTypesCompressed.byteOffset + compressedSize3
|
||
),
|
||
numSpecs
|
||
);
|
||
|
||
for ( let i = 0; i < numSpecs; i ++ ) {
|
||
|
||
this.specs.push( {
|
||
pathIndex: pathIndices[ i ],
|
||
fieldSetIndex: fieldSetIndices[ i ],
|
||
specType: specTypes[ i ]
|
||
} );
|
||
|
||
}
|
||
|
||
}
|
||
|
||
}
|
||
|
||
// ========================================================================
|
||
// Value Reading
|
||
// ========================================================================
|
||
|
||
_readValue( valueRep ) {
|
||
|
||
const type = valueRep.typeEnum;
|
||
const isArray = valueRep.isArray;
|
||
const isInlined = valueRep.isInlined;
|
||
|
||
// Handle TimeSamples specially - they have their own format
|
||
if ( type === TypeEnum.TimeSamples ) {
|
||
|
||
return this._readTimeSamples( valueRep );
|
||
|
||
}
|
||
|
||
if ( isInlined ) {
|
||
|
||
return this._readInlinedValue( valueRep );
|
||
|
||
}
|
||
|
||
// Seek to payload offset and read value
|
||
const offset = valueRep.payload;
|
||
const savedOffset = this.reader.tell();
|
||
this.reader.seek( offset );
|
||
|
||
let value;
|
||
|
||
if ( isArray ) {
|
||
|
||
value = this._readArrayValue( valueRep );
|
||
|
||
} else {
|
||
|
||
value = this._readScalarValue( type );
|
||
|
||
}
|
||
|
||
this.reader.seek( savedOffset );
|
||
return value;
|
||
|
||
}
|
||
|
||
_readInlinedValue( valueRep ) {
|
||
|
||
const type = valueRep.typeEnum;
|
||
const payload = valueRep.getInlinedValue();
|
||
const view = this._conversionView;
|
||
|
||
switch ( type ) {
|
||
|
||
case TypeEnum.Bool:
|
||
return payload !== 0;
|
||
case TypeEnum.UChar:
|
||
return payload & 0xFF;
|
||
case TypeEnum.Int:
|
||
case TypeEnum.UInt:
|
||
return payload;
|
||
case TypeEnum.Float: {
|
||
|
||
view.setUint32( 0, payload, true );
|
||
return view.getFloat32( 0, true );
|
||
|
||
}
|
||
|
||
case TypeEnum.Double: {
|
||
|
||
// When a double is inlined, it's stored as float32 bits in the payload
|
||
view.setUint32( 0, payload, true );
|
||
return view.getFloat32( 0, true );
|
||
|
||
}
|
||
|
||
case TypeEnum.Token:
|
||
return this.tokens[ payload ] || '';
|
||
case TypeEnum.String:
|
||
return this.tokens[ this.strings[ payload ] ] || '';
|
||
case TypeEnum.AssetPath:
|
||
return this.tokens[ payload ] || '';
|
||
case TypeEnum.Specifier:
|
||
return payload; // 0=def, 1=over, 2=class
|
||
case TypeEnum.Permission:
|
||
case TypeEnum.Variability:
|
||
return payload;
|
||
|
||
// Vec2h: Two half-floats fit in 4 bytes, stored directly
|
||
case TypeEnum.Vec2h: {
|
||
|
||
view.setUint32( 0, payload, true );
|
||
return [ this._halfToFloat( view.getUint16( 0, true ) ), this._halfToFloat( view.getUint16( 2, true ) ) ];
|
||
|
||
}
|
||
|
||
// Inlined vectors that don't fit in 4 bytes are encoded as signed 8-bit integers
|
||
// Vec2f = 8 bytes (2x float32), Vec3f = 12 bytes, Vec4f = 16 bytes, etc.
|
||
case TypeEnum.Vec2f:
|
||
case TypeEnum.Vec2i: {
|
||
|
||
view.setUint32( 0, payload, true );
|
||
return [ view.getInt8( 0 ), view.getInt8( 1 ) ];
|
||
|
||
}
|
||
|
||
case TypeEnum.Vec3f:
|
||
case TypeEnum.Vec3i: {
|
||
|
||
view.setUint32( 0, payload, true );
|
||
return [ view.getInt8( 0 ), view.getInt8( 1 ), view.getInt8( 2 ) ];
|
||
|
||
}
|
||
|
||
case TypeEnum.Vec4f:
|
||
case TypeEnum.Vec4i: {
|
||
|
||
view.setUint32( 0, payload, true );
|
||
return [ view.getInt8( 0 ), view.getInt8( 1 ), view.getInt8( 2 ), view.getInt8( 3 ) ];
|
||
|
||
}
|
||
|
||
case TypeEnum.Matrix2d: {
|
||
|
||
// Inlined Matrix2d stores diagonal values as 2 signed int8 values
|
||
view.setUint32( 0, payload, true );
|
||
const d0 = view.getInt8( 0 ), d1 = view.getInt8( 1 );
|
||
return [ d0, 0, 0, d1 ];
|
||
|
||
}
|
||
|
||
case TypeEnum.Matrix3d: {
|
||
|
||
// Inlined Matrix3d stores diagonal values as 3 signed int8 values
|
||
view.setUint32( 0, payload, true );
|
||
const d0 = view.getInt8( 0 ), d1 = view.getInt8( 1 ), d2 = view.getInt8( 2 );
|
||
return [ d0, 0, 0, 0, d1, 0, 0, 0, d2 ];
|
||
|
||
}
|
||
|
||
case TypeEnum.Matrix4d: {
|
||
|
||
// Inlined Matrix4d stores diagonal values as 4 signed int8 values
|
||
view.setUint32( 0, payload, true );
|
||
const d0 = view.getInt8( 0 ), d1 = view.getInt8( 1 ), d2 = view.getInt8( 2 ), d3 = view.getInt8( 3 );
|
||
return [ d0, 0, 0, 0, 0, d1, 0, 0, 0, 0, d2, 0, 0, 0, 0, d3 ];
|
||
|
||
}
|
||
|
||
default:
|
||
return payload;
|
||
|
||
}
|
||
|
||
}
|
||
|
||
_readTimeSamples( valueRep ) {
|
||
|
||
const reader = this.reader;
|
||
const offset = valueRep.payload;
|
||
const savedOffset = reader.tell();
|
||
reader.seek( offset );
|
||
|
||
// TimeSamples format uses RELATIVE offsets (from OpenUSD _RecursiveRead):
|
||
// _RecursiveRead: read int64 relativeOffset at current position, then seek to start + relativeOffset
|
||
// After reading timesRep, continue reading from current position (after timesRep)
|
||
// Layout at TimeSamples location:
|
||
// - int64 timesOffset (relative from start of this int64)
|
||
// At (start + timesOffset): timesRep ValueRep, then int64 valuesOffset, then numValues + ValueReps
|
||
|
||
// Read times relative offset and resolve
|
||
const timesStart = reader.tell();
|
||
const timesRelOffset = reader.readInt64();
|
||
reader.seek( timesStart + timesRelOffset );
|
||
|
||
const timesRepLo = reader.readUint32();
|
||
const timesRepHi = reader.readUint32();
|
||
const timesRep = new ValueRep( timesRepLo, timesRepHi );
|
||
|
||
// Resolve times array
|
||
const times = this._readValue( timesRep );
|
||
|
||
// Continue reading from current position (after timesRep)
|
||
// The second _RecursiveRead reads from CURRENT position, not from the beginning
|
||
const afterTimesRep = timesStart + timesRelOffset + 8;
|
||
reader.seek( afterTimesRep );
|
||
|
||
// Read values relative offset
|
||
const valuesStart = reader.tell();
|
||
const valuesRelOffset = reader.readInt64();
|
||
reader.seek( valuesStart + valuesRelOffset );
|
||
|
||
// Read number of values
|
||
const numValues = reader.readUint64();
|
||
|
||
// Read all ValueReps
|
||
const valueReps = [];
|
||
for ( let i = 0; i < numValues; i ++ ) {
|
||
|
||
const repLo = reader.readUint32();
|
||
const repHi = reader.readUint32();
|
||
valueReps.push( new ValueRep( repLo, repHi ) );
|
||
|
||
}
|
||
|
||
// Resolve each value
|
||
const values = [];
|
||
for ( let i = 0; i < numValues; i ++ ) {
|
||
|
||
values.push( this._readValue( valueReps[ i ] ) );
|
||
|
||
}
|
||
|
||
reader.seek( savedOffset );
|
||
|
||
// Convert times to array if needed
|
||
const timesArray = times instanceof Float64Array ? Array.from( times ) : ( Array.isArray( times ) ? times : [ times ] );
|
||
|
||
return { times: timesArray, values };
|
||
|
||
}
|
||
|
||
_readScalarValue( type ) {
|
||
|
||
const reader = this.reader;
|
||
|
||
switch ( type ) {
|
||
|
||
case TypeEnum.Invalid:
|
||
return null;
|
||
case TypeEnum.Bool:
|
||
return reader.readUint8() !== 0;
|
||
case TypeEnum.UChar:
|
||
return reader.readUint8();
|
||
case TypeEnum.Int:
|
||
return reader.readInt32();
|
||
case TypeEnum.UInt:
|
||
return reader.readUint32();
|
||
case TypeEnum.Int64:
|
||
return reader.readInt64();
|
||
case TypeEnum.UInt64:
|
||
return reader.readUint64();
|
||
case TypeEnum.Half:
|
||
return this._readHalf();
|
||
case TypeEnum.Float:
|
||
return reader.readFloat32();
|
||
case TypeEnum.Double:
|
||
return reader.readFloat64();
|
||
case TypeEnum.String:
|
||
case TypeEnum.Token: {
|
||
|
||
const index = reader.readUint32();
|
||
return this.tokens[ index ] || '';
|
||
|
||
}
|
||
|
||
case TypeEnum.AssetPath: {
|
||
|
||
const index = reader.readUint32();
|
||
return this.tokens[ index ] || '';
|
||
|
||
}
|
||
|
||
case TypeEnum.Vec2f:
|
||
return [ reader.readFloat32(), reader.readFloat32() ];
|
||
case TypeEnum.Vec2d:
|
||
return [ reader.readFloat64(), reader.readFloat64() ];
|
||
case TypeEnum.Vec2i:
|
||
return [ reader.readInt32(), reader.readInt32() ];
|
||
case TypeEnum.Vec3f:
|
||
return [ reader.readFloat32(), reader.readFloat32(), reader.readFloat32() ];
|
||
case TypeEnum.Vec3d:
|
||
return [ reader.readFloat64(), reader.readFloat64(), reader.readFloat64() ];
|
||
case TypeEnum.Vec3i:
|
||
return [ reader.readInt32(), reader.readInt32(), reader.readInt32() ];
|
||
case TypeEnum.Vec4f:
|
||
return [ reader.readFloat32(), reader.readFloat32(), reader.readFloat32(), reader.readFloat32() ];
|
||
case TypeEnum.Vec4d:
|
||
return [ reader.readFloat64(), reader.readFloat64(), reader.readFloat64(), reader.readFloat64() ];
|
||
case TypeEnum.Quatf:
|
||
return [ reader.readFloat32(), reader.readFloat32(), reader.readFloat32(), reader.readFloat32() ];
|
||
case TypeEnum.Quatd:
|
||
return [ reader.readFloat64(), reader.readFloat64(), reader.readFloat64(), reader.readFloat64() ];
|
||
case TypeEnum.Matrix4d: {
|
||
|
||
const m = [];
|
||
for ( let i = 0; i < 16; i ++ ) m.push( reader.readFloat64() );
|
||
return m;
|
||
|
||
}
|
||
|
||
case TypeEnum.TokenVector: {
|
||
|
||
const count = reader.readUint64();
|
||
const tokens = [];
|
||
for ( let i = 0; i < count; i ++ ) {
|
||
|
||
const index = reader.readUint32();
|
||
tokens.push( this.tokens[ index ] || '' );
|
||
|
||
}
|
||
|
||
return tokens;
|
||
|
||
}
|
||
|
||
case TypeEnum.PathVector: {
|
||
|
||
const count = reader.readUint64();
|
||
const paths = [];
|
||
for ( let i = 0; i < count; i ++ ) {
|
||
|
||
const index = reader.readUint32();
|
||
paths.push( this.paths[ index ] || '' );
|
||
|
||
}
|
||
|
||
return paths;
|
||
|
||
}
|
||
|
||
case TypeEnum.DoubleVector: {
|
||
|
||
// DoubleVector is a count-prefixed array of doubles
|
||
const count = reader.readUint64();
|
||
const arr = new Float64Array( count );
|
||
for ( let i = 0; i < count; i ++ ) arr[ i ] = reader.readFloat64();
|
||
return arr;
|
||
|
||
}
|
||
|
||
case TypeEnum.Dictionary: {
|
||
|
||
// Dictionary format:
|
||
// u64 elementCount
|
||
// For each element: u32 keyIndex + i64 valueOffset (relative)
|
||
const elementCount = reader.readUint64();
|
||
const dict = {};
|
||
|
||
for ( let i = 0; i < elementCount; i ++ ) {
|
||
|
||
const keyIdx = reader.readUint32();
|
||
const key = this.tokens[ keyIdx ];
|
||
|
||
// Value offset is relative to current position
|
||
const currentPos = reader.position;
|
||
const valueOffset = reader.readInt64();
|
||
const valuePos = currentPos + valueOffset;
|
||
|
||
// Save position, read value, restore position
|
||
const savedPos = reader.position;
|
||
reader.position = valuePos;
|
||
|
||
// Read the value representation at the offset
|
||
const valueRepData = reader.readUint64();
|
||
const valueRep = new ValueRep( valueRepData );
|
||
|
||
// Read the value based on the representation
|
||
let value = null;
|
||
if ( valueRep.isInlined ) {
|
||
|
||
value = this._readInlinedValue( valueRep );
|
||
|
||
} else if ( valueRep.isArray ) {
|
||
|
||
reader.position = valueRep.payload;
|
||
value = this._readArrayValue( valueRep );
|
||
|
||
} else {
|
||
|
||
reader.position = valueRep.payload;
|
||
value = this._readScalarValue( valueRep.typeEnum );
|
||
|
||
}
|
||
|
||
reader.position = savedPos;
|
||
|
||
if ( key !== undefined && value !== null ) {
|
||
|
||
dict[ key ] = value;
|
||
|
||
}
|
||
|
||
}
|
||
|
||
return dict;
|
||
|
||
}
|
||
|
||
case TypeEnum.TokenListOp:
|
||
case TypeEnum.StringListOp:
|
||
case TypeEnum.IntListOp:
|
||
case TypeEnum.Int64ListOp:
|
||
case TypeEnum.UIntListOp:
|
||
case TypeEnum.UInt64ListOp:
|
||
// These complex types are not needed for geometry loading
|
||
// Skip them silently
|
||
return null;
|
||
|
||
case TypeEnum.PathListOp: {
|
||
|
||
// PathListOp format (from AOUSD Core Spec 16.3.10.25):
|
||
// Header byte bitmask:
|
||
// - bit 0 (0x01): Make Explicit (clears list)
|
||
// - bit 1 (0x02): Add Explicit Items
|
||
// - bit 2 (0x04): Add Items
|
||
// - bit 3 (0x08): Delete Items
|
||
// - bit 4 (0x10): Reorder Items
|
||
// - bit 5 (0x20): Prepend Items
|
||
// - bit 6 (0x40): Append Items
|
||
// Arrays follow in order: Explicit, Add, Prepend, Append, Delete, Reorder
|
||
// Each array: uint64 count + count * uint32 path indices
|
||
const flags = reader.readUint8();
|
||
const hasExplicitItems = ( flags & 0x02 ) !== 0;
|
||
const hasAddItems = ( flags & 0x04 ) !== 0;
|
||
const hasDeleteItems = ( flags & 0x08 ) !== 0;
|
||
const hasReorderItems = ( flags & 0x10 ) !== 0;
|
||
const hasPrependItems = ( flags & 0x20 ) !== 0;
|
||
const hasAppendItems = ( flags & 0x40 ) !== 0;
|
||
|
||
const readPathList = () => {
|
||
|
||
const itemCount = reader.readUint64();
|
||
const paths = [];
|
||
for ( let i = 0; i < itemCount; i ++ ) {
|
||
|
||
const pathIdx = reader.readUint32();
|
||
paths.push( this.paths[ pathIdx ] );
|
||
|
||
}
|
||
|
||
return paths;
|
||
|
||
};
|
||
|
||
// Read arrays in spec order: Explicit, Add, Prepend, Append, Delete, Reorder
|
||
let explicitPaths = null;
|
||
let addPaths = null;
|
||
let prependPaths = null;
|
||
let appendPaths = null;
|
||
|
||
if ( hasExplicitItems ) explicitPaths = readPathList();
|
||
if ( hasAddItems ) addPaths = readPathList();
|
||
if ( hasPrependItems ) prependPaths = readPathList();
|
||
if ( hasAppendItems ) appendPaths = readPathList();
|
||
if ( hasDeleteItems ) readPathList(); // Skip delete items
|
||
if ( hasReorderItems ) readPathList(); // Skip reorder items
|
||
|
||
// Return the first non-empty list (connections are typically prepended)
|
||
if ( prependPaths && prependPaths.length > 0 ) return prependPaths;
|
||
if ( explicitPaths && explicitPaths.length > 0 ) return explicitPaths;
|
||
if ( appendPaths && appendPaths.length > 0 ) return appendPaths;
|
||
if ( addPaths && addPaths.length > 0 ) return addPaths;
|
||
|
||
return null;
|
||
|
||
}
|
||
|
||
case TypeEnum.VariantSelectionMap: {
|
||
|
||
const elementCount = reader.readUint64();
|
||
const map = {};
|
||
|
||
for ( let i = 0; i < elementCount; i ++ ) {
|
||
|
||
const keyIdx = reader.readUint32();
|
||
const valueIdx = reader.readUint32();
|
||
const key = this.tokens[ this.strings[ keyIdx ] ];
|
||
const value = this.tokens[ this.strings[ valueIdx ] ];
|
||
if ( key && value ) map[ key ] = value;
|
||
|
||
}
|
||
|
||
return map;
|
||
|
||
}
|
||
|
||
default:
|
||
console.warn( 'USDCParser: Unsupported scalar type', type );
|
||
return null;
|
||
|
||
}
|
||
|
||
}
|
||
|
||
_readArrayValue( valueRep ) {
|
||
|
||
const reader = this.reader;
|
||
const type = valueRep.typeEnum;
|
||
const isCompressed = valueRep.isCompressed;
|
||
|
||
// Read array size
|
||
let size;
|
||
if ( this.version.major === 0 && this.version.minor < 7 ) {
|
||
|
||
size = reader.readUint32();
|
||
|
||
} else {
|
||
|
||
size = reader.readUint64();
|
||
|
||
}
|
||
|
||
if ( size === 0 ) return [];
|
||
|
||
// Handle compressed arrays
|
||
if ( isCompressed ) {
|
||
|
||
return this._readCompressedArray( type, size );
|
||
|
||
}
|
||
|
||
// Read uncompressed array
|
||
switch ( type ) {
|
||
|
||
case TypeEnum.Int: {
|
||
|
||
const arr = new Int32Array( size );
|
||
for ( let i = 0; i < size; i ++ ) arr[ i ] = reader.readInt32();
|
||
return arr;
|
||
|
||
}
|
||
|
||
case TypeEnum.UInt: {
|
||
|
||
const arr = new Uint32Array( size );
|
||
for ( let i = 0; i < size; i ++ ) arr[ i ] = reader.readUint32();
|
||
return arr;
|
||
|
||
}
|
||
|
||
case TypeEnum.Float: {
|
||
|
||
const arr = new Float32Array( size );
|
||
for ( let i = 0; i < size; i ++ ) arr[ i ] = reader.readFloat32();
|
||
return arr;
|
||
|
||
}
|
||
|
||
case TypeEnum.Double: {
|
||
|
||
const arr = new Float64Array( size );
|
||
for ( let i = 0; i < size; i ++ ) arr[ i ] = reader.readFloat64();
|
||
return arr;
|
||
|
||
}
|
||
|
||
case TypeEnum.Vec2f: {
|
||
|
||
const arr = new Float32Array( size * 2 );
|
||
for ( let i = 0; i < size * 2; i ++ ) arr[ i ] = reader.readFloat32();
|
||
return arr;
|
||
|
||
}
|
||
|
||
case TypeEnum.Vec3f: {
|
||
|
||
const arr = new Float32Array( size * 3 );
|
||
for ( let i = 0; i < size * 3; i ++ ) arr[ i ] = reader.readFloat32();
|
||
return arr;
|
||
|
||
}
|
||
|
||
case TypeEnum.Vec4f: {
|
||
|
||
const arr = new Float32Array( size * 4 );
|
||
for ( let i = 0; i < size * 4; i ++ ) arr[ i ] = reader.readFloat32();
|
||
return arr;
|
||
|
||
}
|
||
|
||
case TypeEnum.Vec3h: {
|
||
|
||
// Half-precision vec3 array (used for scales in skeletal animation)
|
||
const arr = new Float32Array( size * 3 );
|
||
for ( let i = 0; i < size * 3; i ++ ) arr[ i ] = this._readHalf();
|
||
return arr;
|
||
|
||
}
|
||
|
||
case TypeEnum.Quatf: {
|
||
|
||
const arr = new Float32Array( size * 4 );
|
||
for ( let i = 0; i < size * 4; i ++ ) arr[ i ] = reader.readFloat32();
|
||
return arr;
|
||
|
||
}
|
||
|
||
case TypeEnum.Quath: {
|
||
|
||
// Half-precision quaternion array
|
||
const arr = new Float32Array( size * 4 );
|
||
for ( let i = 0; i < size * 4; i ++ ) arr[ i ] = this._readHalf();
|
||
return arr;
|
||
|
||
}
|
||
|
||
case TypeEnum.Matrix4d: {
|
||
|
||
// 4x4 matrix array (16 doubles per matrix, row-major)
|
||
const arr = new Float64Array( size * 16 );
|
||
for ( let i = 0; i < size * 16; i ++ ) arr[ i ] = reader.readFloat64();
|
||
return arr;
|
||
|
||
}
|
||
|
||
case TypeEnum.Token: {
|
||
|
||
const arr = [];
|
||
for ( let i = 0; i < size; i ++ ) {
|
||
|
||
const index = reader.readUint32();
|
||
arr.push( this.tokens[ index ] || '' );
|
||
|
||
}
|
||
|
||
return arr;
|
||
|
||
}
|
||
|
||
case TypeEnum.Half: {
|
||
|
||
const arr = new Float32Array( size );
|
||
for ( let i = 0; i < size; i ++ ) arr[ i ] = this._readHalf();
|
||
return arr;
|
||
|
||
}
|
||
|
||
default:
|
||
console.warn( 'USDCParser: Unsupported array type', type );
|
||
return [];
|
||
|
||
}
|
||
|
||
}
|
||
|
||
_readCompressedArray( type, size ) {
|
||
|
||
const reader = this.reader;
|
||
|
||
switch ( type ) {
|
||
|
||
case TypeEnum.Int:
|
||
case TypeEnum.UInt: {
|
||
|
||
const compressedSize = reader.readUint64();
|
||
const compressed = reader.readBytes( compressedSize );
|
||
return decompressIntegers32(
|
||
compressed.buffer.slice(
|
||
compressed.byteOffset,
|
||
compressed.byteOffset + compressedSize
|
||
),
|
||
size
|
||
);
|
||
|
||
}
|
||
|
||
case TypeEnum.Float: {
|
||
|
||
// Float compression: 'i' = compressed as ints, 't' = lookup table
|
||
const code = reader.readInt8();
|
||
|
||
if ( code === FLOAT_COMPRESSION_INT ) {
|
||
|
||
const compressedSize = reader.readUint64();
|
||
const compressed = reader.readBytes( compressedSize );
|
||
const ints = decompressIntegers32(
|
||
compressed.buffer.slice(
|
||
compressed.byteOffset,
|
||
compressed.byteOffset + compressedSize
|
||
),
|
||
size
|
||
);
|
||
const floats = new Float32Array( size );
|
||
for ( let i = 0; i < size; i ++ ) floats[ i ] = ints[ i ];
|
||
return floats;
|
||
|
||
} else if ( code === FLOAT_COMPRESSION_LUT ) {
|
||
|
||
const lutSize = reader.readUint32();
|
||
const lut = new Float32Array( lutSize );
|
||
for ( let i = 0; i < lutSize; i ++ ) lut[ i ] = reader.readFloat32();
|
||
|
||
const compressedSize = reader.readUint64();
|
||
const compressed = reader.readBytes( compressedSize );
|
||
const indices = decompressIntegers32(
|
||
compressed.buffer.slice(
|
||
compressed.byteOffset,
|
||
compressed.byteOffset + compressedSize
|
||
),
|
||
size
|
||
);
|
||
|
||
const floats = new Float32Array( size );
|
||
for ( let i = 0; i < size; i ++ ) floats[ i ] = lut[ indices[ i ] ];
|
||
return floats;
|
||
|
||
}
|
||
|
||
console.warn( 'USDCParser: Unknown float compression code', code );
|
||
return new Float32Array( size );
|
||
|
||
}
|
||
|
||
default:
|
||
console.warn( 'USDCParser: Unsupported compressed array type', type );
|
||
return [];
|
||
|
||
}
|
||
|
||
}
|
||
|
||
_readHalf() {
|
||
|
||
return this._halfToFloat( this.reader.readUint16() );
|
||
|
||
}
|
||
|
||
_halfToFloat( h ) {
|
||
|
||
const sign = ( h & 0x8000 ) >> 15;
|
||
const exp = ( h & 0x7C00 ) >> 10;
|
||
const frac = h & 0x03FF;
|
||
|
||
if ( exp === 0 ) {
|
||
|
||
// Zero or denormalized number
|
||
if ( frac === 0 ) {
|
||
|
||
return sign ? - 0 : 0;
|
||
|
||
}
|
||
|
||
// Denormalized: value = ±2^-14 × (frac/1024)
|
||
return ( sign ? - 1 : 1 ) * HALF_DENORM_SCALE * ( frac / 1024 );
|
||
|
||
} else if ( exp === 31 ) {
|
||
|
||
return frac ? NaN : ( sign ? - Infinity : Infinity );
|
||
|
||
}
|
||
|
||
return ( sign ? - 1 : 1 ) * HALF_EXPONENT_TABLE[ exp ] * ( 1 + frac / 1024 );
|
||
|
||
}
|
||
|
||
_getFieldsForSpec( spec ) {
|
||
|
||
const fields = {};
|
||
let fieldSetIndex = spec.fieldSetIndex;
|
||
|
||
// Field sets are terminated by FIELD_SET_TERMINATOR
|
||
// Limit iterations to prevent infinite loops from malformed data
|
||
const maxIterations = 10000;
|
||
let iterations = 0;
|
||
|
||
while ( fieldSetIndex < this.fieldSets.length && iterations < maxIterations ) {
|
||
|
||
const fieldIndex = this.fieldSets[ fieldSetIndex ];
|
||
|
||
// Terminator
|
||
if ( fieldIndex === FIELD_SET_TERMINATOR || fieldIndex === - 1 ) break;
|
||
|
||
const field = this.fields[ fieldIndex ];
|
||
if ( field ) {
|
||
|
||
const name = this.tokens[ field.tokenIndex ];
|
||
const value = this._readValue( field.valueRep );
|
||
fields[ name ] = value;
|
||
|
||
}
|
||
|
||
fieldSetIndex ++;
|
||
iterations ++;
|
||
|
||
}
|
||
|
||
return fields;
|
||
|
||
}
|
||
|
||
}
|
||
|
||
export { USDCParser };
|