usd-gltf 0.0.1 → 0.2.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +29 -6
- package/package.json +10 -4
- package/src/convert/gltf-to-usd.js +166 -46
- package/src/convert/usd-material.js +65 -36
- package/src/convert/usd-to-gltf.js +8 -4
- package/src/gltf/document.js +107 -18
- package/src/gltf/glb.js +2 -2
- package/src/image/index.js +129 -1
- package/src/image/inflate.js +14 -12
- package/src/image/png.js +1 -1
- package/src/index.d.ts +92 -0
- package/src/index.js +33 -14
- package/src/limits.js +101 -0
- package/src/usd/stage.js +13 -9
- package/src/usd/usda/reader.js +17 -1
- package/src/usd/usda/writer.js +28 -7
- package/src/usd/usdc/reader.js +63 -13
- package/src/usd/usdc/writer.js +53 -3
- package/src/usd/usdz.js +9 -3
package/src/usd/usdc/reader.js
CHANGED
|
@@ -4,6 +4,7 @@
|
|
|
4
4
|
import { AssetPath, DICT_TYPES, Layer, ListOp, PrimSpec, PropertySpec, Reference } from '../model.js';
|
|
5
5
|
import { decompressFast } from './lz4.js';
|
|
6
6
|
import { decompressInts32, decompressInts64 } from './intcompression.js';
|
|
7
|
+
import { Budget, checkSize } from '../../limits.js';
|
|
7
8
|
import { PERMISSIONS, POD, SCALAR_SIZE, SPEC, SPECIFIERS, TYPE, TYPE_NAMES, halfArrayToFloat32, halfToFloat, isMatrixType, isVecType } from './types.js';
|
|
8
9
|
|
|
9
10
|
const IS_ARRAY = 0x80000000;
|
|
@@ -42,9 +43,10 @@ const V0_8_0 = version( 0, 8, 0 );
|
|
|
42
43
|
|
|
43
44
|
class CrateReader {
|
|
44
45
|
|
|
45
|
-
constructor( bytes ) {
|
|
46
|
+
constructor( bytes, budget = new Budget() ) {
|
|
46
47
|
|
|
47
48
|
this.bytes = bytes;
|
|
49
|
+
this.budget = budget;
|
|
48
50
|
this.view = new DataView( bytes.buffer, bytes.byteOffset, bytes.byteLength );
|
|
49
51
|
this.pos = 0;
|
|
50
52
|
this.timesCache = new Map();
|
|
@@ -136,7 +138,7 @@ class CrateReader {
|
|
|
136
138
|
|
|
137
139
|
take( n ) {
|
|
138
140
|
|
|
139
|
-
if ( this.pos + n > this.bytes.length ) throw new Error( 'usd-gltf: truncated usdc file' );
|
|
141
|
+
if ( ! Number.isSafeInteger( n ) || n < 0 || this.pos + n > this.bytes.length ) throw new Error( 'usd-gltf: truncated usdc file' );
|
|
140
142
|
const b = this.bytes.subarray( this.pos, this.pos + n );
|
|
141
143
|
this.pos += n;
|
|
142
144
|
return b;
|
|
@@ -145,8 +147,37 @@ class CrateReader {
|
|
|
145
147
|
|
|
146
148
|
compressedInts( n ) {
|
|
147
149
|
|
|
148
|
-
const
|
|
149
|
-
|
|
150
|
+
const data = this.take( this.u64() );
|
|
151
|
+
this.decodedInts( n, 4, data );
|
|
152
|
+
return decompressInts32( data, n );
|
|
153
|
+
|
|
154
|
+
}
|
|
155
|
+
|
|
156
|
+
// Sizes come from the file, so they are checked before allocating. Compressed integers take at
|
|
157
|
+
// least 2 bits each before LZ4, which shrinks data at most 255 times.
|
|
158
|
+
decodedInts( n, size, data ) {
|
|
159
|
+
|
|
160
|
+
checkSize( n * size, 'usdc' );
|
|
161
|
+
if ( n > data.length * 1024 + 64 ) throw new Error( 'usd-gltf: corrupt compressed integers in usdc file' );
|
|
162
|
+
this.budget.decode( n * size * 2, 'usdc' ); // the encoded buffer and the decoded integers
|
|
163
|
+
|
|
164
|
+
}
|
|
165
|
+
|
|
166
|
+
lz4( data, size ) {
|
|
167
|
+
|
|
168
|
+
checkSize( size, 'usdc' );
|
|
169
|
+
if ( size > data.length * 256 + 64 ) throw new Error( 'usd-gltf: corrupt compressed data in usdc file' );
|
|
170
|
+
this.budget.decode( size, 'usdc' );
|
|
171
|
+
return decompressFast( data, size );
|
|
172
|
+
|
|
173
|
+
}
|
|
174
|
+
|
|
175
|
+
// count fixed size records at this.pos, which must all be in the file
|
|
176
|
+
records( count, size ) {
|
|
177
|
+
|
|
178
|
+
checkSize( count * size, 'usdc' );
|
|
179
|
+
if ( this.pos + count * size > this.bytes.length ) throw new Error( 'usd-gltf: truncated usdc file' );
|
|
180
|
+
this.budget.decode( count * size, 'usdc' );
|
|
150
181
|
|
|
151
182
|
}
|
|
152
183
|
|
|
@@ -176,7 +207,7 @@ class CrateReader {
|
|
|
176
207
|
|
|
177
208
|
const uncompressedSize = this.u64();
|
|
178
209
|
const compressedSize = this.u64();
|
|
179
|
-
chars =
|
|
210
|
+
chars = this.lz4( this.take( compressedSize ), uncompressedSize );
|
|
180
211
|
|
|
181
212
|
}
|
|
182
213
|
|
|
@@ -213,6 +244,7 @@ class CrateReader {
|
|
|
213
244
|
|
|
214
245
|
if ( this.version < V0_4_0 ) {
|
|
215
246
|
|
|
247
|
+
this.records( count, 16 );
|
|
216
248
|
this.fieldTokens = new Uint32Array( count );
|
|
217
249
|
this.fieldReps = new Uint32Array( count * 2 );
|
|
218
250
|
|
|
@@ -229,7 +261,7 @@ class CrateReader {
|
|
|
229
261
|
|
|
230
262
|
this.fieldTokens = new Uint32Array( this.compressedInts( count ).buffer );
|
|
231
263
|
const repsSize = this.u64();
|
|
232
|
-
const reps =
|
|
264
|
+
const reps = this.lz4( this.take( repsSize ), count * 8 );
|
|
233
265
|
this.fieldReps = new Uint32Array( reps.buffer, 0, count * 2 );
|
|
234
266
|
|
|
235
267
|
}
|
|
@@ -245,6 +277,7 @@ class CrateReader {
|
|
|
245
277
|
|
|
246
278
|
if ( this.version < V0_4_0 ) {
|
|
247
279
|
|
|
280
|
+
this.records( count, 4 );
|
|
248
281
|
this.fieldSets = new Uint32Array( count );
|
|
249
282
|
for ( let i = 0; i < count; i ++ ) this.fieldSets[ i ] = this.u32();
|
|
250
283
|
|
|
@@ -263,16 +296,27 @@ class CrateReader {
|
|
|
263
296
|
if ( ! this.section( 'PATHS' ) ) return;
|
|
264
297
|
|
|
265
298
|
const numPaths = this.u64();
|
|
266
|
-
|
|
267
|
-
|
|
299
|
+
const allocate = () => {
|
|
300
|
+
|
|
301
|
+
this.budget.decode( numPaths * 16, 'usdc' );
|
|
302
|
+
this.paths = new Array( numPaths ).fill( '' );
|
|
303
|
+
this.pathInfo = new Array( numPaths ).fill( null );
|
|
304
|
+
|
|
305
|
+
};
|
|
268
306
|
|
|
269
307
|
if ( this.version < V0_4_0 ) {
|
|
270
308
|
|
|
271
|
-
|
|
309
|
+
const entry = this.version === version( 0, 0, 1 ) ? 16 : 12;
|
|
310
|
+
this.records( numPaths, entry ); // a tree entry per path
|
|
311
|
+
allocate();
|
|
312
|
+
this.readPathTree( entry );
|
|
272
313
|
|
|
273
314
|
} else {
|
|
274
315
|
|
|
275
316
|
const n = this.u64();
|
|
317
|
+
// the tree holds every path, sometimes leaving out an unused first slot
|
|
318
|
+
if ( ! Number.isSafeInteger( numPaths ) || numPaths > n + 1 ) throw new Error( 'usd-gltf: corrupt path table in usdc file' );
|
|
319
|
+
allocate();
|
|
276
320
|
const pathIndexes = this.compressedInts( n );
|
|
277
321
|
const elementTokens = this.compressedInts( n );
|
|
278
322
|
const jumps = this.compressedInts( n );
|
|
@@ -405,6 +449,7 @@ class CrateReader {
|
|
|
405
449
|
if ( this.version < V0_4_0 ) {
|
|
406
450
|
|
|
407
451
|
const pad = this.version === version( 0, 0, 1 ) ? 4 : 0;
|
|
452
|
+
this.records( count, 12 + pad );
|
|
408
453
|
this.specPaths = new Uint32Array( count );
|
|
409
454
|
this.specFieldSets = new Uint32Array( count );
|
|
410
455
|
this.specTypes = new Uint32Array( count );
|
|
@@ -591,6 +636,7 @@ class CrateReader {
|
|
|
591
636
|
readVector( item ) {
|
|
592
637
|
|
|
593
638
|
const n = this.u64();
|
|
639
|
+
this.records( n, 1 ); // every item takes at least a byte
|
|
594
640
|
const out = new Array( n );
|
|
595
641
|
for ( let i = 0; i < n; i ++ ) out[ i ] = item();
|
|
596
642
|
return out;
|
|
@@ -731,8 +777,8 @@ class CrateReader {
|
|
|
731
777
|
|
|
732
778
|
if ( compressed && pod[ 1 ] === 1 && n >= MIN_COMPRESSED_ARRAY_SIZE && ( kind === 'i32' || kind === 'u32' || kind === 'i64' || kind === 'u64' ) && this.version >= V0_5_0 ) {
|
|
733
779
|
|
|
734
|
-
const
|
|
735
|
-
|
|
780
|
+
const data = this.take( this.u64() );
|
|
781
|
+
this.decodedInts( n, kind === 'i32' || kind === 'u32' ? 4 : 8, data );
|
|
736
782
|
if ( kind === 'i32' ) out = decompressInts32( data, n );
|
|
737
783
|
else if ( kind === 'u32' ) out = new Uint32Array( decompressInts32( data, n ).buffer );
|
|
738
784
|
else out = decompressInts64( data, n, kind === 'u64' );
|
|
@@ -769,6 +815,7 @@ class CrateReader {
|
|
|
769
815
|
|
|
770
816
|
readArrayItems( n, item ) {
|
|
771
817
|
|
|
818
|
+
this.records( n, 4 );
|
|
772
819
|
const out = new Array( n );
|
|
773
820
|
for ( let i = 0; i < n; i ++ ) out[ i ] = item();
|
|
774
821
|
return out;
|
|
@@ -778,6 +825,7 @@ class CrateReader {
|
|
|
778
825
|
readRawArray( kind, count ) {
|
|
779
826
|
|
|
780
827
|
const size = SCALAR_SIZE[ kind ];
|
|
828
|
+
this.records( count, kind === 'f16' || kind === 'i64' || kind === 'u64' ? size * 3 : size ); // and any widened copy
|
|
781
829
|
const bytes = this.take( count * size ).slice(); // copy, also aligns
|
|
782
830
|
|
|
783
831
|
switch ( kind ) {
|
|
@@ -812,6 +860,8 @@ class CrateReader {
|
|
|
812
860
|
readCompressedFloats( kind, n ) {
|
|
813
861
|
|
|
814
862
|
const code = this.u8();
|
|
863
|
+
checkSize( n * 8, 'usdc' );
|
|
864
|
+
this.budget.decode( n * ( kind === 'f64' ? 8 : 4 ), 'usdc' );
|
|
815
865
|
const out = typedArrayFor( kind, n );
|
|
816
866
|
|
|
817
867
|
if ( code === 105 ) { // 'i': integers
|
|
@@ -912,11 +962,11 @@ function payloadListOp( value ) {
|
|
|
912
962
|
* @param {string} [identifier]
|
|
913
963
|
* @returns {Layer}
|
|
914
964
|
*/
|
|
915
|
-
export function readUSDC( data, identifier = 'anon.usdc' ) {
|
|
965
|
+
export function readUSDC( data, identifier = 'anon.usdc', { budget = new Budget() } = {} ) {
|
|
916
966
|
|
|
917
967
|
// a plain view, so subarray().slice() copies even for a Node Buffer
|
|
918
968
|
const bytes = ArrayBuffer.isView( data ) ? new Uint8Array( data.buffer, data.byteOffset, data.byteLength ) : new Uint8Array( data );
|
|
919
|
-
const crate = new CrateReader( bytes );
|
|
969
|
+
const crate = new CrateReader( bytes, budget );
|
|
920
970
|
const layer = new Layer( identifier );
|
|
921
971
|
|
|
922
972
|
const specs = new Map(); // path index -> { type, object, order }
|
package/src/usd/usdc/writer.js
CHANGED
|
@@ -4,6 +4,7 @@
|
|
|
4
4
|
import { AssetPath, DICT_TYPES, ListOp, Reference } from '../model.js';
|
|
5
5
|
import { compressFast } from './lz4.js';
|
|
6
6
|
import { compressInts32 } from './intcompression.js';
|
|
7
|
+
import { Budget } from '../../limits.js';
|
|
7
8
|
import { PERMISSIONS, POD, SPEC, SPECIFIERS, TYPE, crateTypeFor, floatToHalf, halfToFloat, isMatrixType, isVecType } from './types.js';
|
|
8
9
|
|
|
9
10
|
const IS_ARRAY = 0x80000000;
|
|
@@ -148,8 +149,9 @@ function pathElements( path ) {
|
|
|
148
149
|
|
|
149
150
|
class CrateWriter {
|
|
150
151
|
|
|
151
|
-
constructor() {
|
|
152
|
+
constructor( budget = new Budget() ) {
|
|
152
153
|
|
|
154
|
+
this.budget = budget;
|
|
153
155
|
this.buf = new Uint8Array( 1 << 16 );
|
|
154
156
|
this.view = new DataView( this.buf.buffer );
|
|
155
157
|
this.pos = 88; // bootstrap, written last
|
|
@@ -173,6 +175,7 @@ class CrateWriter {
|
|
|
173
175
|
this.fieldSetIndex = new Map();
|
|
174
176
|
this.specs = [];
|
|
175
177
|
this.timesCache = new Map();
|
|
178
|
+
this.arrayCache = new Map(); // hash -> [ { values, type, rep } ], so identical arrays are written once
|
|
176
179
|
|
|
177
180
|
}
|
|
178
181
|
|
|
@@ -181,8 +184,10 @@ class CrateWriter {
|
|
|
181
184
|
ensure( n ) {
|
|
182
185
|
|
|
183
186
|
if ( this.pos + n <= this.buf.length ) return;
|
|
187
|
+
this.budget.output( this.pos + n, 'usdc' );
|
|
184
188
|
let size = this.buf.length * 2;
|
|
185
189
|
while ( size < this.pos + n ) size *= 2;
|
|
190
|
+
size = Math.min( size, Math.max( this.pos + n, this.budget.maxOutputBytes ) );
|
|
186
191
|
const buf = new Uint8Array( size );
|
|
187
192
|
buf.set( this.buf );
|
|
188
193
|
this.buf = buf;
|
|
@@ -337,6 +342,7 @@ class CrateWriter {
|
|
|
337
342
|
|
|
338
343
|
addSpec( path, specType, fields ) {
|
|
339
344
|
|
|
345
|
+
this.budget.check();
|
|
340
346
|
const indexes = [];
|
|
341
347
|
|
|
342
348
|
for ( const [ name, [ lo, hi ] ] of fields ) {
|
|
@@ -690,8 +696,24 @@ class CrateWriter {
|
|
|
690
696
|
|
|
691
697
|
}
|
|
692
698
|
|
|
699
|
+
// Identical numeric arrays share one copy in the file, as OpenUSD's crate writer does.
|
|
693
700
|
packArray( values, type ) {
|
|
694
701
|
|
|
702
|
+
if ( ! POD[ type ] || values.length < MIN_COMPRESSED_ARRAY_SIZE ) return this.packArrayData( values, type );
|
|
703
|
+
|
|
704
|
+
const key = type + ':' + values.length + ':' + hashValues( values );
|
|
705
|
+
const entries = this.arrayCache.get( key ) || this.arrayCache.set( key, [] ).get( key );
|
|
706
|
+
const hit = entries.find( ( e ) => sameValues( e.values, values ) );
|
|
707
|
+
if ( hit ) return hit.rep;
|
|
708
|
+
|
|
709
|
+
const rep = this.packArrayData( values, type );
|
|
710
|
+
entries.push( { values, rep } );
|
|
711
|
+
return rep;
|
|
712
|
+
|
|
713
|
+
}
|
|
714
|
+
|
|
715
|
+
packArrayData( values, type ) {
|
|
716
|
+
|
|
695
717
|
const flags = ( IS_ARRAY | ( type << 16 ) ) >>> 0;
|
|
696
718
|
const pod = POD[ type ];
|
|
697
719
|
const total = values.length;
|
|
@@ -949,6 +971,33 @@ class CrateWriter {
|
|
|
949
971
|
|
|
950
972
|
}
|
|
951
973
|
|
|
974
|
+
const F64 = new Float64Array( 1 );
|
|
975
|
+
const F64_WORDS = new Int32Array( F64.buffer );
|
|
976
|
+
|
|
977
|
+
function hashValues( values ) {
|
|
978
|
+
|
|
979
|
+
let h = 0x811c9dc5;
|
|
980
|
+
|
|
981
|
+
for ( let i = 0; i < values.length; i ++ ) {
|
|
982
|
+
|
|
983
|
+
F64[ 0 ] = values[ i ];
|
|
984
|
+
h = Math.imul( h ^ F64_WORDS[ 0 ], 16777619 );
|
|
985
|
+
h = Math.imul( h ^ F64_WORDS[ 1 ], 16777619 );
|
|
986
|
+
|
|
987
|
+
}
|
|
988
|
+
|
|
989
|
+
return h >>> 0;
|
|
990
|
+
|
|
991
|
+
}
|
|
992
|
+
|
|
993
|
+
function sameValues( a, b ) {
|
|
994
|
+
|
|
995
|
+
if ( a.length !== b.length ) return false;
|
|
996
|
+
for ( let i = 0; i < a.length; i ++ ) if ( ! Object.is( a[ i ], b[ i ] ) ) return false;
|
|
997
|
+
return true;
|
|
998
|
+
|
|
999
|
+
}
|
|
1000
|
+
|
|
952
1001
|
function isInt8( v ) {
|
|
953
1002
|
|
|
954
1003
|
return Number.isInteger( v ) && v >= - 128 && v <= 127 && ! Object.is( v, - 0 );
|
|
@@ -1087,11 +1136,12 @@ function writeProperty( w, prop, path ) {
|
|
|
1087
1136
|
/**
|
|
1088
1137
|
* Writes a Layer as a binary USD crate file.
|
|
1089
1138
|
* @param {import('../model.js').Layer} layer
|
|
1139
|
+
* @param {{ budget?: import('../../limits.js').Budget }} [options]
|
|
1090
1140
|
* @returns {Uint8Array}
|
|
1091
1141
|
*/
|
|
1092
|
-
export function writeUSDC( layer ) {
|
|
1142
|
+
export function writeUSDC( layer, { budget = new Budget() } = {} ) {
|
|
1093
1143
|
|
|
1094
|
-
const w = new CrateWriter();
|
|
1144
|
+
const w = new CrateWriter( budget );
|
|
1095
1145
|
const root = layer.root;
|
|
1096
1146
|
|
|
1097
1147
|
const fields = metadataFields( w, layer.metadata );
|
package/src/usd/usdz.js
CHANGED
|
@@ -3,6 +3,7 @@
|
|
|
3
3
|
|
|
4
4
|
import { crc32 } from '../util/crc32.js';
|
|
5
5
|
import { inflate } from '../image/inflate.js';
|
|
6
|
+
import { Budget } from '../limits.js';
|
|
6
7
|
|
|
7
8
|
const LOCAL = 0x04034b50;
|
|
8
9
|
const CENTRAL = 0x02014b50;
|
|
@@ -12,9 +13,10 @@ const ALIGN = 64;
|
|
|
12
13
|
/**
|
|
13
14
|
* Lists the files in a zip, in archive order.
|
|
14
15
|
* @param {Uint8Array} bytes
|
|
16
|
+
* @param {{ budget?: import('../limits.js').Budget }} [options]
|
|
15
17
|
* @returns {Map<string, Uint8Array>}
|
|
16
18
|
*/
|
|
17
|
-
export function readZip( bytes ) {
|
|
19
|
+
export function readZip( bytes, { budget = new Budget() } = {} ) {
|
|
18
20
|
|
|
19
21
|
const view = new DataView( bytes.buffer, bytes.byteOffset, bytes.byteLength );
|
|
20
22
|
const files = new Map();
|
|
@@ -33,6 +35,7 @@ export function readZip( bytes ) {
|
|
|
33
35
|
|
|
34
36
|
const method = view.getUint16( offset + 10, true );
|
|
35
37
|
const compressedSize = view.getUint32( offset + 20, true );
|
|
38
|
+
const size = view.getUint32( offset + 24, true );
|
|
36
39
|
const nameLength = view.getUint16( offset + 28, true );
|
|
37
40
|
const extraLength = view.getUint16( offset + 30, true );
|
|
38
41
|
const commentLength = view.getUint16( offset + 32, true );
|
|
@@ -40,12 +43,13 @@ export function readZip( bytes ) {
|
|
|
40
43
|
const name = decoder.decode( bytes.subarray( offset + 46, offset + 46 + nameLength ) );
|
|
41
44
|
|
|
42
45
|
const dataStart = localOffset + 30 + view.getUint16( localOffset + 26, true ) + view.getUint16( localOffset + 28, true );
|
|
46
|
+
if ( dataStart + compressedSize > bytes.length ) throw new Error( 'usd-gltf: truncated zip archive' );
|
|
43
47
|
const data = bytes.subarray( dataStart, dataStart + compressedSize );
|
|
44
48
|
|
|
45
49
|
if ( ! name.endsWith( '/' ) ) {
|
|
46
50
|
|
|
47
51
|
if ( method === 0 ) files.set( name, data );
|
|
48
|
-
else if ( method === 8 ) files.set( name, inflate( data ) );
|
|
52
|
+
else if ( method === 8 ) { budget.decode( size, 'zip' ); files.set( name, inflate( data, size, size ) ); }
|
|
49
53
|
else throw new Error( `usd-gltf: unsupported zip compression ${ method } for ${ name }` );
|
|
50
54
|
|
|
51
55
|
}
|
|
@@ -61,9 +65,10 @@ export function readZip( bytes ) {
|
|
|
61
65
|
/**
|
|
62
66
|
* Writes a USDZ package. The first entry must be the root layer.
|
|
63
67
|
* @param {Array<{ name: string, data: Uint8Array }>} entries
|
|
68
|
+
* @param {{ budget?: import('../limits.js').Budget }} [options]
|
|
64
69
|
* @returns {Uint8Array}
|
|
65
70
|
*/
|
|
66
|
-
export function writeUSDZ( entries ) {
|
|
71
|
+
export function writeUSDZ( entries, { budget = new Budget() } = {} ) {
|
|
67
72
|
|
|
68
73
|
const encoder = new TextEncoder();
|
|
69
74
|
const locals = [];
|
|
@@ -127,6 +132,7 @@ export function writeUSDZ( entries ) {
|
|
|
127
132
|
ev.setUint32( 12, centralSize, true );
|
|
128
133
|
ev.setUint32( 16, offset, true );
|
|
129
134
|
|
|
135
|
+
budget.output( offset + centralSize + 22, 'usdz' );
|
|
130
136
|
const out = new Uint8Array( offset + centralSize + 22 );
|
|
131
137
|
let p = 0;
|
|
132
138
|
for ( const part of [ ...locals, ...centrals, end ] ) {
|