usd-gltf 0.2.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 +15 -2
- package/package.json +12 -2
- package/src/gltf/document.js +26 -25
- package/src/gltf/draco.js +191 -0
- package/src/index.d.ts +5 -0
- package/src/index.js +5 -5
package/README.md
CHANGED
|
@@ -37,7 +37,20 @@ 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
|
+
```
|
|
41
54
|
|
|
42
55
|
## Limits
|
|
43
56
|
|
|
@@ -62,7 +75,7 @@ Going over a limit rejects with a `ConversionLimitError` whose `stage`, `resourc
|
|
|
62
75
|
Current status:
|
|
63
76
|
|
|
64
77
|
- 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)
|
|
78
|
+
- 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
79
|
|
|
67
80
|
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
81
|
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "usd-gltf",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.3.0",
|
|
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 } 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,9 @@ 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 }} [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
|
|
234
219
|
*/
|
|
235
|
-
export async function readGLTF( bytes, { resolver = null, budget = new Budget() } = {} ) {
|
|
220
|
+
export async function readGLTF( bytes, { resolver = null, budget = new Budget(), dracoDecoder = null } = {} ) {
|
|
236
221
|
|
|
237
222
|
let json, bin = null;
|
|
238
223
|
bytes = plainBytes( bytes );
|
|
@@ -242,7 +227,6 @@ export async function readGLTF( bytes, { resolver = null, budget = new Budget()
|
|
|
242
227
|
|
|
243
228
|
const unsupported = unsupportedExtensions( json );
|
|
244
229
|
if ( unsupported.length ) throw new Error( `usd-gltf: required extension${ unsupported.length > 1 ? 's' : '' } ${ unsupported.join( ', ' ) } not supported` );
|
|
245
|
-
checkDracoFallbacks( json );
|
|
246
230
|
|
|
247
231
|
async function load( uri ) {
|
|
248
232
|
|
|
@@ -283,6 +267,23 @@ export async function readGLTF( bytes, { resolver = null, budget = new Budget()
|
|
|
283
267
|
|
|
284
268
|
} );
|
|
285
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
|
+
|
|
286
287
|
doc.images = await Promise.all( ( json.images || [] ).map( async ( image ) => {
|
|
287
288
|
|
|
288
289
|
if ( image.bufferView !== undefined ) return { bytes: doc.bufferViewBytes( image.bufferView ), mimeType: image.mimeType, name: image.name, uri: null };
|
|
@@ -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/index.d.ts
CHANGED
|
@@ -53,6 +53,11 @@ export interface GLBToUSDOptions extends ConversionLimits {
|
|
|
53
53
|
format?: 'usdz' | 'usda' | 'usdc';
|
|
54
54
|
/** Needed for .gltf files with external buffers or images. */
|
|
55
55
|
resolver?: Resolver;
|
|
56
|
+
/**
|
|
57
|
+
* A Draco decoder module (from draco3d's createDecoderModule, or Google's decoder build), or a
|
|
58
|
+
* promise of one. Without it, Draco files load draco3d in Node and Google's CDN build in browsers.
|
|
59
|
+
*/
|
|
60
|
+
dracoDecoder?: object | Promise<object>;
|
|
56
61
|
imageDecoder?: ImageDecoder;
|
|
57
62
|
log?: ( message: string ) => void;
|
|
58
63
|
}
|
package/src/index.js
CHANGED
|
@@ -50,13 +50,13 @@ export async function usdToGLB( data, options = {} ) {
|
|
|
50
50
|
/**
|
|
51
51
|
* Converts GLB (or glTF JSON with embedded or resolvable buffers) to USD.
|
|
52
52
|
* @param {ArrayBuffer|Uint8Array} data
|
|
53
|
-
* @param {{ format?: 'usda'|'usdc'|'usdz', resolver?: ( uri: string ) => Promise<Uint8Array>, maxDecodedBytes?: number, maxOutputBytes?: number, signal?: AbortSignal }} [options]
|
|
53
|
+
* @param {{ format?: 'usda'|'usdc'|'usdz', resolver?: ( uri: string ) => Promise<Uint8Array>, dracoDecoder?: object, maxDecodedBytes?: number, maxOutputBytes?: number, signal?: AbortSignal }} [options]
|
|
54
54
|
* @returns {Promise<Uint8Array>}
|
|
55
55
|
*/
|
|
56
|
-
export async function glbToUSD( data, { format = 'usdz', resolver = null, log, imageDecoder, ...limits } = {} ) {
|
|
56
|
+
export async function glbToUSD( data, { format = 'usdz', resolver = null, log, imageDecoder, dracoDecoder, ...limits } = {} ) {
|
|
57
57
|
|
|
58
58
|
const budget = budgetFor( limits );
|
|
59
|
-
const doc = await readGLTF( toBytes( data ), { resolver, budget } );
|
|
59
|
+
const doc = await readGLTF( toBytes( data ), { resolver, budget, dracoDecoder } );
|
|
60
60
|
const { layer, files } = await new GLTFToUSD( doc, { log, imageDecoder } ).convert();
|
|
61
61
|
let out;
|
|
62
62
|
|
|
@@ -73,10 +73,10 @@ export async function glbToUSD( data, { format = 'usdz', resolver = null, log, i
|
|
|
73
73
|
* For usda / usdc output the textures are separate files; this returns them alongside the layer.
|
|
74
74
|
* @returns {Promise<{ layer: Uint8Array, files: Array<{ name: string, data: Uint8Array }> }>}
|
|
75
75
|
*/
|
|
76
|
-
export async function glbToUSDFiles( data, { format = 'usda', resolver = null, log, imageDecoder, ...limits } = {} ) {
|
|
76
|
+
export async function glbToUSDFiles( data, { format = 'usda', resolver = null, log, imageDecoder, dracoDecoder, ...limits } = {} ) {
|
|
77
77
|
|
|
78
78
|
const budget = budgetFor( limits );
|
|
79
|
-
const doc = await readGLTF( toBytes( data ), { resolver, budget } );
|
|
79
|
+
const doc = await readGLTF( toBytes( data ), { resolver, budget, dracoDecoder } );
|
|
80
80
|
const { layer, files } = await new GLTFToUSD( doc, { log, imageDecoder } ).convert();
|
|
81
81
|
const bytes = format === 'usdc' ? writeUSDC( layer, { budget } ) : new TextEncoder().encode( writeUSDA( layer, { budget } ) );
|
|
82
82
|
budget.output( files.reduce( ( n, f ) => n + f.data.byteLength, bytes.byteLength ), 'usd files' );
|