usd-gltf 0.1.0 → 0.3.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 +36 -3
- package/package.json +12 -2
- package/src/convert/gltf-to-usd.js +166 -46
- package/src/convert/usd-material.js +7 -2
- package/src/convert/usd-to-gltf.js +8 -4
- package/src/gltf/document.js +109 -19
- package/src/gltf/draco.js +191 -0
- package/src/gltf/glb.js +2 -2
- package/src/image/index.js +52 -1
- package/src/image/inflate.js +14 -12
- package/src/image/png.js +1 -1
- package/src/index.d.ts +30 -4
- package/src/index.js +33 -14
- package/src/limits.js +101 -0
- package/src/usd/stage.js +13 -9
- package/src/usd/usda/writer.js +16 -1
- 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/gltf/document.js
CHANGED
|
@@ -2,6 +2,8 @@
|
|
|
2
2
|
|
|
3
3
|
import { readGLB, writeGLB } from './glb.js';
|
|
4
4
|
import { decodeMeshoptView } from './meshopt.js';
|
|
5
|
+
import { decodeDracoPrimitive, loadDracoDecoder, needsDraco } from './draco.js';
|
|
6
|
+
import { Budget, checkSize } from '../limits.js';
|
|
5
7
|
|
|
6
8
|
const COMPONENT_ARRAYS = {
|
|
7
9
|
5120: Int8Array,
|
|
@@ -14,6 +16,25 @@ const COMPONENT_ARRAYS = {
|
|
|
14
16
|
|
|
15
17
|
export const TYPE_SIZES = { SCALAR: 1, VEC2: 2, VEC3: 3, VEC4: 4, MAT2: 4, MAT3: 9, MAT4: 16 };
|
|
16
18
|
|
|
19
|
+
// Extensions an asset may require. Material extensions without a UsdPreviewSurface equivalent are
|
|
20
|
+
// approximated and kept on the USD material for the way back, so every KHR_materials_* is accepted.
|
|
21
|
+
const SUPPORTED_EXTENSIONS = new Set( [
|
|
22
|
+
'EXT_mesh_gpu_instancing', 'EXT_meshopt_compression', 'KHR_meshopt_compression', 'KHR_mesh_quantization', 'KHR_draco_mesh_compression',
|
|
23
|
+
'EXT_texture_webp', 'EXT_texture_avif', 'KHR_texture_transform', 'KHR_animation_pointer', 'KHR_lights_punctual',
|
|
24
|
+
'KHR_node_visibility', 'KHR_xmp_json_ld',
|
|
25
|
+
] );
|
|
26
|
+
|
|
27
|
+
/**
|
|
28
|
+
* The asset's required extensions that can't be converted (e.g. KHR_texture_basisu).
|
|
29
|
+
* @param {object} json
|
|
30
|
+
* @returns {string[]}
|
|
31
|
+
*/
|
|
32
|
+
export function unsupportedExtensions( json ) {
|
|
33
|
+
|
|
34
|
+
return ( json.extensionsRequired || [] ).filter( ( name ) => ! SUPPORTED_EXTENSIONS.has( name ) && ! name.startsWith( 'KHR_materials_' ) );
|
|
35
|
+
|
|
36
|
+
}
|
|
37
|
+
|
|
17
38
|
function decodeDataURI( uri ) {
|
|
18
39
|
|
|
19
40
|
const comma = uri.indexOf( ',' );
|
|
@@ -38,13 +59,15 @@ function decodeDataURI( uri ) {
|
|
|
38
59
|
*/
|
|
39
60
|
export class GLTFDocument {
|
|
40
61
|
|
|
41
|
-
constructor( json, buffers, images ) {
|
|
62
|
+
constructor( json, buffers, images, budget = new Budget() ) {
|
|
42
63
|
|
|
43
64
|
this.json = json;
|
|
44
65
|
this.buffers = buffers; // Uint8Array per json.buffers entry
|
|
45
66
|
this.images = images; // { bytes, mimeType, name } per json.images entry
|
|
67
|
+
this.budget = budget;
|
|
46
68
|
this.accessorCache = new Map();
|
|
47
69
|
this.decodedViews = new Map(); // bufferView index -> bytes, for compressed views
|
|
70
|
+
this.decodedAccessors = new Map(); // accessor index -> data, for Draco compressed primitives
|
|
48
71
|
|
|
49
72
|
}
|
|
50
73
|
|
|
@@ -52,9 +75,11 @@ export class GLTFDocument {
|
|
|
52
75
|
|
|
53
76
|
const decoded = this.decodedViews.get( index );
|
|
54
77
|
if ( decoded ) return decoded;
|
|
55
|
-
const view = this.json.bufferViews[ index ];
|
|
56
|
-
const buffer = this.buffers[ view.buffer ];
|
|
57
|
-
|
|
78
|
+
const view = this.json.bufferViews?.[ index ];
|
|
79
|
+
const buffer = view && this.buffers[ view.buffer ];
|
|
80
|
+
const start = view?.byteOffset || 0;
|
|
81
|
+
if ( ! buffer || ! ( start + view.byteLength <= buffer.length ) ) throw new Error( `usd-gltf: bufferView ${ index } is outside its buffer` );
|
|
82
|
+
return buffer.subarray( start, start + view.byteLength );
|
|
58
83
|
|
|
59
84
|
}
|
|
60
85
|
|
|
@@ -68,11 +93,16 @@ export class GLTFDocument {
|
|
|
68
93
|
const key = index + ( normalize ? 'n' : '' );
|
|
69
94
|
if ( this.accessorCache.has( key ) ) return this.accessorCache.get( key );
|
|
70
95
|
|
|
71
|
-
const accessor = this.json.accessors[ index ];
|
|
72
|
-
const ArrayType = COMPONENT_ARRAYS[ accessor
|
|
73
|
-
const itemSize = TYPE_SIZES[ accessor
|
|
96
|
+
const accessor = this.json.accessors?.[ index ];
|
|
97
|
+
const ArrayType = COMPONENT_ARRAYS[ accessor?.componentType ];
|
|
98
|
+
const itemSize = TYPE_SIZES[ accessor?.type ];
|
|
99
|
+
if ( ! ArrayType || ! itemSize ) throw new Error( `usd-gltf: accessor ${ index } is missing or has an invalid type` );
|
|
74
100
|
const count = accessor.count;
|
|
75
|
-
|
|
101
|
+
const stage = `accessor ${ index }`;
|
|
102
|
+
const size = count * itemSize * ArrayType.BYTES_PER_ELEMENT;
|
|
103
|
+
checkSize( size, stage );
|
|
104
|
+
|
|
105
|
+
let read = null;
|
|
76
106
|
|
|
77
107
|
if ( accessor.bufferView !== undefined ) {
|
|
78
108
|
|
|
@@ -88,6 +118,18 @@ export class GLTFDocument {
|
|
|
88
118
|
const columnStride = Math.ceil( rows * elementBytes / 4 ) * 4;
|
|
89
119
|
const packed = columns === 1 ? itemSize * elementBytes : columnStride * columns;
|
|
90
120
|
const stride = view.byteStride || packed;
|
|
121
|
+
if ( count > 0 && ! ( offset + stride * ( count - 1 ) + packed <= bytes.length ) ) throw new Error( `usd-gltf: ${ stage } reads past the end of its bufferView` );
|
|
122
|
+
read = { view, bytes, elementBytes, offset, columns, rows, columnStride, stride };
|
|
123
|
+
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
const decoded = accessor.bufferView === undefined ? this.decodedAccessors.get( index ) : undefined; // charged when decoded
|
|
127
|
+
if ( ! decoded ) this.budget.decode( size, stage );
|
|
128
|
+
let array = decoded || new ArrayType( count * itemSize );
|
|
129
|
+
|
|
130
|
+
if ( read ) {
|
|
131
|
+
|
|
132
|
+
const { bytes, elementBytes, offset, columns, rows, columnStride, stride } = read;
|
|
91
133
|
|
|
92
134
|
const dv = new DataView( bytes.buffer, bytes.byteOffset, bytes.byteLength );
|
|
93
135
|
const getters = { 5120: 'getInt8', 5121: 'getUint8', 5122: 'getInt16', 5123: 'getUint16', 5125: 'getUint32', 5126: 'getFloat32' };
|
|
@@ -120,9 +162,16 @@ export class GLTFDocument {
|
|
|
120
162
|
const IndexType = COMPONENT_ARRAYS[ indices.componentType ];
|
|
121
163
|
const ib = this.bufferViewBytes( indices.bufferView );
|
|
122
164
|
const vb = this.bufferViewBytes( values.bufferView );
|
|
165
|
+
if ( ! IndexType || ! Number.isSafeInteger( n ) || n < 0 || n > count ) throw new Error( `usd-gltf: ${ stage } has an invalid sparse count` );
|
|
166
|
+
if ( ! ( ( indices.byteOffset || 0 ) + n * IndexType.BYTES_PER_ELEMENT <= ib.length && ( values.byteOffset || 0 ) + n * itemSize * ArrayType.BYTES_PER_ELEMENT <= vb.length ) ) throw new Error( `usd-gltf: ${ stage } sparse data is outside its bufferViews` );
|
|
123
167
|
const idx = new IndexType( ib.slice( indices.byteOffset || 0, ( indices.byteOffset || 0 ) + n * IndexType.BYTES_PER_ELEMENT ).buffer );
|
|
124
168
|
const vals = new ArrayType( vb.slice( values.byteOffset || 0, ( values.byteOffset || 0 ) + n * itemSize * ArrayType.BYTES_PER_ELEMENT ).buffer );
|
|
125
|
-
for ( let i = 0; i < n; i ++ )
|
|
169
|
+
for ( let i = 0; i < n; i ++ ) {
|
|
170
|
+
|
|
171
|
+
if ( idx[ i ] >= count ) throw new Error( `usd-gltf: ${ stage } sparse index is out of range` );
|
|
172
|
+
for ( let c = 0; c < itemSize; c ++ ) array[ idx[ i ] * itemSize + c ] = vals[ i * itemSize + c ];
|
|
173
|
+
|
|
174
|
+
}
|
|
126
175
|
|
|
127
176
|
}
|
|
128
177
|
|
|
@@ -130,6 +179,7 @@ export class GLTFDocument {
|
|
|
130
179
|
|
|
131
180
|
if ( normalize && normalized ) {
|
|
132
181
|
|
|
182
|
+
this.budget.decode( array.length * 4, stage );
|
|
133
183
|
const out = new Float32Array( array.length );
|
|
134
184
|
const ct = accessor.componentType;
|
|
135
185
|
for ( let i = 0; i < array.length; i ++ ) {
|
|
@@ -154,7 +204,9 @@ export class GLTFDocument {
|
|
|
154
204
|
getFloats( index ) {
|
|
155
205
|
|
|
156
206
|
const { array, itemSize, count } = this.getAccessor( index );
|
|
157
|
-
|
|
207
|
+
if ( array instanceof Float32Array ) return { array, itemSize, count };
|
|
208
|
+
this.budget.decode( array.length * 4, `accessor ${ index }` );
|
|
209
|
+
return { array: Float32Array.from( array ), itemSize, count };
|
|
158
210
|
|
|
159
211
|
}
|
|
160
212
|
|
|
@@ -163,15 +215,19 @@ export class GLTFDocument {
|
|
|
163
215
|
/**
|
|
164
216
|
* Loads a glTF or GLB.
|
|
165
217
|
* @param {Uint8Array} bytes
|
|
166
|
-
* @param {{ resolver?: ( uri: string ) => Promise<Uint8Array>|Uint8Array }} [options] loads external uris (already decoded from URI escaping)
|
|
218
|
+
* @param {{ resolver?: ( uri: string ) => Promise<Uint8Array>|Uint8Array, dracoDecoder?: object|Promise<object> }} [options] resolver loads external uris (already decoded from URI escaping); dracoDecoder is a Draco decoder module, loaded when needed if not given
|
|
167
219
|
*/
|
|
168
|
-
export async function readGLTF( bytes, { resolver = null } = {} ) {
|
|
220
|
+
export async function readGLTF( bytes, { resolver = null, budget = new Budget(), dracoDecoder = null } = {} ) {
|
|
169
221
|
|
|
170
222
|
let json, bin = null;
|
|
223
|
+
bytes = plainBytes( bytes );
|
|
171
224
|
|
|
172
225
|
if ( bytes[ 0 ] === 0x67 && bytes[ 1 ] === 0x6C && bytes[ 2 ] === 0x54 && bytes[ 3 ] === 0x46 ) ( { json, bin } = readGLB( bytes ) );
|
|
173
226
|
else json = JSON.parse( new TextDecoder().decode( bytes ).replace( /^/, '' ) );
|
|
174
227
|
|
|
228
|
+
const unsupported = unsupportedExtensions( json );
|
|
229
|
+
if ( unsupported.length ) throw new Error( `usd-gltf: required extension${ unsupported.length > 1 ? 's' : '' } ${ unsupported.join( ', ' ) } not supported` );
|
|
230
|
+
|
|
175
231
|
async function load( uri ) {
|
|
176
232
|
|
|
177
233
|
if ( uri.startsWith( 'data:' ) ) return decodeDataURI( uri ).bytes;
|
|
@@ -179,7 +235,7 @@ export async function readGLTF( bytes, { resolver = null } = {} ) {
|
|
|
179
235
|
let path = uri;
|
|
180
236
|
try { path = decodeURIComponent( uri ); } catch { /* keep as is */ }
|
|
181
237
|
const data = await resolver( path );
|
|
182
|
-
return
|
|
238
|
+
return plainBytes( data );
|
|
183
239
|
|
|
184
240
|
}
|
|
185
241
|
|
|
@@ -188,8 +244,9 @@ export async function readGLTF( bytes, { resolver = null } = {} ) {
|
|
|
188
244
|
if ( buffer.uri === undefined ) {
|
|
189
245
|
|
|
190
246
|
if ( i === 0 && bin ) return bin;
|
|
191
|
-
// e.g. EXT_meshopt_compression fallback buffers
|
|
192
|
-
|
|
247
|
+
// e.g. EXT_meshopt_compression fallback buffers: only read through decoded views, so they
|
|
248
|
+
// need no storage, and any other view into one fails its bounds check
|
|
249
|
+
return new Uint8Array( 0 );
|
|
193
250
|
|
|
194
251
|
}
|
|
195
252
|
|
|
@@ -197,15 +254,36 @@ export async function readGLTF( bytes, { resolver = null } = {} ) {
|
|
|
197
254
|
|
|
198
255
|
} ) );
|
|
199
256
|
|
|
200
|
-
const doc = new GLTFDocument( json, buffers, [] );
|
|
257
|
+
const doc = new GLTFDocument( json, buffers, [], budget );
|
|
201
258
|
|
|
202
259
|
( json.bufferViews || [] ).forEach( ( view, i ) => {
|
|
203
260
|
|
|
204
261
|
const ext = view.extensions?.KHR_meshopt_compression || view.extensions?.EXT_meshopt_compression;
|
|
205
|
-
if (
|
|
262
|
+
if ( ! ext ) return;
|
|
263
|
+
const start = ext.byteOffset || 0;
|
|
264
|
+
if ( ! buffers[ ext.buffer ] || ! ( start + ext.byteLength <= buffers[ ext.buffer ].length ) ) throw new Error( `usd-gltf: bufferView ${ i } compressed data is outside its buffer` );
|
|
265
|
+
budget.decode( ext.count * ext.byteStride, `bufferView ${ i }` );
|
|
266
|
+
doc.decodedViews.set( i, decodeMeshoptView( ext, buffers ) );
|
|
206
267
|
|
|
207
268
|
} );
|
|
208
269
|
|
|
270
|
+
// Draco primitives without uncompressed fallback data
|
|
271
|
+
const draco = ( json.meshes || [] ).flatMap( ( mesh ) => mesh.primitives ).filter( ( p ) => needsDraco( json, p ) );
|
|
272
|
+
|
|
273
|
+
if ( draco.length ) {
|
|
274
|
+
|
|
275
|
+
const decoder = await ( dracoDecoder || loadDracoDecoder() );
|
|
276
|
+
|
|
277
|
+
for ( const primitive of draco ) {
|
|
278
|
+
|
|
279
|
+
budget.check();
|
|
280
|
+
const bytes = doc.bufferViewBytes( primitive.extensions.KHR_draco_mesh_compression.bufferView );
|
|
281
|
+
for ( const [ index, data ] of decodeDracoPrimitive( decoder, bytes, json, primitive, budget ) ) doc.decodedAccessors.set( index, data );
|
|
282
|
+
|
|
283
|
+
}
|
|
284
|
+
|
|
285
|
+
}
|
|
286
|
+
|
|
209
287
|
doc.images = await Promise.all( ( json.images || [] ).map( async ( image ) => {
|
|
210
288
|
|
|
211
289
|
if ( image.bufferView !== undefined ) return { bytes: doc.bufferViewBytes( image.bufferView ), mimeType: image.mimeType, name: image.name, uri: null };
|
|
@@ -228,6 +306,14 @@ export async function readGLTF( bytes, { resolver = null } = {} ) {
|
|
|
228
306
|
|
|
229
307
|
}
|
|
230
308
|
|
|
309
|
+
// A plain Uint8Array view: a Node Buffer's slice() aliases its memory instead of copying, so data
|
|
310
|
+
// read through .slice().buffer would be the wrong bytes.
|
|
311
|
+
function plainBytes( data ) {
|
|
312
|
+
|
|
313
|
+
return ArrayBuffer.isView( data ) ? new Uint8Array( data.buffer, data.byteOffset, data.byteLength ) : new Uint8Array( data );
|
|
314
|
+
|
|
315
|
+
}
|
|
316
|
+
|
|
231
317
|
function guessMime( uri ) {
|
|
232
318
|
|
|
233
319
|
const ext = uri.split( '?' )[ 0 ].split( '.' ).pop().toLowerCase();
|
|
@@ -240,8 +326,9 @@ function guessMime( uri ) {
|
|
|
240
326
|
*/
|
|
241
327
|
export class GLTFBuilder {
|
|
242
328
|
|
|
243
|
-
constructor( generator = 'usd-gltf' ) {
|
|
329
|
+
constructor( generator = 'usd-gltf', budget = new Budget() ) {
|
|
244
330
|
|
|
331
|
+
this.budget = budget;
|
|
245
332
|
this.json = { asset: { version: '2.0', generator }, scenes: [], nodes: [], meshes: [], materials: [], textures: [], images: [], samplers: [], accessors: [], bufferViews: [], buffers: [] };
|
|
246
333
|
this.chunks = [];
|
|
247
334
|
this.byteLength = 0;
|
|
@@ -262,6 +349,7 @@ export class GLTFBuilder {
|
|
|
262
349
|
const pad = ( 4 - ( this.byteLength % 4 ) ) % 4;
|
|
263
350
|
if ( pad ) { this.chunks.push( new Uint8Array( pad ) ); this.byteLength += pad; }
|
|
264
351
|
|
|
352
|
+
this.budget.output( this.byteLength + bytes.byteLength, 'glb buffer' );
|
|
265
353
|
const view = { buffer: 0, byteOffset: this.byteLength, byteLength: bytes.byteLength };
|
|
266
354
|
if ( target ) view.target = target;
|
|
267
355
|
this.chunks.push( bytes instanceof Uint8Array ? bytes : new Uint8Array( bytes.buffer, bytes.byteOffset, bytes.byteLength ) );
|
|
@@ -323,7 +411,9 @@ export class GLTFBuilder {
|
|
|
323
411
|
|
|
324
412
|
toGLB() {
|
|
325
413
|
|
|
326
|
-
const json = this.finalize();
|
|
414
|
+
const json = new TextEncoder().encode( JSON.stringify( this.finalize() ) );
|
|
415
|
+
const pad4 = ( n ) => Math.ceil( n / 4 ) * 4;
|
|
416
|
+
this.budget.output( 20 + pad4( json.byteLength ) + ( this.byteLength ? 8 + pad4( this.byteLength ) : 0 ), 'glb' );
|
|
327
417
|
const bin = new Uint8Array( this.byteLength );
|
|
328
418
|
let offset = 0;
|
|
329
419
|
for ( const chunk of this.chunks ) {
|
|
@@ -0,0 +1,191 @@
|
|
|
1
|
+
// KHR_draco_mesh_compression, decoded with Google's Draco decoder, which usd-gltf doesn't bundle.
|
|
2
|
+
// The decoder is the dracoDecoder option when given; otherwise the draco3d package in Node (an
|
|
3
|
+
// optional peer dependency), or in browsers Google's CDN build, pinned to one version and checked
|
|
4
|
+
// against its hash before it runs. Only loaded when a file needs it.
|
|
5
|
+
// https://github.com/KhronosGroup/glTF/tree/main/extensions/2.0/Khronos/KHR_draco_mesh_compression
|
|
6
|
+
|
|
7
|
+
const CDN = 'https://www.gstatic.com/draco/versioned/decoders/1.5.7/';
|
|
8
|
+
const CDN_FILES = {
|
|
9
|
+
'draco_wasm_wrapper_gltf.js': 'i7KVLSun1n4UFPjfgZQQywQ0pma+U/Zx//dfaIQ9dvY=',
|
|
10
|
+
'draco_decoder_gltf.wasm': 'cS2zRJriBB1uiiJMOVvabO20nlEyL644t9ub64s4GIk=',
|
|
11
|
+
};
|
|
12
|
+
|
|
13
|
+
const DATA_TYPES = { 5120: 'DT_INT8', 5121: 'DT_UINT8', 5122: 'DT_INT16', 5123: 'DT_UINT16', 5125: 'DT_UINT32', 5126: 'DT_FLOAT32' };
|
|
14
|
+
const COMPONENT_ARRAYS = { 5120: Int8Array, 5121: Uint8Array, 5122: Int16Array, 5123: Uint16Array, 5125: Uint32Array, 5126: Float32Array };
|
|
15
|
+
const TYPE_SIZES = { SCALAR: 1, VEC2: 2, VEC3: 3, VEC4: 4, MAT2: 4, MAT3: 9, MAT4: 16 };
|
|
16
|
+
|
|
17
|
+
let defaultDecoder = null;
|
|
18
|
+
|
|
19
|
+
/**
|
|
20
|
+
* The default Draco decoder module: draco3d in Node, Google's CDN in browsers. Loaded once.
|
|
21
|
+
* @returns {Promise<object>}
|
|
22
|
+
*/
|
|
23
|
+
export function loadDracoDecoder() {
|
|
24
|
+
|
|
25
|
+
defaultDecoder ||= ( isNode() ? loadNode() : loadDracoFromCDN() ).catch( ( e ) => {
|
|
26
|
+
|
|
27
|
+
defaultDecoder = null; // let a later conversion try again
|
|
28
|
+
throw e;
|
|
29
|
+
|
|
30
|
+
} );
|
|
31
|
+
|
|
32
|
+
return defaultDecoder;
|
|
33
|
+
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
function isNode() {
|
|
37
|
+
|
|
38
|
+
return typeof process !== 'undefined' && !! process.versions?.node;
|
|
39
|
+
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
async function loadNode() {
|
|
43
|
+
|
|
44
|
+
const name = 'draco3d'; // not a literal, so bundlers don't try to include it
|
|
45
|
+
let draco3d;
|
|
46
|
+
|
|
47
|
+
try {
|
|
48
|
+
|
|
49
|
+
draco3d = await import( /* webpackIgnore: true */ /* @vite-ignore */ name );
|
|
50
|
+
|
|
51
|
+
} catch {
|
|
52
|
+
|
|
53
|
+
throw new Error( 'usd-gltf: this glTF is Draco compressed; install the decoder with npm install draco3d, or pass a dracoDecoder' );
|
|
54
|
+
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
return ( draco3d.default ?? draco3d ).createDecoderModule();
|
|
58
|
+
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
// Exported for tests; loadDracoDecoder picks the source.
|
|
62
|
+
export async function loadDracoFromCDN() {
|
|
63
|
+
|
|
64
|
+
if ( ! globalThis.crypto?.subtle ) throw new Error( 'usd-gltf: loading the Draco decoder needs a secure context (https) to check it; pass a dracoDecoder instead' );
|
|
65
|
+
const [ wrapper, wasm ] = await Promise.all( Object.keys( CDN_FILES ).map( fetchVerified ) );
|
|
66
|
+
// the wrapper is a classic script defining DracoDecoderModule, so it can't be imported as a module
|
|
67
|
+
const factory = new Function( new TextDecoder().decode( wrapper ) + '\nreturn DracoDecoderModule;' )();
|
|
68
|
+
return factory( { wasmBinary: wasm.buffer } );
|
|
69
|
+
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
async function fetchVerified( name ) {
|
|
73
|
+
|
|
74
|
+
const response = await fetch( CDN + name );
|
|
75
|
+
if ( ! response.ok ) throw new Error( `usd-gltf: could not load the Draco decoder (${ response.status } for ${ CDN + name })` );
|
|
76
|
+
const bytes = new Uint8Array( await response.arrayBuffer() );
|
|
77
|
+
const digest = new Uint8Array( await crypto.subtle.digest( 'SHA-256', bytes ) );
|
|
78
|
+
if ( btoa( String.fromCharCode( ...digest ) ) !== CDN_FILES[ name ] ) throw new Error( `usd-gltf: ${ CDN + name } doesn't match its expected hash` );
|
|
79
|
+
return bytes;
|
|
80
|
+
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
/**
|
|
84
|
+
* Whether a primitive's data is only in its Draco extension: Draco is decoded only when the
|
|
85
|
+
* accessors have no uncompressed fallback.
|
|
86
|
+
*/
|
|
87
|
+
export function needsDraco( json, primitive ) {
|
|
88
|
+
|
|
89
|
+
if ( ! primitive.extensions?.KHR_draco_mesh_compression ) return false;
|
|
90
|
+
const used = [ ...Object.values( primitive.attributes ), primitive.indices ].filter( ( a ) => a !== undefined );
|
|
91
|
+
return used.some( ( a ) => json.accessors[ a ]?.bufferView === undefined && ! json.accessors[ a ]?.sparse );
|
|
92
|
+
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
/**
|
|
96
|
+
* Decodes one Draco compressed primitive into the data of the accessors it describes.
|
|
97
|
+
* @param {object} draco decoder module
|
|
98
|
+
* @param {Uint8Array} bytes the extension's bufferView
|
|
99
|
+
* @param {object} json
|
|
100
|
+
* @param {object} primitive
|
|
101
|
+
* @param {import('../limits.js').Budget} budget
|
|
102
|
+
* @returns {Map<number, ArrayLike<number>>} accessor index -> data, in the accessor's component type
|
|
103
|
+
*/
|
|
104
|
+
export function decodeDracoPrimitive( draco, bytes, json, primitive, budget ) {
|
|
105
|
+
|
|
106
|
+
const ext = primitive.extensions.KHR_draco_mesh_compression;
|
|
107
|
+
const decoder = new draco.Decoder();
|
|
108
|
+
const array = new Int8Array( bytes.buffer, bytes.byteOffset, bytes.byteLength );
|
|
109
|
+
const out = new Map();
|
|
110
|
+
let geometry = null;
|
|
111
|
+
|
|
112
|
+
try {
|
|
113
|
+
|
|
114
|
+
const type = decoder.GetEncodedGeometryType( array );
|
|
115
|
+
let status;
|
|
116
|
+
|
|
117
|
+
if ( type === draco.TRIANGULAR_MESH ) {
|
|
118
|
+
|
|
119
|
+
geometry = new draco.Mesh();
|
|
120
|
+
status = decoder.DecodeArrayToMesh( array, array.byteLength, geometry );
|
|
121
|
+
|
|
122
|
+
} else if ( type === draco.POINT_CLOUD ) {
|
|
123
|
+
|
|
124
|
+
geometry = new draco.PointCloud();
|
|
125
|
+
status = decoder.DecodeArrayToPointCloud( array, array.byteLength, geometry );
|
|
126
|
+
|
|
127
|
+
} else {
|
|
128
|
+
|
|
129
|
+
throw new Error( 'usd-gltf: Draco data is neither a mesh nor a point cloud' );
|
|
130
|
+
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
if ( ! status.ok() || geometry.ptr === 0 ) throw new Error( `usd-gltf: Draco decoding failed: ${ status.error_msg() }` );
|
|
134
|
+
|
|
135
|
+
const points = geometry.num_points();
|
|
136
|
+
|
|
137
|
+
for ( const [ name, id ] of Object.entries( ext.attributes ) ) {
|
|
138
|
+
|
|
139
|
+
const index = primitive.attributes[ name ];
|
|
140
|
+
const accessor = json.accessors[ index ];
|
|
141
|
+
if ( accessor === undefined ) continue;
|
|
142
|
+
const ArrayType = COMPONENT_ARRAYS[ accessor.componentType ];
|
|
143
|
+
const size = TYPE_SIZES[ accessor.type ];
|
|
144
|
+
const attribute = decoder.GetAttributeByUniqueId( geometry, id );
|
|
145
|
+
if ( ! ArrayType || ! size || ! attribute || attribute.ptr === 0 ) throw new Error( `usd-gltf: Draco data has no ${ name } attribute ${ id }` );
|
|
146
|
+
if ( points !== accessor.count || attribute.num_components() !== size ) throw new Error( `usd-gltf: Draco ${ name } doesn't match accessor ${ index }` );
|
|
147
|
+
out.set( index, read( draco, points * size, ArrayType, budget, ( length, ptr ) => decoder.GetAttributeDataArrayForAllPoints( geometry, attribute, draco[ DATA_TYPES[ accessor.componentType ] ], length, ptr ) ) );
|
|
148
|
+
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
if ( primitive.indices !== undefined && type === draco.TRIANGULAR_MESH ) {
|
|
152
|
+
|
|
153
|
+
const accessor = json.accessors[ primitive.indices ];
|
|
154
|
+
const count = geometry.num_faces() * 3;
|
|
155
|
+
if ( ! accessor || accessor.count !== count ) throw new Error( `usd-gltf: Draco indices don't match accessor ${ primitive.indices }` );
|
|
156
|
+
const indices = read( draco, count, Uint32Array, budget, ( length, ptr ) => decoder.GetTrianglesUInt32Array( geometry, length, ptr ) );
|
|
157
|
+
const IndexType = COMPONENT_ARRAYS[ accessor.componentType ] || Uint32Array;
|
|
158
|
+
out.set( primitive.indices, IndexType === Uint32Array ? indices : IndexType.from( indices ) );
|
|
159
|
+
|
|
160
|
+
}
|
|
161
|
+
|
|
162
|
+
} finally {
|
|
163
|
+
|
|
164
|
+
if ( geometry ) draco.destroy( geometry );
|
|
165
|
+
draco.destroy( decoder );
|
|
166
|
+
|
|
167
|
+
}
|
|
168
|
+
|
|
169
|
+
return out;
|
|
170
|
+
|
|
171
|
+
}
|
|
172
|
+
|
|
173
|
+
// Has the decoder fill a buffer on its heap, then copies it out.
|
|
174
|
+
function read( draco, count, ArrayType, budget, fill ) {
|
|
175
|
+
|
|
176
|
+
const length = count * ArrayType.BYTES_PER_ELEMENT;
|
|
177
|
+
budget.decode( length, 'draco' );
|
|
178
|
+
const ptr = draco._malloc( length );
|
|
179
|
+
|
|
180
|
+
try {
|
|
181
|
+
|
|
182
|
+
if ( ! fill( length, ptr ) ) throw new Error( 'usd-gltf: Draco attribute could not be read' );
|
|
183
|
+
return new ArrayType( draco.HEAPF32.buffer, ptr, count ).slice();
|
|
184
|
+
|
|
185
|
+
} finally {
|
|
186
|
+
|
|
187
|
+
draco._free( ptr );
|
|
188
|
+
|
|
189
|
+
}
|
|
190
|
+
|
|
191
|
+
}
|
package/src/gltf/glb.js
CHANGED
|
@@ -68,13 +68,13 @@ export function readGLB( data ) {
|
|
|
68
68
|
|
|
69
69
|
/**
|
|
70
70
|
* Packs glTF JSON and an optional binary buffer into a GLB.
|
|
71
|
-
* @param {object} json
|
|
71
|
+
* @param {object|Uint8Array} json the glTF JSON, or it already encoded as UTF-8
|
|
72
72
|
* @param {Uint8Array|null} [bin]
|
|
73
73
|
* @returns {Uint8Array}
|
|
74
74
|
*/
|
|
75
75
|
export function writeGLB( json, bin = null ) {
|
|
76
76
|
|
|
77
|
-
const jsonBytes = encoder.encode( JSON.stringify( json ) );
|
|
77
|
+
const jsonBytes = json instanceof Uint8Array ? json : encoder.encode( JSON.stringify( json ) );
|
|
78
78
|
const jsonLength = pad4( jsonBytes.byteLength );
|
|
79
79
|
const binLength = bin ? pad4( bin.byteLength ) : 0;
|
|
80
80
|
|
package/src/image/index.js
CHANGED
|
@@ -65,6 +65,54 @@ export function isGreyscaleImage( bytes ) {
|
|
|
65
65
|
|
|
66
66
|
}
|
|
67
67
|
|
|
68
|
+
/**
|
|
69
|
+
* Width, height and bytes per pixel of a PNG, JPEG or WebP from its header, or null.
|
|
70
|
+
* @param {Uint8Array} bytes
|
|
71
|
+
* @returns {{ width: number, height: number, pixelBytes: number }|null}
|
|
72
|
+
*/
|
|
73
|
+
export function imageSize( bytes ) {
|
|
74
|
+
|
|
75
|
+
bytes = toBytes( bytes );
|
|
76
|
+
const type = imageMimeType( bytes );
|
|
77
|
+
const be16 = ( o ) => ( bytes[ o ] << 8 ) | bytes[ o + 1 ];
|
|
78
|
+
const le16 = ( o ) => bytes[ o ] | ( bytes[ o + 1 ] << 8 );
|
|
79
|
+
const le24 = ( o ) => le16( o ) | ( bytes[ o + 2 ] << 16 );
|
|
80
|
+
|
|
81
|
+
if ( type === 'image/png' && bytes.length >= 26 ) {
|
|
82
|
+
|
|
83
|
+
const width = ( be16( 16 ) * 65536 ) + be16( 18 ), height = ( be16( 20 ) * 65536 ) + be16( 22 );
|
|
84
|
+
return { width, height, pixelBytes: bytes[ 24 ] === 16 ? 8 : 4 };
|
|
85
|
+
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
if ( type === 'image/jpeg' ) {
|
|
89
|
+
|
|
90
|
+
for ( let o = 2; o + 9 < bytes.length; ) {
|
|
91
|
+
|
|
92
|
+
if ( bytes[ o ] !== 0xFF ) return null;
|
|
93
|
+
const marker = bytes[ o + 1 ];
|
|
94
|
+
if ( marker >= 0xC0 && marker <= 0xCF && marker !== 0xC4 && marker !== 0xC8 && marker !== 0xCC ) return { width: be16( o + 7 ), height: be16( o + 5 ), pixelBytes: 4 };
|
|
95
|
+
o += 2 + be16( o + 2 );
|
|
96
|
+
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
return null;
|
|
100
|
+
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
if ( type === 'image/webp' && bytes.length >= 30 ) {
|
|
104
|
+
|
|
105
|
+
const chunk = String.fromCharCode( bytes[ 12 ], bytes[ 13 ], bytes[ 14 ], bytes[ 15 ] );
|
|
106
|
+
if ( chunk === 'VP8X' ) return { width: le24( 24 ) + 1, height: le24( 27 ) + 1, pixelBytes: 4 };
|
|
107
|
+
if ( chunk === 'VP8 ' ) return { width: le16( 26 ) & 0x3FFF, height: le16( 28 ) & 0x3FFF, pixelBytes: 4 };
|
|
108
|
+
if ( chunk === 'VP8L' ) { const b = bytes[ 21 ] | ( bytes[ 22 ] << 8 ) | ( bytes[ 23 ] << 16 ) | ( bytes[ 24 ] << 24 ); return { width: ( b & 0x3FFF ) + 1, height: ( ( b >>> 14 ) & 0x3FFF ) + 1, pixelBytes: 4 }; }
|
|
109
|
+
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
return null;
|
|
113
|
+
|
|
114
|
+
}
|
|
115
|
+
|
|
68
116
|
/**
|
|
69
117
|
* Decodes a PNG, JPEG or WebP. The format is sniffed from the bytes; mimeType is only a fallback.
|
|
70
118
|
* @param {Uint8Array|ArrayBuffer} bytes
|
|
@@ -141,10 +189,13 @@ export async function encodePNGAsync( image ) {
|
|
|
141
189
|
* @param {Uint8Array} bytes
|
|
142
190
|
* @param {string} [mimeType]
|
|
143
191
|
* @param {( bytes: Uint8Array, mimeType?: string ) => Promise<object>|object} [decoder] overrides both
|
|
192
|
+
* @param {import('../limits.js').Budget} [budget] charged with the decoded size, from the header, before decoding
|
|
144
193
|
*/
|
|
145
|
-
export async function decodeImageAsync( bytes, mimeType, decoder = null ) {
|
|
194
|
+
export async function decodeImageAsync( bytes, mimeType, decoder = null, budget = null ) {
|
|
146
195
|
|
|
147
196
|
bytes = toBytes( bytes );
|
|
197
|
+
const size = budget && imageSize( bytes );
|
|
198
|
+
if ( size ) budget.decode( size.width * size.height * size.pixelBytes, 'image' );
|
|
148
199
|
if ( decoder ) return decoder( bytes, mimeType );
|
|
149
200
|
const type = imageMimeType( bytes ) || mimeType;
|
|
150
201
|
|
package/src/image/inflate.js
CHANGED
|
@@ -137,12 +137,12 @@ function corrupt( what ) {
|
|
|
137
137
|
}
|
|
138
138
|
|
|
139
139
|
// Inflates the raw DEFLATE stream starting at src[ pos ]. Returns the output and the byte offset just past the stream.
|
|
140
|
-
function inflateStream( src, pos, sizeHint ) {
|
|
140
|
+
function inflateStream( src, pos, sizeHint, maxSize ) {
|
|
141
141
|
|
|
142
142
|
const srcLen = src.length;
|
|
143
143
|
const overrun = srcLen + 8;
|
|
144
144
|
|
|
145
|
-
let out = new Uint8Array( sizeHint > 0 ? sizeHint : Math.max( 1 << 15, srcLen * 4 ) );
|
|
145
|
+
let out = new Uint8Array( Math.min( sizeHint > 0 ? sizeHint : Math.max( 1 << 15, srcLen * 4 ), maxSize ) );
|
|
146
146
|
let op = 0;
|
|
147
147
|
|
|
148
148
|
let bitBuf = 0;
|
|
@@ -154,7 +154,7 @@ function inflateStream( src, pos, sizeHint ) {
|
|
|
154
154
|
let clTable = null;
|
|
155
155
|
|
|
156
156
|
let final = 0;
|
|
157
|
-
const state = { src, overrun, pos, bitBuf, bitCnt, out, op };
|
|
157
|
+
const state = { src, overrun, pos, bitBuf, bitCnt, out, op, maxSize };
|
|
158
158
|
|
|
159
159
|
do {
|
|
160
160
|
|
|
@@ -188,7 +188,7 @@ function inflateStream( src, pos, sizeHint ) {
|
|
|
188
188
|
pos += 4;
|
|
189
189
|
if ( pos + len > srcLen ) throw corrupt( 'unexpected end' );
|
|
190
190
|
|
|
191
|
-
if ( op + len > out.length ) out = grow( out, op + len );
|
|
191
|
+
if ( op + len > out.length ) out = grow( out, op + len, maxSize );
|
|
192
192
|
out.set( src.subarray( pos, pos + len ), op );
|
|
193
193
|
op += len;
|
|
194
194
|
pos += len;
|
|
@@ -371,7 +371,7 @@ function decodeSymbols( state, litTable, litMask, distTable, distMask ) {
|
|
|
371
371
|
|
|
372
372
|
if ( op === outLen ) {
|
|
373
373
|
|
|
374
|
-
out = grow( out, op + 1 );
|
|
374
|
+
out = grow( out, op + 1, state.maxSize );
|
|
375
375
|
outLen = out.length;
|
|
376
376
|
|
|
377
377
|
}
|
|
@@ -435,7 +435,7 @@ function decodeSymbols( state, litTable, litMask, distTable, distMask ) {
|
|
|
435
435
|
|
|
436
436
|
if ( op + len > outLen ) {
|
|
437
437
|
|
|
438
|
-
out = grow( out, op + len );
|
|
438
|
+
out = grow( out, op + len, state.maxSize );
|
|
439
439
|
outLen = out.length;
|
|
440
440
|
|
|
441
441
|
}
|
|
@@ -464,9 +464,10 @@ function decodeSymbols( state, litTable, litMask, distTable, distMask ) {
|
|
|
464
464
|
|
|
465
465
|
}
|
|
466
466
|
|
|
467
|
-
function grow( out, need ) {
|
|
467
|
+
function grow( out, need, maxSize ) {
|
|
468
468
|
|
|
469
|
-
|
|
469
|
+
if ( need > maxSize ) throw corrupt( `more than the expected ${ maxSize } bytes` );
|
|
470
|
+
const next = new Uint8Array( Math.min( Math.max( need, out.length * 2 ), maxSize ) );
|
|
470
471
|
next.set( out );
|
|
471
472
|
return next;
|
|
472
473
|
|
|
@@ -482,11 +483,12 @@ function toBytes( data ) {
|
|
|
482
483
|
* Decompresses raw DEFLATE data.
|
|
483
484
|
* @param {Uint8Array} bytes
|
|
484
485
|
* @param {number} [sizeHint] expected output size, avoids reallocation when known
|
|
486
|
+
* @param {number} [maxSize] fails rather than produce more than this
|
|
485
487
|
* @returns {Uint8Array}
|
|
486
488
|
*/
|
|
487
|
-
export function inflate( bytes, sizeHint = 0 ) {
|
|
489
|
+
export function inflate( bytes, sizeHint = 0, maxSize = Infinity ) {
|
|
488
490
|
|
|
489
|
-
return inflateStream( toBytes( bytes ), 0, sizeHint ).data;
|
|
491
|
+
return inflateStream( toBytes( bytes ), 0, sizeHint, maxSize ).data;
|
|
490
492
|
|
|
491
493
|
}
|
|
492
494
|
|
|
@@ -496,7 +498,7 @@ export function inflate( bytes, sizeHint = 0 ) {
|
|
|
496
498
|
* @param {number} [sizeHint]
|
|
497
499
|
* @returns {Uint8Array}
|
|
498
500
|
*/
|
|
499
|
-
export function inflateZlib( bytes, sizeHint = 0 ) {
|
|
501
|
+
export function inflateZlib( bytes, sizeHint = 0, maxSize = Infinity ) {
|
|
500
502
|
|
|
501
503
|
bytes = toBytes( bytes );
|
|
502
504
|
if ( bytes.length < 2 ) throw corrupt( 'missing zlib header' );
|
|
@@ -506,7 +508,7 @@ export function inflateZlib( bytes, sizeHint = 0 ) {
|
|
|
506
508
|
if ( ( cmf & 15 ) !== 8 || ( cmf >> 4 ) > 7 || ( ( cmf << 8 ) | flg ) % 31 !== 0 ) throw corrupt( 'bad zlib header' );
|
|
507
509
|
if ( flg & 32 ) throw corrupt( 'zlib preset dictionary not supported' );
|
|
508
510
|
|
|
509
|
-
const { data, end } = inflateStream( bytes, 2, sizeHint );
|
|
511
|
+
const { data, end } = inflateStream( bytes, 2, sizeHint, maxSize );
|
|
510
512
|
|
|
511
513
|
if ( end + 4 <= bytes.length ) {
|
|
512
514
|
|
package/src/image/png.js
CHANGED
|
@@ -219,7 +219,7 @@ export function decodePNG( bytes ) {
|
|
|
219
219
|
|
|
220
220
|
}
|
|
221
221
|
|
|
222
|
-
const raw = inflateZlib( zdata, expected );
|
|
222
|
+
const raw = inflateZlib( zdata, expected, expected * 2 + 1024 ); // tolerates trailing data, not a decompression bomb
|
|
223
223
|
if ( raw.length < expected ) throw new Error( 'usd-gltf: PNG image data is truncated' );
|
|
224
224
|
|
|
225
225
|
const channels = colorType === 3 ? ( trns !== null ? 4 : 3 ) : samples + ( trns !== null ? 1 : 0 );
|