usd-gltf 0.2.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 +30 -2
- package/package.json +12 -2
- package/src/gltf/document.js +28 -25
- package/src/gltf/draco.js +405 -0
- package/src/index.d.ts +40 -0
- package/src/index.js +6 -5
package/README.md
CHANGED
|
@@ -37,7 +37,35 @@ USD to glTF composes the stage first (sublayers, references, payloads, inherits,
|
|
|
37
37
|
|
|
38
38
|
glTF to USD writes `UsdPreviewSurface` materials, UsdSkel skins and blend shapes, time sampled animation, variant sets for material variants and multiple animations, cameras and lights. glTF features that `UsdPreviewSurface` can't express (transmission, sheen, iridescence, ...) are kept in the material's `customData`, so converting back to glTF is lossless. Primitives of a mesh that share their vertex data become one USD Mesh with a GeomSubset per primitive, and each Mesh keeps only the vertices it uses, so vertex data is never repeated per primitive.
|
|
39
39
|
|
|
40
|
-
glTF compression: meshopt (`EXT_meshopt_compression`, `KHR_meshopt_compression`)
|
|
40
|
+
glTF compression: meshopt (`EXT_meshopt_compression`, `KHR_meshopt_compression`), `KHR_mesh_quantization` and Draco (`KHR_draco_mesh_compression`, see below) are decoded. KTX2 textures (`KHR_texture_basisu`) are not: an asset that requires them fails with an error naming the extension, and when they are optional the PNG or JPEG fallback is used.
|
|
41
|
+
|
|
42
|
+
### Draco
|
|
43
|
+
|
|
44
|
+
usd-gltf doesn't bundle a Draco decoder; it loads Google's only when a file is Draco compressed, and uses the uncompressed fallback without loading anything when a file has one.
|
|
45
|
+
|
|
46
|
+
- **Node:** install [`draco3d`](https://www.npmjs.com/package/draco3d) alongside, an optional peer dependency, and Draco files just work: `npm install draco3d`.
|
|
47
|
+
- **Browsers:** the decoder (about 250 KB) is fetched from Google's CDN, `www.gstatic.com/draco/versioned/decoders/1.5.7/`, pinned to that version and checked against its SHA-256 before it runs. This needs a secure context, and a Content Security Policy that allows the fetch, `unsafe-eval` and WebAssembly.
|
|
48
|
+
- **Your own:** pass a decoder module as `dracoDecoder` to use a self hosted copy, or where the CDN isn't allowed:
|
|
49
|
+
|
|
50
|
+
```js
|
|
51
|
+
import draco3d from 'draco3d';
|
|
52
|
+
const usdz = await glbToUSD( glb, { dracoDecoder: await draco3d.createDecoderModule() } );
|
|
53
|
+
```
|
|
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
|
+
```
|
|
41
69
|
|
|
42
70
|
## Limits
|
|
43
71
|
|
|
@@ -62,7 +90,7 @@ Going over a limit rejects with a `ConversionLimitError` whose `stage`, `resourc
|
|
|
62
90
|
Current status:
|
|
63
91
|
|
|
64
92
|
- All 18 USD files in Apple's AR Quick Look gallery convert to GLB. Per mesh triangle counts and world space bounds at the start, middle and end of each animation match OpenUSD with skinning baked, and the Khronos glTF Validator reports no errors. (The gallery's three `.reality` files are RealityKit's format, not USD.)
|
|
65
|
-
- All 150 Khronos glTF sample models convert to USDZ that OpenUSD's validators accept with no errors or warnings, with geometry matching the source at several animation times, and convert back to glTF with identical materials, material variants and animations. Each model is checked as `.gltf`, and also as `.glb` (120 models)
|
|
93
|
+
- All 150 Khronos glTF sample models convert to USDZ that OpenUSD's validators accept with no errors or warnings, with geometry matching the source at several animation times, and convert back to glTF with identical materials, material variants and animations. Each model is checked as `.gltf`, and also as `.glb` (120 models), embedded `.gltf` (32 models) and Draco compressed (16 models) where the sample set has those versions: 318 conversions in all.
|
|
66
94
|
|
|
67
95
|
The acceptance corpora are every model in [Apple's AR Quick Look gallery](https://developer.apple.com/augmented-reality/quick-look/) and every model in the [Khronos glTF Sample Assets](https://github.com/KhronosGroup/glTF-Sample-Assets). Conversions are checked against Pixar's OpenUSD (`usd-core`) and the Khronos glTF Validator: per mesh triangle counts and world space bounds at several animation times, with skinning baked by OpenUSD, must match.
|
|
68
96
|
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "usd-gltf",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.3.1",
|
|
4
4
|
"description": "Convert between USD (usda, usdc, usdz) and glTF / GLB, pure JavaScript with no dependencies",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"main": "src/index.js",
|
|
@@ -35,7 +35,8 @@
|
|
|
35
35
|
"gltf",
|
|
36
36
|
"glb",
|
|
37
37
|
"converter",
|
|
38
|
-
"3d"
|
|
38
|
+
"3d",
|
|
39
|
+
"draco"
|
|
39
40
|
],
|
|
40
41
|
"author": "Richard Monette",
|
|
41
42
|
"license": "MIT",
|
|
@@ -47,7 +48,16 @@
|
|
|
47
48
|
"url": "https://github.com/richardmonette/usd-gltf/issues"
|
|
48
49
|
},
|
|
49
50
|
"homepage": "https://github.com/richardmonette/usd-gltf#readme",
|
|
51
|
+
"peerDependencies": {
|
|
52
|
+
"draco3d": "^1.5.7"
|
|
53
|
+
},
|
|
54
|
+
"peerDependenciesMeta": {
|
|
55
|
+
"draco3d": {
|
|
56
|
+
"optional": true
|
|
57
|
+
}
|
|
58
|
+
},
|
|
50
59
|
"devDependencies": {
|
|
60
|
+
"draco3d": "^1.5.7",
|
|
51
61
|
"gltf-validator": "^2.0.0-dev.3.10",
|
|
52
62
|
"meshoptimizer": "^1.3.0"
|
|
53
63
|
},
|
package/src/gltf/document.js
CHANGED
|
@@ -2,6 +2,7 @@
|
|
|
2
2
|
|
|
3
3
|
import { readGLB, writeGLB } from './glb.js';
|
|
4
4
|
import { decodeMeshoptView } from './meshopt.js';
|
|
5
|
+
import { decodeDracoPrimitive, loadDracoDecoder, needsDraco, resolveDracoAccessors } from './draco.js';
|
|
5
6
|
import { Budget, checkSize } from '../limits.js';
|
|
6
7
|
|
|
7
8
|
const COMPONENT_ARRAYS = {
|
|
@@ -18,13 +19,13 @@ export const TYPE_SIZES = { SCALAR: 1, VEC2: 2, VEC3: 3, VEC4: 4, MAT2: 4, MAT3:
|
|
|
18
19
|
// Extensions an asset may require. Material extensions without a UsdPreviewSurface equivalent are
|
|
19
20
|
// approximated and kept on the USD material for the way back, so every KHR_materials_* is accepted.
|
|
20
21
|
const SUPPORTED_EXTENSIONS = new Set( [
|
|
21
|
-
'EXT_mesh_gpu_instancing', 'EXT_meshopt_compression', 'KHR_meshopt_compression', 'KHR_mesh_quantization',
|
|
22
|
+
'EXT_mesh_gpu_instancing', 'EXT_meshopt_compression', 'KHR_meshopt_compression', 'KHR_mesh_quantization', 'KHR_draco_mesh_compression',
|
|
22
23
|
'EXT_texture_webp', 'EXT_texture_avif', 'KHR_texture_transform', 'KHR_animation_pointer', 'KHR_lights_punctual',
|
|
23
24
|
'KHR_node_visibility', 'KHR_xmp_json_ld',
|
|
24
25
|
] );
|
|
25
26
|
|
|
26
27
|
/**
|
|
27
|
-
* The asset's required extensions that can't be converted (e.g.
|
|
28
|
+
* The asset's required extensions that can't be converted (e.g. KHR_texture_basisu).
|
|
28
29
|
* @param {object} json
|
|
29
30
|
* @returns {string[]}
|
|
30
31
|
*/
|
|
@@ -34,24 +35,6 @@ export function unsupportedExtensions( json ) {
|
|
|
34
35
|
|
|
35
36
|
}
|
|
36
37
|
|
|
37
|
-
// Optional Draco needs uncompressed fallback data: without a bufferView or sparse values the
|
|
38
|
-
// accessors would read as zeros.
|
|
39
|
-
function checkDracoFallbacks( json ) {
|
|
40
|
-
|
|
41
|
-
( json.meshes || [] ).forEach( ( mesh, m ) => mesh.primitives.forEach( ( primitive, p ) => {
|
|
42
|
-
|
|
43
|
-
if ( ! primitive.extensions?.KHR_draco_mesh_compression ) return;
|
|
44
|
-
const used = [ ...Object.values( primitive.attributes ), primitive.indices ].filter( ( a ) => a !== undefined );
|
|
45
|
-
if ( used.some( ( a ) => json.accessors[ a ].bufferView === undefined && ! json.accessors[ a ].sparse ) ) {
|
|
46
|
-
|
|
47
|
-
throw new Error( `usd-gltf: mesh ${ m } primitive ${ p } is Draco compressed with no uncompressed fallback; KHR_draco_mesh_compression is not supported` );
|
|
48
|
-
|
|
49
|
-
}
|
|
50
|
-
|
|
51
|
-
} ) );
|
|
52
|
-
|
|
53
|
-
}
|
|
54
|
-
|
|
55
38
|
function decodeDataURI( uri ) {
|
|
56
39
|
|
|
57
40
|
const comma = uri.indexOf( ',' );
|
|
@@ -84,6 +67,7 @@ export class GLTFDocument {
|
|
|
84
67
|
this.budget = budget;
|
|
85
68
|
this.accessorCache = new Map();
|
|
86
69
|
this.decodedViews = new Map(); // bufferView index -> bytes, for compressed views
|
|
70
|
+
this.decodedAccessors = new Map(); // accessor index -> data, for Draco compressed primitives
|
|
87
71
|
|
|
88
72
|
}
|
|
89
73
|
|
|
@@ -139,8 +123,9 @@ export class GLTFDocument {
|
|
|
139
123
|
|
|
140
124
|
}
|
|
141
125
|
|
|
142
|
-
this.
|
|
143
|
-
|
|
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 );
|
|
144
129
|
|
|
145
130
|
if ( read ) {
|
|
146
131
|
|
|
@@ -230,9 +215,11 @@ export class GLTFDocument {
|
|
|
230
215
|
/**
|
|
231
216
|
* Loads a glTF or GLB.
|
|
232
217
|
* @param {Uint8Array} bytes
|
|
233
|
-
* @param {{ resolver?: ( uri: string ) => Promise<Uint8Array>|Uint8Array
|
|
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
|
|
234
221
|
*/
|
|
235
|
-
export async function readGLTF( bytes, { resolver = null, budget = new Budget() } = {} ) {
|
|
222
|
+
export async function readGLTF( bytes, { resolver = null, budget = new Budget(), dracoDecoder = null, recoverDracoCounts = false, log = () => {} } = {} ) {
|
|
236
223
|
|
|
237
224
|
let json, bin = null;
|
|
238
225
|
bytes = plainBytes( bytes );
|
|
@@ -242,7 +229,6 @@ export async function readGLTF( bytes, { resolver = null, budget = new Budget()
|
|
|
242
229
|
|
|
243
230
|
const unsupported = unsupportedExtensions( json );
|
|
244
231
|
if ( unsupported.length ) throw new Error( `usd-gltf: required extension${ unsupported.length > 1 ? 's' : '' } ${ unsupported.join( ', ' ) } not supported` );
|
|
245
|
-
checkDracoFallbacks( json );
|
|
246
232
|
|
|
247
233
|
async function load( uri ) {
|
|
248
234
|
|
|
@@ -283,6 +269,23 @@ export async function readGLTF( bytes, { resolver = null, budget = new Budget()
|
|
|
283
269
|
|
|
284
270
|
} );
|
|
285
271
|
|
|
272
|
+
// Draco primitives without uncompressed fallback data
|
|
273
|
+
const draco = ( json.meshes || [] ).flatMap( ( mesh, m ) => ( mesh.primitives || [] ).map( ( primitive, index ) => ( { mesh: m, index, primitive } ) ) ).filter( ( p ) => needsDraco( json, p.primitive ) );
|
|
274
|
+
|
|
275
|
+
if ( draco.length ) {
|
|
276
|
+
|
|
277
|
+
const decoder = await ( dracoDecoder || loadDracoDecoder() );
|
|
278
|
+
const decoded = draco.map( ( where ) => {
|
|
279
|
+
|
|
280
|
+
budget.check();
|
|
281
|
+
const bytes = doc.bufferViewBytes( where.primitive.extensions.KHR_draco_mesh_compression.bufferView );
|
|
282
|
+
return { ...where, ...decodeDracoPrimitive( decoder, bytes, json, where, budget ) };
|
|
283
|
+
|
|
284
|
+
} );
|
|
285
|
+
doc.decodedAccessors = resolveDracoAccessors( json, decoded, { recoverCounts: recoverDracoCounts, log } );
|
|
286
|
+
|
|
287
|
+
}
|
|
288
|
+
|
|
286
289
|
doc.images = await Promise.all( ( json.images || [] ).map( async ( image ) => {
|
|
287
290
|
|
|
288
291
|
if ( image.bufferView !== undefined ) return { bytes: doc.bufferViewBytes( image.bufferView ), mimeType: image.mimeType, name: image.name, uri: null };
|
|
@@ -0,0 +1,405 @@
|
|
|
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 INDEX_ARRAYS = { 5121: Uint8Array, 5123: Uint16Array, 5125: Uint32Array };
|
|
16
|
+
const INDEX_MAX = { 5121: 0xFF, 5123: 0xFFFF, 5125: 0xFFFFFFFF };
|
|
17
|
+
const TYPE_SIZES = { SCALAR: 1, VEC2: 2, VEC3: 3, VEC4: 4, MAT2: 4, MAT3: 9, MAT4: 16 };
|
|
18
|
+
|
|
19
|
+
let defaultDecoder = null;
|
|
20
|
+
|
|
21
|
+
/**
|
|
22
|
+
* The default Draco decoder module: draco3d in Node, Google's CDN in browsers. Loaded once.
|
|
23
|
+
* @returns {Promise<object>}
|
|
24
|
+
*/
|
|
25
|
+
export function loadDracoDecoder() {
|
|
26
|
+
|
|
27
|
+
defaultDecoder ||= ( isNode() ? loadNode() : loadDracoFromCDN() ).catch( ( e ) => {
|
|
28
|
+
|
|
29
|
+
defaultDecoder = null; // let a later conversion try again
|
|
30
|
+
throw e;
|
|
31
|
+
|
|
32
|
+
} );
|
|
33
|
+
|
|
34
|
+
return defaultDecoder;
|
|
35
|
+
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
function isNode() {
|
|
39
|
+
|
|
40
|
+
return typeof process !== 'undefined' && !! process.versions?.node;
|
|
41
|
+
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
async function loadNode() {
|
|
45
|
+
|
|
46
|
+
const name = 'draco3d'; // not a literal, so bundlers don't try to include it
|
|
47
|
+
let draco3d;
|
|
48
|
+
|
|
49
|
+
try {
|
|
50
|
+
|
|
51
|
+
draco3d = await import( /* webpackIgnore: true */ /* @vite-ignore */ name );
|
|
52
|
+
|
|
53
|
+
} catch {
|
|
54
|
+
|
|
55
|
+
throw new Error( 'usd-gltf: this glTF is Draco compressed; install the decoder with npm install draco3d, or pass a dracoDecoder' );
|
|
56
|
+
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
return ( draco3d.default ?? draco3d ).createDecoderModule();
|
|
60
|
+
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
// Exported for tests; loadDracoDecoder picks the source.
|
|
64
|
+
export async function loadDracoFromCDN() {
|
|
65
|
+
|
|
66
|
+
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' );
|
|
67
|
+
const [ wrapper, wasm ] = await Promise.all( Object.keys( CDN_FILES ).map( fetchVerified ) );
|
|
68
|
+
// the wrapper is a classic script defining DracoDecoderModule, so it can't be imported as a module
|
|
69
|
+
const factory = new Function( new TextDecoder().decode( wrapper ) + '\nreturn DracoDecoderModule;' )();
|
|
70
|
+
return factory( { wasmBinary: wasm.buffer } );
|
|
71
|
+
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
async function fetchVerified( name ) {
|
|
75
|
+
|
|
76
|
+
const response = await fetch( CDN + name );
|
|
77
|
+
if ( ! response.ok ) throw new Error( `usd-gltf: could not load the Draco decoder (${ response.status } for ${ CDN + name })` );
|
|
78
|
+
const bytes = new Uint8Array( await response.arrayBuffer() );
|
|
79
|
+
const digest = new Uint8Array( await crypto.subtle.digest( 'SHA-256', bytes ) );
|
|
80
|
+
if ( btoa( String.fromCharCode( ...digest ) ) !== CDN_FILES[ name ] ) throw new Error( `usd-gltf: ${ CDN + name } doesn't match its expected hash` );
|
|
81
|
+
return bytes;
|
|
82
|
+
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
/**
|
|
86
|
+
* Whether a primitive's data is only in its Draco extension: Draco is decoded only when the
|
|
87
|
+
* accessors have no uncompressed fallback.
|
|
88
|
+
*/
|
|
89
|
+
export function needsDraco( json, primitive ) {
|
|
90
|
+
|
|
91
|
+
if ( ! primitive.extensions?.KHR_draco_mesh_compression ) return false;
|
|
92
|
+
const used = [ ...Object.values( primitive.attributes ), primitive.indices ].filter( ( a ) => a !== undefined );
|
|
93
|
+
return used.some( ( a ) => json.accessors[ a ]?.bufferView === undefined && ! json.accessors[ a ]?.sparse );
|
|
94
|
+
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
/**
|
|
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.
|
|
135
|
+
* @param {object} draco decoder module
|
|
136
|
+
* @param {Uint8Array} bytes the extension's bufferView
|
|
137
|
+
* @param {object} json
|
|
138
|
+
* @param {{ mesh: number, index: number, primitive: object }} where the primitive and its place in the document
|
|
139
|
+
* @param {import('../limits.js').Budget} budget
|
|
140
|
+
* @returns {{ points: number, indexCount: number|null, data: Map<number, ArrayLike<number>> }} data is accessor index -> values, in the accessor's component type
|
|
141
|
+
*/
|
|
142
|
+
export function decodeDracoPrimitive( draco, bytes, json, { mesh, index: primitiveIndex, primitive }, budget ) {
|
|
143
|
+
|
|
144
|
+
const ext = primitive.extensions.KHR_draco_mesh_compression;
|
|
145
|
+
const decoder = new draco.Decoder();
|
|
146
|
+
const array = new Int8Array( bytes.buffer, bytes.byteOffset, bytes.byteLength );
|
|
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
|
+
};
|
|
157
|
+
|
|
158
|
+
try {
|
|
159
|
+
|
|
160
|
+
const type = decoder.GetEncodedGeometryType( array );
|
|
161
|
+
let status;
|
|
162
|
+
|
|
163
|
+
if ( type === draco.TRIANGULAR_MESH ) {
|
|
164
|
+
|
|
165
|
+
geometry = new draco.Mesh();
|
|
166
|
+
status = decoder.DecodeArrayToMesh( array, array.byteLength, geometry );
|
|
167
|
+
|
|
168
|
+
} else if ( type === draco.POINT_CLOUD ) {
|
|
169
|
+
|
|
170
|
+
geometry = new draco.PointCloud();
|
|
171
|
+
status = decoder.DecodeArrayToPointCloud( array, array.byteLength, geometry );
|
|
172
|
+
|
|
173
|
+
} else {
|
|
174
|
+
|
|
175
|
+
throw new Error( 'usd-gltf: Draco data is neither a mesh nor a point cloud' );
|
|
176
|
+
|
|
177
|
+
}
|
|
178
|
+
|
|
179
|
+
if ( ! status.ok() || geometry.ptr === 0 ) throw new Error( `usd-gltf: Draco decoding failed: ${ status.error_msg() }` );
|
|
180
|
+
|
|
181
|
+
points = geometry.num_points();
|
|
182
|
+
|
|
183
|
+
for ( const [ semantic, id ] of Object.entries( ext.attributes ) ) {
|
|
184
|
+
|
|
185
|
+
const index = primitive.attributes[ semantic ];
|
|
186
|
+
const accessor = json.accessors[ index ];
|
|
187
|
+
if ( accessor === undefined ) continue;
|
|
188
|
+
const ArrayType = COMPONENT_ARRAYS[ accessor.componentType ];
|
|
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 } );
|
|
192
|
+
const attribute = decoder.GetAttributeByUniqueId( geometry, id );
|
|
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 ) ) );
|
|
196
|
+
|
|
197
|
+
}
|
|
198
|
+
|
|
199
|
+
if ( primitive.indices !== undefined && type === draco.TRIANGULAR_MESH ) {
|
|
200
|
+
|
|
201
|
+
const accessor = json.accessors[ primitive.indices ];
|
|
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 ) );
|
|
211
|
+
|
|
212
|
+
}
|
|
213
|
+
|
|
214
|
+
} finally {
|
|
215
|
+
|
|
216
|
+
if ( geometry ) draco.destroy( geometry );
|
|
217
|
+
draco.destroy( decoder );
|
|
218
|
+
|
|
219
|
+
}
|
|
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;
|
|
276
|
+
return out;
|
|
277
|
+
|
|
278
|
+
}
|
|
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
|
+
|
|
387
|
+
// Has the decoder fill a buffer on its heap, then copies it out.
|
|
388
|
+
function read( draco, count, ArrayType, budget, fill ) {
|
|
389
|
+
|
|
390
|
+
const length = count * ArrayType.BYTES_PER_ELEMENT;
|
|
391
|
+
budget.decode( length, 'draco' );
|
|
392
|
+
const ptr = draco._malloc( length );
|
|
393
|
+
|
|
394
|
+
try {
|
|
395
|
+
|
|
396
|
+
if ( ! fill( length, ptr ) ) throw new Error( 'usd-gltf: Draco attribute could not be read' );
|
|
397
|
+
return new ArrayType( draco.HEAPF32.buffer, ptr, count ).slice();
|
|
398
|
+
|
|
399
|
+
} finally {
|
|
400
|
+
|
|
401
|
+
draco._free( ptr );
|
|
402
|
+
|
|
403
|
+
}
|
|
404
|
+
|
|
405
|
+
}
|
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;
|
|
@@ -53,7 +81,19 @@ export interface GLBToUSDOptions extends ConversionLimits {
|
|
|
53
81
|
format?: 'usdz' | 'usda' | 'usdc';
|
|
54
82
|
/** Needed for .gltf files with external buffers or images. */
|
|
55
83
|
resolver?: Resolver;
|
|
84
|
+
/**
|
|
85
|
+
* A Draco decoder module (from draco3d's createDecoderModule, or Google's decoder build), or a
|
|
86
|
+
* promise of one. Without it, Draco files load draco3d in Node and Google's CDN build in browsers.
|
|
87
|
+
*/
|
|
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;
|
|
56
95
|
imageDecoder?: ImageDecoder;
|
|
96
|
+
/** Receives non fatal problems, such as textures that could not be read or recovered Draco counts. */
|
|
57
97
|
log?: ( message: string ) => void;
|
|
58
98
|
}
|
|
59
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>, 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, ...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 } );
|
|
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, ...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 } );
|
|
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' );
|