usd-gltf 0.3.0 → 0.3.1
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 +15 -0
- package/package.json +1 -1
- package/src/gltf/document.js +11 -9
- package/src/gltf/draco.js +231 -17
- package/src/index.d.ts +35 -0
- package/src/index.js +6 -5
package/README.md
CHANGED
|
@@ -52,6 +52,21 @@ import draco3d from 'draco3d';
|
|
|
52
52
|
const usdz = await glbToUSD( glb, { dracoDecoder: await draco3d.createDecoderModule() } );
|
|
53
53
|
```
|
|
54
54
|
|
|
55
|
+
The extension requires accessors to describe the decoded data exactly. When they don't, conversion rejects with a `DracoMismatchError`. Its `kind` says what disagrees: `'count'`, `'index-count'`, `'components'`, `'component-type'`, `'attribute'` or `'shared-accessor'`. Its `mesh`, `primitive`, `semantic`, `accessor`, `declared` and `decoded` fields say where and by how much.
|
|
56
|
+
|
|
57
|
+
Some files declare stale counts, for example from metadata written before compression changed the vertex layout. `recoverDracoCounts: true` converts those using the decoded counts and reports each one through `log`. It only does so when everything else that depends on those counts can be checked against the decoded data:
|
|
58
|
+
|
|
59
|
+
- the accessors are used only by Draco primitives decoding the same data;
|
|
60
|
+
- the primitive's uncompressed attributes and morph targets have the decoded vertex count;
|
|
61
|
+
- the accessors aren't sparse;
|
|
62
|
+
- the decoded positions match `POSITION`'s `min` and `max`.
|
|
63
|
+
|
|
64
|
+
Otherwise it still rejects, with `unrecoverable` saying why. Other mismatches, such as a different number of components, are never recovered. The decoder's output is sized by the decoded data, never by the declared counts, and counts against the limits below either way.
|
|
65
|
+
|
|
66
|
+
```js
|
|
67
|
+
const usdz = await glbToUSD( glb, { recoverDracoCounts: true, log: console.warn } );
|
|
68
|
+
```
|
|
69
|
+
|
|
55
70
|
## Limits
|
|
56
71
|
|
|
57
72
|
Input size alone doesn't bound how much memory a conversion needs: an accessor can declare millions of vertices with no data behind them, and compressed geometry and images expand when decoded. Both directions take opt-in limits:
|
package/package.json
CHANGED
package/src/gltf/document.js
CHANGED
|
@@ -2,7 +2,7 @@
|
|
|
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';
|
|
5
|
+
import { decodeDracoPrimitive, loadDracoDecoder, needsDraco, resolveDracoAccessors } from './draco.js';
|
|
6
6
|
import { Budget, checkSize } from '../limits.js';
|
|
7
7
|
|
|
8
8
|
const COMPONENT_ARRAYS = {
|
|
@@ -215,9 +215,11 @@ export class GLTFDocument {
|
|
|
215
215
|
/**
|
|
216
216
|
* Loads a glTF or GLB.
|
|
217
217
|
* @param {Uint8Array} bytes
|
|
218
|
-
* @param {{ resolver?: ( uri: string ) => Promise<Uint8Array>|Uint8Array, dracoDecoder?: object|Promise<object
|
|
218
|
+
* @param {{ resolver?: ( uri: string ) => Promise<Uint8Array>|Uint8Array, dracoDecoder?: object|Promise<object>, recoverDracoCounts?: boolean, log?: ( message: string ) => void }} [options]
|
|
219
|
+
* resolver loads external uris (already decoded from URI escaping); dracoDecoder is a Draco decoder module, loaded when needed if not given;
|
|
220
|
+
* recoverDracoCounts uses decoded Draco counts that disagree with their accessors when that can be checked, and logs it
|
|
219
221
|
*/
|
|
220
|
-
export async function readGLTF( bytes, { resolver = null, budget = new Budget(), dracoDecoder = null } = {} ) {
|
|
222
|
+
export async function readGLTF( bytes, { resolver = null, budget = new Budget(), dracoDecoder = null, recoverDracoCounts = false, log = () => {} } = {} ) {
|
|
221
223
|
|
|
222
224
|
let json, bin = null;
|
|
223
225
|
bytes = plainBytes( bytes );
|
|
@@ -268,19 +270,19 @@ export async function readGLTF( bytes, { resolver = null, budget = new Budget(),
|
|
|
268
270
|
} );
|
|
269
271
|
|
|
270
272
|
// Draco primitives without uncompressed fallback data
|
|
271
|
-
const draco = ( json.meshes || [] ).flatMap( ( mesh ) => mesh.primitives ).filter( ( p ) => needsDraco( json, p ) );
|
|
273
|
+
const draco = ( json.meshes || [] ).flatMap( ( mesh, m ) => ( mesh.primitives || [] ).map( ( primitive, index ) => ( { mesh: m, index, primitive } ) ) ).filter( ( p ) => needsDraco( json, p.primitive ) );
|
|
272
274
|
|
|
273
275
|
if ( draco.length ) {
|
|
274
276
|
|
|
275
277
|
const decoder = await ( dracoDecoder || loadDracoDecoder() );
|
|
276
|
-
|
|
277
|
-
for ( const primitive of draco ) {
|
|
278
|
+
const decoded = draco.map( ( where ) => {
|
|
278
279
|
|
|
279
280
|
budget.check();
|
|
280
|
-
const bytes = doc.bufferViewBytes( primitive.extensions.KHR_draco_mesh_compression.bufferView );
|
|
281
|
-
|
|
281
|
+
const bytes = doc.bufferViewBytes( where.primitive.extensions.KHR_draco_mesh_compression.bufferView );
|
|
282
|
+
return { ...where, ...decodeDracoPrimitive( decoder, bytes, json, where, budget ) };
|
|
282
283
|
|
|
283
|
-
}
|
|
284
|
+
} );
|
|
285
|
+
doc.decodedAccessors = resolveDracoAccessors( json, decoded, { recoverCounts: recoverDracoCounts, log } );
|
|
284
286
|
|
|
285
287
|
}
|
|
286
288
|
|
package/src/gltf/draco.js
CHANGED
|
@@ -12,6 +12,8 @@ const CDN_FILES = {
|
|
|
12
12
|
|
|
13
13
|
const DATA_TYPES = { 5120: 'DT_INT8', 5121: 'DT_UINT8', 5122: 'DT_INT16', 5123: 'DT_UINT16', 5125: 'DT_UINT32', 5126: 'DT_FLOAT32' };
|
|
14
14
|
const COMPONENT_ARRAYS = { 5120: Int8Array, 5121: Uint8Array, 5122: Int16Array, 5123: Uint16Array, 5125: Uint32Array, 5126: Float32Array };
|
|
15
|
+
const INDEX_ARRAYS = { 5121: Uint8Array, 5123: Uint16Array, 5125: Uint32Array };
|
|
16
|
+
const INDEX_MAX = { 5121: 0xFF, 5123: 0xFFFF, 5125: 0xFFFFFFFF };
|
|
15
17
|
const TYPE_SIZES = { SCALAR: 1, VEC2: 2, VEC3: 3, VEC4: 4, MAT2: 4, MAT3: 9, MAT4: 16 };
|
|
16
18
|
|
|
17
19
|
let defaultDecoder = null;
|
|
@@ -93,21 +95,65 @@ export function needsDraco( json, primitive ) {
|
|
|
93
95
|
}
|
|
94
96
|
|
|
95
97
|
/**
|
|
96
|
-
*
|
|
98
|
+
* Thrown when decoded Draco data doesn't match the accessors describing it, which the extension
|
|
99
|
+
* requires. kind says what disagrees:
|
|
100
|
+
* - 'count': an attribute decodes to a different number of elements than its accessor's count
|
|
101
|
+
* - 'index-count': the decoded triangles need a different number of indices than declared
|
|
102
|
+
* - 'components': an attribute decodes to a different number of components than its type has
|
|
103
|
+
* - 'component-type': the accessor's componentType can't hold the decoded values
|
|
104
|
+
* - 'attribute': the Draco data has no attribute with the extension's id
|
|
105
|
+
* - 'shared-accessor': one accessor is decoded more than once, to different data
|
|
106
|
+
* With recoverDracoCounts, count mismatches that can be checked are recovered instead; when one
|
|
107
|
+
* can't be, unrecoverable says why.
|
|
108
|
+
*/
|
|
109
|
+
export class DracoMismatchError extends Error {
|
|
110
|
+
|
|
111
|
+
constructor( { kind, mesh, primitive, semantic, accessor, declared, decoded, unrecoverable = null } ) {
|
|
112
|
+
|
|
113
|
+
const what = {
|
|
114
|
+
'count': () => `${ semantic } decodes to ${ decoded } elements, but accessor ${ accessor } declares ${ declared }`,
|
|
115
|
+
'index-count': () => `the triangles decode to ${ decoded } indices, but accessor ${ accessor } declares ${ declared }`,
|
|
116
|
+
'components': () => `${ semantic } decodes to ${ decoded } components, but accessor ${ accessor } is ${ declared }`,
|
|
117
|
+
'component-type': () => `${ semantic } decodes to values accessor ${ accessor } can't hold as componentType ${ declared }`,
|
|
118
|
+
'attribute': () => `there is no ${ semantic } attribute with id ${ declared }`,
|
|
119
|
+
'shared-accessor': () => `accessor ${ accessor } (${ semantic }) is decoded more than once, to different data`,
|
|
120
|
+
}[ kind ]();
|
|
121
|
+
const counts = kind === 'count' || kind === 'index-count';
|
|
122
|
+
const hint = unrecoverable ? `; not recovered, as ${ unrecoverable }` : counts ? '; the recoverDracoCounts option can use the decoded count' : '';
|
|
123
|
+
super( `usd-gltf: Draco mesh ${ mesh } primitive ${ primitive }: ${ what }${ hint }` );
|
|
124
|
+
this.name = 'DracoMismatchError';
|
|
125
|
+
this.code = 'USD_GLTF_DRACO_MISMATCH';
|
|
126
|
+
Object.assign( this, { kind, mesh, primitive, semantic, accessor, declared, decoded, unrecoverable } );
|
|
127
|
+
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
/**
|
|
133
|
+
* Decodes one Draco compressed primitive. Allocations are sized by the decoded data, never by
|
|
134
|
+
* the declared accessor counts, which resolveDracoAccessors checks afterwards.
|
|
97
135
|
* @param {object} draco decoder module
|
|
98
136
|
* @param {Uint8Array} bytes the extension's bufferView
|
|
99
137
|
* @param {object} json
|
|
100
|
-
* @param {object} primitive
|
|
138
|
+
* @param {{ mesh: number, index: number, primitive: object }} where the primitive and its place in the document
|
|
101
139
|
* @param {import('../limits.js').Budget} budget
|
|
102
|
-
* @returns {Map<number, ArrayLike<number>>} accessor index ->
|
|
140
|
+
* @returns {{ points: number, indexCount: number|null, data: Map<number, ArrayLike<number>> }} data is accessor index -> values, in the accessor's component type
|
|
103
141
|
*/
|
|
104
|
-
export function decodeDracoPrimitive( draco, bytes, json, primitive, budget ) {
|
|
142
|
+
export function decodeDracoPrimitive( draco, bytes, json, { mesh, index: primitiveIndex, primitive }, budget ) {
|
|
105
143
|
|
|
106
144
|
const ext = primitive.extensions.KHR_draco_mesh_compression;
|
|
107
145
|
const decoder = new draco.Decoder();
|
|
108
146
|
const array = new Int8Array( bytes.buffer, bytes.byteOffset, bytes.byteLength );
|
|
109
|
-
const
|
|
110
|
-
|
|
147
|
+
const mismatch = ( info ) => new DracoMismatchError( { mesh, primitive: primitiveIndex, ...info } );
|
|
148
|
+
const data = new Map();
|
|
149
|
+
let geometry = null, points = 0, indexCount = null;
|
|
150
|
+
|
|
151
|
+
const put = ( semantic, accessor, values ) => {
|
|
152
|
+
|
|
153
|
+
if ( data.has( accessor ) && ! sameValues( data.get( accessor ), values ) ) throw mismatch( { kind: 'shared-accessor', semantic, accessor } );
|
|
154
|
+
data.set( accessor, values );
|
|
155
|
+
|
|
156
|
+
};
|
|
111
157
|
|
|
112
158
|
try {
|
|
113
159
|
|
|
@@ -132,30 +178,36 @@ export function decodeDracoPrimitive( draco, bytes, json, primitive, budget ) {
|
|
|
132
178
|
|
|
133
179
|
if ( ! status.ok() || geometry.ptr === 0 ) throw new Error( `usd-gltf: Draco decoding failed: ${ status.error_msg() }` );
|
|
134
180
|
|
|
135
|
-
|
|
181
|
+
points = geometry.num_points();
|
|
136
182
|
|
|
137
|
-
for ( const [
|
|
183
|
+
for ( const [ semantic, id ] of Object.entries( ext.attributes ) ) {
|
|
138
184
|
|
|
139
|
-
const index = primitive.attributes[
|
|
185
|
+
const index = primitive.attributes[ semantic ];
|
|
140
186
|
const accessor = json.accessors[ index ];
|
|
141
187
|
if ( accessor === undefined ) continue;
|
|
142
188
|
const ArrayType = COMPONENT_ARRAYS[ accessor.componentType ];
|
|
143
189
|
const size = TYPE_SIZES[ accessor.type ];
|
|
190
|
+
if ( ! size ) throw new Error( `usd-gltf: accessor ${ index } has an invalid type` );
|
|
191
|
+
if ( ! ArrayType ) throw mismatch( { kind: 'component-type', semantic, accessor: index, declared: accessor.componentType } );
|
|
144
192
|
const attribute = decoder.GetAttributeByUniqueId( geometry, id );
|
|
145
|
-
if ( !
|
|
146
|
-
if (
|
|
147
|
-
|
|
193
|
+
if ( ! attribute || attribute.ptr === 0 ) throw mismatch( { kind: 'attribute', semantic, accessor: index, declared: id } );
|
|
194
|
+
if ( attribute.num_components() !== size ) throw mismatch( { kind: 'components', semantic, accessor: index, declared: accessor.type, decoded: attribute.num_components() } );
|
|
195
|
+
put( semantic, index, read( draco, points * size, ArrayType, budget, ( length, ptr ) => decoder.GetAttributeDataArrayForAllPoints( geometry, attribute, draco[ DATA_TYPES[ accessor.componentType ] ], length, ptr ) ) );
|
|
148
196
|
|
|
149
197
|
}
|
|
150
198
|
|
|
151
199
|
if ( primitive.indices !== undefined && type === draco.TRIANGULAR_MESH ) {
|
|
152
200
|
|
|
153
201
|
const accessor = json.accessors[ primitive.indices ];
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
const indices = read( draco,
|
|
157
|
-
|
|
158
|
-
|
|
202
|
+
if ( ! accessor ) throw new Error( `usd-gltf: accessor ${ primitive.indices } doesn't exist` );
|
|
203
|
+
indexCount = geometry.num_faces() * 3;
|
|
204
|
+
const indices = read( draco, indexCount, Uint32Array, budget, ( length, ptr ) => decoder.GetTrianglesUInt32Array( geometry, length, ptr ) );
|
|
205
|
+
let max = - 1;
|
|
206
|
+
for ( let i = 0; i < indices.length; i ++ ) if ( indices[ i ] > max ) max = indices[ i ];
|
|
207
|
+
if ( max >= points ) throw new Error( 'usd-gltf: Draco indices are out of range' );
|
|
208
|
+
const IndexType = INDEX_ARRAYS[ accessor.componentType ];
|
|
209
|
+
if ( ! IndexType || max > INDEX_MAX[ accessor.componentType ] ) throw mismatch( { kind: 'component-type', semantic: 'indices', accessor: primitive.indices, declared: accessor.componentType } );
|
|
210
|
+
put( 'indices', primitive.indices, IndexType === Uint32Array ? indices : IndexType.from( indices ) );
|
|
159
211
|
|
|
160
212
|
}
|
|
161
213
|
|
|
@@ -166,10 +218,172 @@ export function decodeDracoPrimitive( draco, bytes, json, primitive, budget ) {
|
|
|
166
218
|
|
|
167
219
|
}
|
|
168
220
|
|
|
221
|
+
return { points, indexCount, data };
|
|
222
|
+
|
|
223
|
+
}
|
|
224
|
+
|
|
225
|
+
/**
|
|
226
|
+
* Checks decoded primitives against their accessors and returns the accessor data. Count
|
|
227
|
+
* mismatches are errors, unless recoverCounts is set and everything else that depends on those
|
|
228
|
+
* counts agrees with the decoded data: then the accessors take the decoded counts, and log says so.
|
|
229
|
+
* @param {object} json changed only by recovered counts
|
|
230
|
+
* @param {Array<{ mesh: number, index: number, primitive: object, points: number, indexCount: number|null, data: Map<number, ArrayLike<number>> }>} decoded
|
|
231
|
+
* @param {{ recoverCounts?: boolean, log?: ( message: string ) => void }} [options]
|
|
232
|
+
* @returns {Map<number, ArrayLike<number>>}
|
|
233
|
+
*/
|
|
234
|
+
export function resolveDracoAccessors( json, decoded, { recoverCounts = false, log = () => {} } = {} ) {
|
|
235
|
+
|
|
236
|
+
const out = new Map();
|
|
237
|
+
|
|
238
|
+
for ( const d of decoded ) for ( const [ index, values ] of d.data ) {
|
|
239
|
+
|
|
240
|
+
if ( out.has( index ) && ! sameValues( out.get( index ), values ) ) throw new DracoMismatchError( { kind: 'shared-accessor', mesh: d.mesh, primitive: d.index, semantic: semanticOf( d.primitive, index ), accessor: index } );
|
|
241
|
+
out.set( index, values );
|
|
242
|
+
|
|
243
|
+
}
|
|
244
|
+
|
|
245
|
+
const uses = recoverCounts ? accessorUses( json ) : null;
|
|
246
|
+
const recovered = new Map(); // accessor -> decoded count
|
|
247
|
+
|
|
248
|
+
for ( const d of decoded ) {
|
|
249
|
+
|
|
250
|
+
const { primitive } = d;
|
|
251
|
+
const mismatches = [];
|
|
252
|
+
|
|
253
|
+
for ( const semantic of Object.keys( primitive.extensions.KHR_draco_mesh_compression.attributes ) ) {
|
|
254
|
+
|
|
255
|
+
const index = primitive.attributes[ semantic ];
|
|
256
|
+
const declared = json.accessors[ index ]?.count;
|
|
257
|
+
if ( index !== undefined && declared !== d.points ) mismatches.push( { kind: 'count', semantic, accessor: index, declared, decoded: d.points } );
|
|
258
|
+
|
|
259
|
+
}
|
|
260
|
+
|
|
261
|
+
const declaredIndices = d.indexCount !== null ? json.accessors[ primitive.indices ].count : null;
|
|
262
|
+
if ( declaredIndices !== d.indexCount ) mismatches.push( { kind: 'index-count', semantic: 'indices', accessor: primitive.indices, declared: declaredIndices, decoded: d.indexCount } );
|
|
263
|
+
|
|
264
|
+
if ( ! mismatches.length ) continue;
|
|
265
|
+
const fail = ( unrecoverable ) => new DracoMismatchError( { mesh: d.mesh, primitive: d.index, ...mismatches[ 0 ], unrecoverable } );
|
|
266
|
+
if ( ! recoverCounts ) throw fail( null );
|
|
267
|
+
const reason = unrecoverable( json, d, mismatches, uses, out );
|
|
268
|
+
if ( reason ) throw fail( reason );
|
|
269
|
+
|
|
270
|
+
for ( const m of mismatches ) recovered.set( m.accessor, m.decoded );
|
|
271
|
+
log( `Draco mesh ${ d.mesh } primitive ${ d.index }: using the decoded counts, ${ mismatches.map( ( m ) => `${ m.semantic } ${ m.decoded } where accessor ${ m.accessor } declares ${ m.declared }` ).join( ', ' ) }` );
|
|
272
|
+
|
|
273
|
+
}
|
|
274
|
+
|
|
275
|
+
for ( const [ index, count ] of recovered ) json.accessors[ index ].count = count;
|
|
169
276
|
return out;
|
|
170
277
|
|
|
171
278
|
}
|
|
172
279
|
|
|
280
|
+
// Why a primitive's mismatched counts can't be replaced by the decoded ones, or null when they can.
|
|
281
|
+
function unrecoverable( json, d, mismatches, uses, decodedData ) {
|
|
282
|
+
|
|
283
|
+
const { primitive, points } = d;
|
|
284
|
+
const compressed = primitive.extensions.KHR_draco_mesh_compression.attributes;
|
|
285
|
+
|
|
286
|
+
for ( const m of mismatches ) {
|
|
287
|
+
|
|
288
|
+
if ( json.accessors[ m.accessor ].sparse ) return `accessor ${ m.accessor } is sparse`;
|
|
289
|
+
// other Draco primitives decoding the same data are checked on their own
|
|
290
|
+
const other = uses.get( m.accessor ).find( ( use ) => ! use.draco );
|
|
291
|
+
if ( other ) return `accessor ${ m.accessor } is also used by ${ other.what }`;
|
|
292
|
+
|
|
293
|
+
}
|
|
294
|
+
|
|
295
|
+
for ( const [ semantic, index ] of Object.entries( primitive.attributes ) ) {
|
|
296
|
+
|
|
297
|
+
const count = json.accessors[ index ]?.count;
|
|
298
|
+
if ( ! ( semantic in compressed ) && count !== points ) return `${ semantic } isn't Draco compressed and has ${ count } elements, not ${ points }`;
|
|
299
|
+
|
|
300
|
+
}
|
|
301
|
+
|
|
302
|
+
for ( const [ t, target ] of ( primitive.targets || [] ).entries() ) {
|
|
303
|
+
|
|
304
|
+
for ( const [ semantic, index ] of Object.entries( target ) ) {
|
|
305
|
+
|
|
306
|
+
const count = json.accessors[ index ]?.count;
|
|
307
|
+
if ( count !== points ) return `morph target ${ t } ${ semantic } has ${ count } elements, not ${ points }`;
|
|
308
|
+
|
|
309
|
+
}
|
|
310
|
+
|
|
311
|
+
}
|
|
312
|
+
|
|
313
|
+
const position = primitive.attributes.POSITION;
|
|
314
|
+
|
|
315
|
+
if ( mismatches.some( ( m ) => m.accessor === position ) ) {
|
|
316
|
+
|
|
317
|
+
// the bounds must still describe the decoded positions, allowing for 8 bits of Draco quantization over the largest extent
|
|
318
|
+
const { min, max } = json.accessors[ position ];
|
|
319
|
+
if ( ! ( min?.length === 3 && max?.length === 3 ) ) return `POSITION accessor ${ position } has no min and max to check the decoded positions against`;
|
|
320
|
+
const values = decodedData.get( position );
|
|
321
|
+
const lo = [ Infinity, Infinity, Infinity ], hi = [ - Infinity, - Infinity, - Infinity ];
|
|
322
|
+
|
|
323
|
+
for ( let i = 0; i < values.length; i += 3 ) for ( let c = 0; c < 3; c ++ ) {
|
|
324
|
+
|
|
325
|
+
lo[ c ] = Math.min( lo[ c ], values[ i + c ] );
|
|
326
|
+
hi[ c ] = Math.max( hi[ c ], values[ i + c ] );
|
|
327
|
+
|
|
328
|
+
}
|
|
329
|
+
|
|
330
|
+
const tolerance = Math.max( max[ 0 ] - min[ 0 ], max[ 1 ] - min[ 1 ], max[ 2 ] - min[ 2 ] ) / 256 + 1e-6;
|
|
331
|
+
for ( let c = 0; c < 3; c ++ ) if ( ! ( Math.abs( lo[ c ] - min[ c ] ) <= tolerance && Math.abs( hi[ c ] - max[ c ] ) <= tolerance ) ) return `the decoded positions don't match POSITION accessor ${ position }'s min and max`;
|
|
332
|
+
|
|
333
|
+
}
|
|
334
|
+
|
|
335
|
+
return null;
|
|
336
|
+
|
|
337
|
+
}
|
|
338
|
+
|
|
339
|
+
// Every reference to each accessor, marking those a Draco extension provides.
|
|
340
|
+
function accessorUses( json ) {
|
|
341
|
+
|
|
342
|
+
const uses = new Map();
|
|
343
|
+
const add = ( accessor, what, draco ) => { if ( accessor !== undefined ) uses.set( accessor, [ ...( uses.get( accessor ) || [] ), { what, draco } ] ); };
|
|
344
|
+
|
|
345
|
+
( json.meshes || [] ).forEach( ( mesh, m ) => ( mesh.primitives || [] ).forEach( ( primitive, p ) => {
|
|
346
|
+
|
|
347
|
+
const draco = primitive.extensions?.KHR_draco_mesh_compression;
|
|
348
|
+
const where = `mesh ${ m } primitive ${ p }`;
|
|
349
|
+
for ( const [ semantic, a ] of Object.entries( primitive.attributes || {} ) ) add( a, `${ where } ${ semantic }`, !! draco && semantic in draco.attributes );
|
|
350
|
+
add( primitive.indices, `${ where } indices`, !! draco );
|
|
351
|
+
( primitive.targets || [] ).forEach( ( target, t ) => { for ( const [ semantic, a ] of Object.entries( target ) ) add( a, `${ where } morph target ${ t } ${ semantic }`, false ); } );
|
|
352
|
+
|
|
353
|
+
} ) );
|
|
354
|
+
|
|
355
|
+
( json.skins || [] ).forEach( ( skin, s ) => add( skin.inverseBindMatrices, `skin ${ s }`, false ) );
|
|
356
|
+
( json.animations || [] ).forEach( ( animation, a ) => ( animation.samplers || [] ).forEach( ( sampler, s ) => {
|
|
357
|
+
|
|
358
|
+
add( sampler.input, `animation ${ a } sampler ${ s }`, false );
|
|
359
|
+
add( sampler.output, `animation ${ a } sampler ${ s }`, false );
|
|
360
|
+
|
|
361
|
+
} ) );
|
|
362
|
+
( json.nodes || [] ).forEach( ( node, n ) => {
|
|
363
|
+
|
|
364
|
+
for ( const a of Object.values( node.extensions?.EXT_mesh_gpu_instancing?.attributes || {} ) ) add( a, `node ${ n } instancing`, false );
|
|
365
|
+
|
|
366
|
+
} );
|
|
367
|
+
|
|
368
|
+
return uses;
|
|
369
|
+
|
|
370
|
+
}
|
|
371
|
+
|
|
372
|
+
function semanticOf( primitive, accessor ) {
|
|
373
|
+
|
|
374
|
+
if ( primitive.indices === accessor ) return 'indices';
|
|
375
|
+
return Object.keys( primitive.attributes ).find( ( s ) => primitive.attributes[ s ] === accessor );
|
|
376
|
+
|
|
377
|
+
}
|
|
378
|
+
|
|
379
|
+
function sameValues( a, b ) {
|
|
380
|
+
|
|
381
|
+
if ( a.length !== b.length || a.constructor !== b.constructor ) return false;
|
|
382
|
+
for ( let i = 0; i < a.length; i ++ ) if ( ! Object.is( a[ i ], b[ i ] ) ) return false;
|
|
383
|
+
return true;
|
|
384
|
+
|
|
385
|
+
}
|
|
386
|
+
|
|
173
387
|
// Has the decoder fill a buffer on its heap, then copies it out.
|
|
174
388
|
function read( draco, count, ArrayType, budget, fill ) {
|
|
175
389
|
|
package/src/index.d.ts
CHANGED
|
@@ -34,6 +34,34 @@ export class ConversionLimitError extends Error {
|
|
|
34
34
|
attempted: number;
|
|
35
35
|
}
|
|
36
36
|
|
|
37
|
+
/**
|
|
38
|
+
* Thrown when decoded Draco data doesn't match the accessors describing it. With recoverDracoCounts,
|
|
39
|
+
* count mismatches that can't be checked still throw, with unrecoverable saying why.
|
|
40
|
+
*/
|
|
41
|
+
export class DracoMismatchError extends Error {
|
|
42
|
+
name: 'DracoMismatchError';
|
|
43
|
+
code: 'USD_GLTF_DRACO_MISMATCH';
|
|
44
|
+
/**
|
|
45
|
+
* count / index-count: a different number of elements or indices than declared;
|
|
46
|
+
* components: a different number of components than the accessor type has;
|
|
47
|
+
* component-type: values the accessor's componentType can't hold;
|
|
48
|
+
* attribute: no Draco attribute with the extension's id;
|
|
49
|
+
* shared-accessor: one accessor decoded more than once to different data.
|
|
50
|
+
*/
|
|
51
|
+
kind: 'count' | 'index-count' | 'components' | 'component-type' | 'attribute' | 'shared-accessor';
|
|
52
|
+
mesh: number;
|
|
53
|
+
primitive: number;
|
|
54
|
+
/** The attribute semantic, e.g. 'POSITION', or 'indices'. */
|
|
55
|
+
semantic: string;
|
|
56
|
+
accessor: number;
|
|
57
|
+
/** What the accessor declares: its count, type, componentType, or the Draco attribute id. */
|
|
58
|
+
declared?: number | string;
|
|
59
|
+
/** What the Draco data has: a count of elements, indices or components. */
|
|
60
|
+
decoded?: number;
|
|
61
|
+
/** With recoverDracoCounts, why the decoded counts couldn't be used. */
|
|
62
|
+
unrecoverable: string | null;
|
|
63
|
+
}
|
|
64
|
+
|
|
37
65
|
export interface USDToGLBOptions extends ConversionLimits {
|
|
38
66
|
/** Name of the input file, used to resolve relative paths. */
|
|
39
67
|
identifier?: string;
|
|
@@ -58,7 +86,14 @@ export interface GLBToUSDOptions extends ConversionLimits {
|
|
|
58
86
|
* promise of one. Without it, Draco files load draco3d in Node and Google's CDN build in browsers.
|
|
59
87
|
*/
|
|
60
88
|
dracoDecoder?: object | Promise<object>;
|
|
89
|
+
/**
|
|
90
|
+
* Use decoded Draco counts that disagree with their accessors' counts, which the extension
|
|
91
|
+
* doesn't allow, when everything else depending on them can be checked against the decoded data.
|
|
92
|
+
* Each recovery is reported through log. Default false: such files throw a DracoMismatchError.
|
|
93
|
+
*/
|
|
94
|
+
recoverDracoCounts?: boolean;
|
|
61
95
|
imageDecoder?: ImageDecoder;
|
|
96
|
+
/** Receives non fatal problems, such as textures that could not be read or recovered Draco counts. */
|
|
62
97
|
log?: ( message: string ) => void;
|
|
63
98
|
}
|
|
64
99
|
|
package/src/index.js
CHANGED
|
@@ -15,6 +15,7 @@ export { writeUSDA } from './usd/usda/writer.js';
|
|
|
15
15
|
export { readUSDC } from './usd/usdc/reader.js';
|
|
16
16
|
export { writeUSDC } from './usd/usdc/writer.js';
|
|
17
17
|
export { ConversionLimitError } from './limits.js';
|
|
18
|
+
export { DracoMismatchError } from './gltf/draco.js';
|
|
18
19
|
|
|
19
20
|
function toBytes( data ) {
|
|
20
21
|
|
|
@@ -50,13 +51,13 @@ export async function usdToGLB( data, options = {} ) {
|
|
|
50
51
|
/**
|
|
51
52
|
* Converts GLB (or glTF JSON with embedded or resolvable buffers) to USD.
|
|
52
53
|
* @param {ArrayBuffer|Uint8Array} data
|
|
53
|
-
* @param {{ format?: 'usda'|'usdc'|'usdz', resolver?: ( uri: string ) => Promise<Uint8Array>, dracoDecoder?: object, maxDecodedBytes?: number, maxOutputBytes?: number, signal?: AbortSignal }} [options]
|
|
54
|
+
* @param {{ format?: 'usda'|'usdc'|'usdz', resolver?: ( uri: string ) => Promise<Uint8Array>, dracoDecoder?: object, recoverDracoCounts?: boolean, log?: ( message: string ) => void, maxDecodedBytes?: number, maxOutputBytes?: number, signal?: AbortSignal }} [options]
|
|
54
55
|
* @returns {Promise<Uint8Array>}
|
|
55
56
|
*/
|
|
56
|
-
export async function glbToUSD( data, { format = 'usdz', resolver = null, log, imageDecoder, dracoDecoder, ...limits } = {} ) {
|
|
57
|
+
export async function glbToUSD( data, { format = 'usdz', resolver = null, log, imageDecoder, dracoDecoder, recoverDracoCounts, ...limits } = {} ) {
|
|
57
58
|
|
|
58
59
|
const budget = budgetFor( limits );
|
|
59
|
-
const doc = await readGLTF( toBytes( data ), { resolver, budget, dracoDecoder } );
|
|
60
|
+
const doc = await readGLTF( toBytes( data ), { resolver, budget, dracoDecoder, recoverDracoCounts, log } );
|
|
60
61
|
const { layer, files } = await new GLTFToUSD( doc, { log, imageDecoder } ).convert();
|
|
61
62
|
let out;
|
|
62
63
|
|
|
@@ -73,10 +74,10 @@ export async function glbToUSD( data, { format = 'usdz', resolver = null, log, i
|
|
|
73
74
|
* For usda / usdc output the textures are separate files; this returns them alongside the layer.
|
|
74
75
|
* @returns {Promise<{ layer: Uint8Array, files: Array<{ name: string, data: Uint8Array }> }>}
|
|
75
76
|
*/
|
|
76
|
-
export async function glbToUSDFiles( data, { format = 'usda', resolver = null, log, imageDecoder, dracoDecoder, ...limits } = {} ) {
|
|
77
|
+
export async function glbToUSDFiles( data, { format = 'usda', resolver = null, log, imageDecoder, dracoDecoder, recoverDracoCounts, ...limits } = {} ) {
|
|
77
78
|
|
|
78
79
|
const budget = budgetFor( limits );
|
|
79
|
-
const doc = await readGLTF( toBytes( data ), { resolver, budget, dracoDecoder } );
|
|
80
|
+
const doc = await readGLTF( toBytes( data ), { resolver, budget, dracoDecoder, recoverDracoCounts, log } );
|
|
80
81
|
const { layer, files } = await new GLTFToUSD( doc, { log, imageDecoder } ).convert();
|
|
81
82
|
const bytes = format === 'usdc' ? writeUSDC( layer, { budget } ) : new TextEncoder().encode( writeUSDA( layer, { budget } ) );
|
|
82
83
|
budget.output( files.reduce( ( n, f ) => n + f.data.byteLength, bytes.byteLength ), 'usd files' );
|