usd-gltf 0.1.0 → 0.3.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +36 -3
- package/package.json +12 -2
- package/src/convert/gltf-to-usd.js +166 -46
- package/src/convert/usd-material.js +7 -2
- package/src/convert/usd-to-gltf.js +8 -4
- package/src/gltf/document.js +109 -19
- package/src/gltf/draco.js +191 -0
- package/src/gltf/glb.js +2 -2
- package/src/image/index.js +52 -1
- package/src/image/inflate.js +14 -12
- package/src/image/png.js +1 -1
- package/src/index.d.ts +30 -4
- package/src/index.js +33 -14
- package/src/limits.js +101 -0
- package/src/usd/stage.js +13 -9
- package/src/usd/usda/writer.js +16 -1
- package/src/usd/usdc/reader.js +63 -13
- package/src/usd/usdc/writer.js +53 -3
- package/src/usd/usdz.js +9 -3
package/README.md
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
# usd-gltf
|
|
2
2
|
|
|
3
|
-
Convert between [OpenUSD](https://openusd.org/) and [glTF](https://www.khronos.org/gltf/) in both directions, in pure JavaScript with no dependencies. Runs in Node and the browser, [live demo](https://richardmonette.github.io/usd-gltf/examples/). Reads and writes all three USD encodings: text `.usda`, binary crate `.usdc`, and the `.usdz` package.
|
|
3
|
+
Convert between [OpenUSD](https://openusd.org/) and [glTF](https://www.khronos.org/gltf/) (`.glb`, or `.gltf` with embedded or external files) in both directions, in pure JavaScript with no dependencies. Runs in Node and the browser, [live demo](https://richardmonette.github.io/usd-gltf/examples/). Reads and writes all three USD encodings: text `.usda`, binary crate `.usdc`, and the `.usdz` package.
|
|
4
4
|
|
|
5
5
|
```
|
|
6
6
|
npm install usd-gltf
|
|
@@ -35,14 +35,47 @@ USD to glTF composes the stage first (sublayers, references, payloads, inherits,
|
|
|
35
35
|
- Material variant sets as `KHR_materials_variants`, animation variant sets as separate glTF animations
|
|
36
36
|
- Cameras, UsdLux lights as `KHR_lights_punctual`, PointInstancers
|
|
37
37
|
|
|
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.
|
|
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
|
+
|
|
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
|
+
## Limits
|
|
56
|
+
|
|
57
|
+
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:
|
|
58
|
+
|
|
59
|
+
```js
|
|
60
|
+
const usdz = await glbToUSD( glb, {
|
|
61
|
+
maxDecodedBytes: 512 * 2 ** 20, // decoded data held during the conversion
|
|
62
|
+
maxOutputBytes: 256 * 2 ** 20, // any output buffer: the layer, the usdz or glb, as it grows
|
|
63
|
+
signal: AbortSignal.timeout( 30000 ), // cancellation
|
|
64
|
+
} );
|
|
65
|
+
```
|
|
66
|
+
|
|
67
|
+
- `maxDecodedBytes` counts, before each allocation: glTF accessors (including ones without a bufferView) and meshopt decoding; USD crate arrays, compressed sections and zip entries; images, from the size in their header; and the arrays written into the converted USD layer, so data referenced many times is counted every time it is written. It is not a limit on the whole process's memory: the input, its parsed JSON or USD text, and other transient copies aren't counted.
|
|
68
|
+
- `maxOutputBytes` is checked as the usdc, usda and glb writers grow and before the final usdz or glb buffer is allocated.
|
|
69
|
+
- `signal` is checked between prims, nodes, meshes, materials and layers. A cancelled conversion rejects with the signal's reason and returns nothing. To stop work that is in a single long step, run the conversion in a worker and terminate it.
|
|
70
|
+
|
|
71
|
+
Going over a limit rejects with a `ConversionLimitError` whose `stage`, `resource` (`'decoded'` or `'output'`), `limit` and `attempted` say what was too big; it never includes input data or file names. Without limits nothing changes. Sizes that can't be right (an accessor reading past its bufferView, a count too big to allocate, a zip entry inflating past its stated size) are rejected whatever the limits.
|
|
39
72
|
|
|
40
73
|
## Conformance
|
|
41
74
|
|
|
42
75
|
Current status:
|
|
43
76
|
|
|
44
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.)
|
|
45
|
-
- 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.
|
|
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.
|
|
46
79
|
|
|
47
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.
|
|
48
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
|
},
|
|
@@ -14,6 +14,7 @@
|
|
|
14
14
|
import { AssetPath, Layer, ListOp, PrimSpec, PropertySpec } from '../usd/model.js';
|
|
15
15
|
import { compose, decompose, identity, invert, multiply } from '../util/math.js';
|
|
16
16
|
import { decodeImageAsync, encodePNG, imageMimeType } from '../image/index.js';
|
|
17
|
+
import { rethrowFatal } from '../limits.js';
|
|
17
18
|
|
|
18
19
|
const MODES = { POINTS: 0, LINES: 1, LINE_LOOP: 2, LINE_STRIP: 3, TRIANGLES: 4, TRIANGLE_STRIP: 5, TRIANGLE_FAN: 6 };
|
|
19
20
|
const WRAP = { 10497: 'repeat', 33648: 'mirror', 33071: 'clamp' };
|
|
@@ -33,8 +34,9 @@ function sanitize( name, fallback ) {
|
|
|
33
34
|
|
|
34
35
|
class Builder {
|
|
35
36
|
|
|
36
|
-
constructor() {
|
|
37
|
+
constructor( budget ) {
|
|
37
38
|
|
|
39
|
+
this.budget = budget;
|
|
38
40
|
this.layer = new Layer( 'root.usda' );
|
|
39
41
|
this.paths = new Map(); // PrimSpec -> path
|
|
40
42
|
|
|
@@ -65,6 +67,8 @@ class Builder {
|
|
|
65
67
|
|
|
66
68
|
attr( prim, name, typeName, value, { interpolation, uniform = false, metadata = {}, timeSamples = null, custom = false } = {} ) {
|
|
67
69
|
|
|
70
|
+
// arrays authored in the layer count as decoded data: they live until the layer is written
|
|
71
|
+
if ( ArrayBuffer.isView( value ) ) this.budget.decode( value.byteLength, 'usd layer' );
|
|
68
72
|
const p = prim.addProperty( new PropertySpec( name, 'attribute' ) );
|
|
69
73
|
p.typeName = typeName;
|
|
70
74
|
if ( value !== undefined ) p.default = value;
|
|
@@ -110,17 +114,20 @@ export class GLTFToUSD {
|
|
|
110
114
|
|
|
111
115
|
this.doc = doc;
|
|
112
116
|
this.json = doc.json;
|
|
117
|
+
this.budget = doc.budget;
|
|
113
118
|
this.log = log;
|
|
114
119
|
this.textureDir = textureDir;
|
|
115
120
|
this.imageDecoder = imageDecoder;
|
|
116
121
|
this.bakeNormalScale = bakeNormalScale;
|
|
117
122
|
this.debug = debug; // returns the node/primitive -> prim path mapping, for verification
|
|
118
|
-
this.b = new Builder();
|
|
123
|
+
this.b = new Builder( this.budget );
|
|
119
124
|
this.files = []; // { name, data } for the package
|
|
120
125
|
this.imageFiles = new Map(); // image index -> path | null
|
|
121
126
|
this.materialPaths = new Map(); // material index (or 'default') -> path
|
|
122
127
|
this.nodePrims = new Map(); // node index -> prim spec
|
|
123
128
|
this.meshPrims = new Map(); // `${node}/${primitive}` -> prim spec
|
|
129
|
+
this.bindingPrims = new Map(); // `${node}/${primitive}` -> prim spec its material binds to (the mesh or a GeomSubset)
|
|
130
|
+
this.vertexOrders = new Map(); // prim spec -> glTF vertex index per USD point, when compacted
|
|
124
131
|
this.jointNodes = new Set();
|
|
125
132
|
this.timeCodes = new Set();
|
|
126
133
|
|
|
@@ -199,11 +206,12 @@ export class GLTFToUSD {
|
|
|
199
206
|
// usdz packages may only hold png and jpeg images
|
|
200
207
|
try {
|
|
201
208
|
|
|
202
|
-
bytes = encodePNG( await decodeImageAsync( bytes, mime, this.imageDecoder ) );
|
|
209
|
+
bytes = encodePNG( await decodeImageAsync( bytes, mime, this.imageDecoder, this.budget ) );
|
|
203
210
|
mime = 'image/png';
|
|
204
211
|
|
|
205
212
|
} catch ( e ) {
|
|
206
213
|
|
|
214
|
+
rethrowFatal( e, this.budget );
|
|
207
215
|
this.log( `image ${ index }: ${ e.message }` );
|
|
208
216
|
bytes = null;
|
|
209
217
|
|
|
@@ -238,7 +246,7 @@ export class GLTFToUSD {
|
|
|
238
246
|
|
|
239
247
|
try {
|
|
240
248
|
|
|
241
|
-
const img = await decodeImageAsync( image.bytes, image.mimeType, this.imageDecoder );
|
|
249
|
+
const img = await decodeImageAsync( image.bytes, image.mimeType, this.imageDecoder, this.budget );
|
|
242
250
|
const max = img.bitDepth === 16 ? 65535 : 255;
|
|
243
251
|
const out = new Uint8Array( img.width * img.height * 3 );
|
|
244
252
|
|
|
@@ -261,6 +269,7 @@ export class GLTFToUSD {
|
|
|
261
269
|
|
|
262
270
|
} catch ( e ) {
|
|
263
271
|
|
|
272
|
+
rethrowFatal( e, this.budget );
|
|
264
273
|
this.log( `normal map ${ source }: ${ e.message }` );
|
|
265
274
|
|
|
266
275
|
}
|
|
@@ -275,7 +284,8 @@ export class GLTFToUSD {
|
|
|
275
284
|
const texture = this.json.textures?.[ textureIndex ];
|
|
276
285
|
if ( ! texture ) return undefined;
|
|
277
286
|
const ext = texture.extensions || {};
|
|
278
|
-
|
|
287
|
+
// KTX2 can't be decoded, so an optional basisu texture uses its fallback
|
|
288
|
+
return ext.EXT_texture_webp?.source ?? ext.EXT_texture_avif?.source ?? texture.source ?? ext.KHR_texture_basisu?.source;
|
|
279
289
|
|
|
280
290
|
}
|
|
281
291
|
|
|
@@ -323,6 +333,7 @@ export class GLTFToUSD {
|
|
|
323
333
|
|
|
324
334
|
async convertMaterial( material, index ) {
|
|
325
335
|
|
|
336
|
+
this.budget.check();
|
|
326
337
|
const b = this.b;
|
|
327
338
|
const m = b.def( this.materialScope, sanitize( material.name, 'material' ), 'Material' );
|
|
328
339
|
const mPath = b.path( m );
|
|
@@ -572,6 +583,7 @@ export class GLTFToUSD {
|
|
|
572
583
|
|
|
573
584
|
convertNode( index, parent ) {
|
|
574
585
|
|
|
586
|
+
this.budget.check();
|
|
575
587
|
const node = this.json.nodes[ index ];
|
|
576
588
|
const b = this.b;
|
|
577
589
|
if ( this.jointNodes.has( index ) && ! this.hasContent( index ) ) return null;
|
|
@@ -664,22 +676,31 @@ export class GLTFToUSD {
|
|
|
664
676
|
const node = this.json.nodes[ nodeIndex ];
|
|
665
677
|
const mesh = this.json.meshes[ node.mesh ];
|
|
666
678
|
const skinned = node.skin !== undefined;
|
|
667
|
-
const instancing = node.extensions?.EXT_mesh_gpu_instancing;
|
|
679
|
+
const instancing = node.extensions?.EXT_mesh_gpu_instancing && ! skinned;
|
|
680
|
+
const groups = instancing ? mesh.primitives.map( ( _, p ) => [ p ] ) : this.primitiveGroups( mesh );
|
|
668
681
|
|
|
669
|
-
|
|
682
|
+
groups.forEach( ( group ) => {
|
|
670
683
|
|
|
671
|
-
const
|
|
684
|
+
const p = group[ 0 ];
|
|
685
|
+
const primitive = mesh.primitives[ p ];
|
|
686
|
+
const name = sanitize( groups.length > 1 ? `${ mesh.name || 'mesh' }_${ p }` : mesh.name, `mesh_${ node.mesh }` );
|
|
672
687
|
|
|
673
|
-
if ( instancing
|
|
688
|
+
if ( instancing ) {
|
|
674
689
|
|
|
675
|
-
this.convertInstancer( prim, name, primitive,
|
|
690
|
+
this.convertInstancer( prim, name, primitive, node.extensions.EXT_mesh_gpu_instancing, nodeIndex, p );
|
|
676
691
|
return;
|
|
677
692
|
|
|
678
693
|
}
|
|
679
694
|
|
|
680
|
-
const meshPrim = this.convertPrimitive( prim, name, primitive, { nodeIndex, joints: skinned } );
|
|
695
|
+
const meshPrim = this.convertPrimitive( prim, name, primitive, { nodeIndex, joints: skinned, group: group.map( ( q ) => mesh.primitives[ q ] ) } );
|
|
681
696
|
if ( ! meshPrim ) return;
|
|
682
|
-
|
|
697
|
+
|
|
698
|
+
group.forEach( ( q, k ) => {
|
|
699
|
+
|
|
700
|
+
this.meshPrims.set( `${ nodeIndex }/${ q }`, meshPrim );
|
|
701
|
+
this.bindingPrims.set( `${ nodeIndex }/${ q }`, group.length > 1 ? meshPrim.children.get( `primitive_${ k }` ) : meshPrim );
|
|
702
|
+
|
|
703
|
+
} );
|
|
683
704
|
|
|
684
705
|
// skins are bound once skeletons exist; morph only meshes get a rigid one joint skeleton
|
|
685
706
|
if ( skinned ) ( this.pendingSkinned ||= [] ).push( { nodeIndex, primitive, p, meshPrim } );
|
|
@@ -690,9 +711,32 @@ export class GLTFToUSD {
|
|
|
690
711
|
}
|
|
691
712
|
|
|
692
713
|
/**
|
|
693
|
-
*
|
|
714
|
+
* Groups a mesh's primitives that share all vertex data, so each group becomes one USD Mesh
|
|
715
|
+
* with a GeomSubset per primitive (how USD -> glTF splits a mesh by material).
|
|
716
|
+
* @returns {number[][]} primitive indices
|
|
717
|
+
*/
|
|
718
|
+
primitiveGroups( mesh ) {
|
|
719
|
+
|
|
720
|
+
const groups = new Map();
|
|
721
|
+
|
|
722
|
+
mesh.primitives.forEach( ( q, p ) => {
|
|
723
|
+
|
|
724
|
+
const triangles = ( q.mode ?? MODES.TRIANGLES ) >= MODES.TRIANGLES && q.indices !== undefined && q.attributes.POSITION !== undefined;
|
|
725
|
+
const doubleSided = !! this.json.materials?.[ q.material ]?.doubleSided;
|
|
726
|
+
const key = triangles ? JSON.stringify( [ Object.entries( q.attributes ).sort(), q.targets ?? null, doubleSided ] ) : p;
|
|
727
|
+
( groups.get( key ) || groups.set( key, [] ).get( key ) ).push( p );
|
|
728
|
+
|
|
729
|
+
} );
|
|
730
|
+
|
|
731
|
+
return [ ...groups.values() ];
|
|
732
|
+
|
|
733
|
+
}
|
|
734
|
+
|
|
735
|
+
/**
|
|
736
|
+
* Writes one glTF primitive as a Mesh, Points or BasisCurves prim. Triangle primitives sharing
|
|
737
|
+
* vertex data (group) become one Mesh with a GeomSubset each. Unreferenced vertices are dropped.
|
|
694
738
|
*/
|
|
695
|
-
convertPrimitive( parent, name, primitive, { nodeIndex, joints = null } = {} ) {
|
|
739
|
+
convertPrimitive( parent, name, primitive, { nodeIndex, joints = null, group = [ primitive ] } = {} ) {
|
|
696
740
|
|
|
697
741
|
const doc = this.doc;
|
|
698
742
|
const b = this.b;
|
|
@@ -750,57 +794,72 @@ export class GLTFToUSD {
|
|
|
750
794
|
}
|
|
751
795
|
|
|
752
796
|
// triangles; strips and fans are expanded
|
|
753
|
-
|
|
754
|
-
|
|
755
|
-
if ( mode === MODES.TRIANGLE_STRIP ) {
|
|
797
|
+
const parts = group.map( ( q ) => q === primitive ? triangleList( indices, mode ) : triangleList( doc.getAccessor( q.indices ).array, q.mode ?? MODES.TRIANGLES ) );
|
|
798
|
+
let tris = parts[ 0 ];
|
|
756
799
|
|
|
757
|
-
|
|
758
|
-
for ( let i = 0; i + 2 < indices.length; i ++ ) {
|
|
800
|
+
if ( parts.length > 1 ) {
|
|
759
801
|
|
|
760
|
-
|
|
761
|
-
|
|
762
|
-
|
|
763
|
-
}
|
|
764
|
-
|
|
765
|
-
tris = Uint32Array.from( out );
|
|
766
|
-
|
|
767
|
-
} else if ( mode === MODES.TRIANGLE_FAN ) {
|
|
768
|
-
|
|
769
|
-
const out = [];
|
|
770
|
-
for ( let i = 1; i + 1 < indices.length; i ++ ) out.push( indices[ 0 ], indices[ i ], indices[ i + 1 ] );
|
|
771
|
-
tris = Uint32Array.from( out );
|
|
802
|
+
tris = new Uint32Array( parts.reduce( ( n, t ) => n + t.length, 0 ) );
|
|
803
|
+
parts.reduce( ( at, t ) => ( tris.set( t, at ), at + t.length ), 0 );
|
|
772
804
|
|
|
773
805
|
}
|
|
774
806
|
|
|
775
|
-
|
|
807
|
+
// keep only referenced vertices, in their original order
|
|
808
|
+
const order = vertexOrder( tris, count );
|
|
809
|
+
if ( order ) tris = remapIndices( tris, order, count );
|
|
810
|
+
const pick = ( array, size ) => order ? gather( array, size, order ) : array;
|
|
811
|
+
const points = pick( positions, 3 );
|
|
812
|
+
|
|
776
813
|
const prim = b.def( parent, name, 'Mesh' );
|
|
777
|
-
b.attr( prim, 'points', 'point3f[]',
|
|
814
|
+
b.attr( prim, 'points', 'point3f[]', points );
|
|
778
815
|
b.attr( prim, 'faceVertexCounts', 'int[]', new Int32Array( tris.length / 3 ).fill( 3 ) );
|
|
779
816
|
b.attr( prim, 'faceVertexIndices', 'int[]', Int32Array.from( tris ) );
|
|
780
|
-
b.attr( prim, 'extent', 'float3[]', extent(
|
|
817
|
+
b.attr( prim, 'extent', 'float3[]', extent( points ) );
|
|
781
818
|
b.attr( prim, 'subdivisionScheme', 'token', 'none', { uniform: true } );
|
|
819
|
+
if ( order ) this.vertexOrders.set( prim, order );
|
|
782
820
|
|
|
783
|
-
if ( attrs.NORMAL !== undefined ) b.attr( prim, 'normals', 'normal3f[]', doc.getFloats( attrs.NORMAL ).array, { interpolation: 'vertex' } );
|
|
821
|
+
if ( attrs.NORMAL !== undefined ) b.attr( prim, 'normals', 'normal3f[]', pick( doc.getFloats( attrs.NORMAL ).array, 3 ), { interpolation: 'vertex' } );
|
|
784
822
|
|
|
785
823
|
for ( let i = 0; attrs[ 'TEXCOORD_' + i ] !== undefined; i ++ ) {
|
|
786
824
|
|
|
787
|
-
const uv = doc.getFloats( attrs[ 'TEXCOORD_' + i ] ).array;
|
|
825
|
+
const uv = pick( doc.getFloats( attrs[ 'TEXCOORD_' + i ] ).array, 2 );
|
|
788
826
|
const st = new Float32Array( uv.length );
|
|
789
827
|
for ( let k = 0; k < uv.length; k += 2 ) { st[ k ] = uv[ k ]; st[ k + 1 ] = 1 - uv[ k + 1 ]; }
|
|
790
828
|
b.attr( prim, i ? `primvars:st${ i }` : 'primvars:st', 'texCoord2f[]', st, { interpolation: 'vertex' } );
|
|
791
829
|
|
|
792
830
|
}
|
|
793
831
|
|
|
794
|
-
if ( attrs.TANGENT !== undefined ) b.attr( prim, 'primvars:tangents', 'float4[]', doc.getFloats( attrs.TANGENT ).array, { interpolation: 'vertex' } );
|
|
832
|
+
if ( attrs.TANGENT !== undefined ) b.attr( prim, 'primvars:tangents', 'float4[]', pick( doc.getFloats( attrs.TANGENT ).array, 4 ), { interpolation: 'vertex' } );
|
|
795
833
|
|
|
796
|
-
this.writeColors( prim, attrs,
|
|
834
|
+
this.writeColors( prim, attrs, order, 'vertex' );
|
|
797
835
|
if ( attrs.COLOR_0 !== undefined ) gltfData( prim ).color0 = true;
|
|
798
836
|
|
|
799
837
|
const material = primitive.material !== undefined ? this.json.materials[ primitive.material ] : null;
|
|
800
838
|
if ( material?.doubleSided ) b.attr( prim, 'doubleSided', 'bool', true, { uniform: true } );
|
|
801
|
-
this.bindMaterial( prim, primitive.material );
|
|
802
839
|
|
|
803
|
-
if (
|
|
840
|
+
if ( group.length === 1 ) {
|
|
841
|
+
|
|
842
|
+
this.bindMaterial( prim, primitive.material );
|
|
843
|
+
|
|
844
|
+
} else {
|
|
845
|
+
|
|
846
|
+
b.attr( prim, 'subsetFamily:materialBind:familyType', 'token', 'partition', { uniform: true } );
|
|
847
|
+
let face = 0;
|
|
848
|
+
|
|
849
|
+
parts.forEach( ( t, k ) => {
|
|
850
|
+
|
|
851
|
+
const subset = b.def( prim, `primitive_${ k }`, 'GeomSubset' );
|
|
852
|
+
b.attr( subset, 'elementType', 'token', 'face', { uniform: true } );
|
|
853
|
+
b.attr( subset, 'familyName', 'token', 'materialBind', { uniform: true } );
|
|
854
|
+
b.attr( subset, 'indices', 'int[]', Int32Array.from( { length: t.length / 3 }, ( _, i ) => face + i ) );
|
|
855
|
+
face += t.length / 3;
|
|
856
|
+
this.bindMaterial( subset, group[ k ].material );
|
|
857
|
+
|
|
858
|
+
} );
|
|
859
|
+
|
|
860
|
+
}
|
|
861
|
+
|
|
862
|
+
if ( joints ) this.writeSkinWeights( prim, attrs, joints, order );
|
|
804
863
|
|
|
805
864
|
void nodeIndex;
|
|
806
865
|
return prim;
|
|
@@ -865,10 +924,11 @@ export class GLTFToUSD {
|
|
|
865
924
|
|
|
866
925
|
// --- skins and morph targets ---
|
|
867
926
|
|
|
868
|
-
writeSkinWeights( prim, attrs, joints ) {
|
|
927
|
+
writeSkinWeights( prim, attrs, joints, order = null ) {
|
|
869
928
|
|
|
929
|
+
const pick = ( array ) => order ? gather( array, 4, order ) : array;
|
|
870
930
|
const sets = [];
|
|
871
|
-
for ( let s = 0; attrs[ 'JOINTS_' + s ] !== undefined; s ++ ) sets.push( [ this.doc.getAccessor( attrs[ 'JOINTS_' + s ], { normalize: false } ).array, this.doc.getFloats( attrs[ 'WEIGHTS_' + s ] ).array ] );
|
|
931
|
+
for ( let s = 0; attrs[ 'JOINTS_' + s ] !== undefined; s ++ ) sets.push( [ pick( this.doc.getAccessor( attrs[ 'JOINTS_' + s ], { normalize: false } ).array ), pick( this.doc.getFloats( attrs[ 'WEIGHTS_' + s ] ).array ) ] );
|
|
872
932
|
if ( ! sets.length ) return;
|
|
873
933
|
|
|
874
934
|
const n = sets[ 0 ][ 0 ].length / 4;
|
|
@@ -907,14 +967,16 @@ export class GLTFToUSD {
|
|
|
907
967
|
const b = this.b;
|
|
908
968
|
const targets = [];
|
|
909
969
|
const shapeNames = [];
|
|
970
|
+
const order = this.vertexOrders.get( meshPrim );
|
|
971
|
+
const pick = ( array ) => order ? gather( array, 3, order ) : array;
|
|
910
972
|
|
|
911
973
|
primitive.targets.forEach( ( target, i ) => {
|
|
912
974
|
|
|
913
975
|
const name = sanitize( names?.[ i ], `target_${ i }` );
|
|
914
976
|
const shape = b.def( meshPrim, name, 'BlendShape' );
|
|
915
|
-
const count = this.doc.json.accessors[ primitive.attributes.POSITION ].count;
|
|
916
|
-
b.attr( shape, 'offsets', 'vector3f[]', target.POSITION !== undefined ? this.doc.getFloats( target.POSITION ).array : new Float32Array( count * 3 ), { uniform: true } );
|
|
917
|
-
if ( target.NORMAL !== undefined ) b.attr( shape, 'normalOffsets', 'vector3f[]', this.doc.getFloats( target.NORMAL ).array, { uniform: true } );
|
|
977
|
+
const count = order ? order.length : this.doc.json.accessors[ primitive.attributes.POSITION ].count;
|
|
978
|
+
b.attr( shape, 'offsets', 'vector3f[]', target.POSITION !== undefined ? pick( this.doc.getFloats( target.POSITION ).array ) : new Float32Array( count * 3 ), { uniform: true } );
|
|
979
|
+
if ( target.NORMAL !== undefined ) b.attr( shape, 'normalOffsets', 'vector3f[]', pick( this.doc.getFloats( target.NORMAL ).array ), { uniform: true } );
|
|
918
980
|
targets.push( b.path( shape ) );
|
|
919
981
|
shapeNames.push( shape.name );
|
|
920
982
|
|
|
@@ -1648,7 +1710,7 @@ export class GLTFToUSD {
|
|
|
1648
1710
|
const variant = new PrimSpec( name, 'over', '' );
|
|
1649
1711
|
set.set( name, variant );
|
|
1650
1712
|
|
|
1651
|
-
for ( const [ key, prim ] of this.
|
|
1713
|
+
for ( const [ key, prim ] of this.bindingPrims ) {
|
|
1652
1714
|
|
|
1653
1715
|
const [ nodeIndex, p ] = key.split( '/' ).map( Number );
|
|
1654
1716
|
const primitive = this.json.meshes[ this.json.nodes[ nodeIndex ].mesh ].primitives[ p ];
|
|
@@ -1771,6 +1833,64 @@ export function sampleTrack( track, time ) {
|
|
|
1771
1833
|
|
|
1772
1834
|
}
|
|
1773
1835
|
|
|
1836
|
+
// Triangle indices of a TRIANGLES, TRIANGLE_STRIP or TRIANGLE_FAN primitive.
|
|
1837
|
+
function triangleList( indices, mode ) {
|
|
1838
|
+
|
|
1839
|
+
let tris = indices;
|
|
1840
|
+
|
|
1841
|
+
if ( mode === MODES.TRIANGLE_STRIP ) {
|
|
1842
|
+
|
|
1843
|
+
const out = [];
|
|
1844
|
+
for ( let i = 0; i + 2 < indices.length; i ++ ) {
|
|
1845
|
+
|
|
1846
|
+
if ( i % 2 === 0 ) out.push( indices[ i ], indices[ i + 1 ], indices[ i + 2 ] );
|
|
1847
|
+
else out.push( indices[ i + 1 ], indices[ i ], indices[ i + 2 ] );
|
|
1848
|
+
|
|
1849
|
+
}
|
|
1850
|
+
|
|
1851
|
+
tris = Uint32Array.from( out );
|
|
1852
|
+
|
|
1853
|
+
} else if ( mode === MODES.TRIANGLE_FAN ) {
|
|
1854
|
+
|
|
1855
|
+
const out = [];
|
|
1856
|
+
for ( let i = 1; i + 1 < indices.length; i ++ ) out.push( indices[ 0 ], indices[ i ], indices[ i + 1 ] );
|
|
1857
|
+
tris = Uint32Array.from( out );
|
|
1858
|
+
|
|
1859
|
+
}
|
|
1860
|
+
|
|
1861
|
+
return tris.subarray( 0, tris.length - ( tris.length % 3 ) );
|
|
1862
|
+
|
|
1863
|
+
}
|
|
1864
|
+
|
|
1865
|
+
// The referenced vertices in ascending order, or null when every vertex is used.
|
|
1866
|
+
function vertexOrder( indices, count ) {
|
|
1867
|
+
|
|
1868
|
+
const used = new Uint8Array( count );
|
|
1869
|
+
let n = 0;
|
|
1870
|
+
for ( let i = 0; i < indices.length; i ++ ) if ( indices[ i ] < count && ! used[ indices[ i ] ] ) { used[ indices[ i ] ] = 1; n ++; }
|
|
1871
|
+
if ( n === count ) return null;
|
|
1872
|
+
const order = new Uint32Array( n );
|
|
1873
|
+
for ( let v = 0, k = 0; v < count; v ++ ) if ( used[ v ] ) order[ k ++ ] = v;
|
|
1874
|
+
return order;
|
|
1875
|
+
|
|
1876
|
+
}
|
|
1877
|
+
|
|
1878
|
+
function remapIndices( indices, order, count ) {
|
|
1879
|
+
|
|
1880
|
+
const map = new Uint32Array( count );
|
|
1881
|
+
order.forEach( ( v, k ) => { map[ v ] = k; } );
|
|
1882
|
+
return Uint32Array.from( indices, ( v ) => map[ v ] );
|
|
1883
|
+
|
|
1884
|
+
}
|
|
1885
|
+
|
|
1886
|
+
function gather( array, size, order ) {
|
|
1887
|
+
|
|
1888
|
+
const out = new array.constructor( order.length * size );
|
|
1889
|
+
for ( let k = 0; k < order.length; k ++ ) for ( let c = 0; c < size; c ++ ) out[ k * size + c ] = array[ order[ k ] * size + c ];
|
|
1890
|
+
return out;
|
|
1891
|
+
|
|
1892
|
+
}
|
|
1893
|
+
|
|
1774
1894
|
function extent( positions ) {
|
|
1775
1895
|
|
|
1776
1896
|
const min = [ Infinity, Infinity, Infinity ], max = [ - Infinity, - Infinity, - Infinity ];
|
|
@@ -9,6 +9,7 @@
|
|
|
9
9
|
import { decodeImageAsync, encodePNG, encodePNGAsync, imageMimeType, isGreyscaleImage } from '../image/index.js';
|
|
10
10
|
import { multiply } from '../util/math.js';
|
|
11
11
|
import { resolveAssetPath } from '../usd/stage.js';
|
|
12
|
+
import { Budget, rethrowFatal } from '../limits.js';
|
|
12
13
|
|
|
13
14
|
const WRAP = { repeat: 10497, mirror: 33648, clamp: 33071, black: 33071 };
|
|
14
15
|
const CHANNEL = { r: 0, g: 1, b: 2, a: 3, x: 0, y: 1, z: 2, w: 3 };
|
|
@@ -68,9 +69,10 @@ function binary( a, b, op ) {
|
|
|
68
69
|
|
|
69
70
|
export class MaterialConverter {
|
|
70
71
|
|
|
71
|
-
constructor( stage, builder, { log = () => {}, imageDecoder = null } = {} ) {
|
|
72
|
+
constructor( stage, builder, { log = () => {}, imageDecoder = null, budget = new Budget() } = {} ) {
|
|
72
73
|
|
|
73
74
|
this.stage = stage;
|
|
75
|
+
this.budget = budget;
|
|
74
76
|
this.builder = builder;
|
|
75
77
|
this.log = log;
|
|
76
78
|
this.imageDecoder = imageDecoder;
|
|
@@ -467,6 +469,7 @@ export class MaterialConverter {
|
|
|
467
469
|
|
|
468
470
|
} catch ( e ) {
|
|
469
471
|
|
|
472
|
+
rethrowFatal( e, this.budget );
|
|
470
473
|
this.log( `texture ${ file }: ${ e.message }` );
|
|
471
474
|
|
|
472
475
|
}
|
|
@@ -498,10 +501,11 @@ export class MaterialConverter {
|
|
|
498
501
|
|
|
499
502
|
try {
|
|
500
503
|
|
|
501
|
-
return await decodeImageAsync( await this.stage.readAsset( file ), undefined, this.imageDecoder );
|
|
504
|
+
return await decodeImageAsync( await this.stage.readAsset( file ), undefined, this.imageDecoder, this.budget );
|
|
502
505
|
|
|
503
506
|
} catch ( e ) {
|
|
504
507
|
|
|
508
|
+
rethrowFatal( e, this.budget );
|
|
505
509
|
this.log( `decode ${ file }: ${ e.message }` );
|
|
506
510
|
return null;
|
|
507
511
|
|
|
@@ -680,6 +684,7 @@ export class MaterialConverter {
|
|
|
680
684
|
*/
|
|
681
685
|
async convert( material, { doubleSided = false, vertexColors = false, variant = '' } = {} ) {
|
|
682
686
|
|
|
687
|
+
this.budget.check();
|
|
683
688
|
const key = material.path + ( doubleSided ? '|ds' : '' ) + ( variant ? '|' + variant : '' );
|
|
684
689
|
if ( this.materials.has( key ) ) return this.materials.get( key );
|
|
685
690
|
|
|
@@ -6,6 +6,7 @@ import { getXformOps, evaluateOps, xformSampleTimes } from '../usd/xform.js';
|
|
|
6
6
|
import { MaterialConverter, gltfTextureTransform } from './usd-material.js';
|
|
7
7
|
import { triangulateMesh, normalizeNormals } from './usd-mesh.js';
|
|
8
8
|
import { generateShape } from './usd-shapes.js';
|
|
9
|
+
import { Budget } from '../limits.js';
|
|
9
10
|
import { compose, decompose, identity, invert, isIdentity, multiply, normalMatrix, transformDirection, transformPoint } from '../util/math.js';
|
|
10
11
|
|
|
11
12
|
const SKIP_TYPES = new Set( [ 'Material', 'Shader', 'NodeGraph', 'GeomSubset', 'SkelAnimation', 'BlendShape', 'RenderSettings', 'RenderProduct', 'RenderVar', 'GeometryLight', 'DomeLight', 'Backdrop' ] );
|
|
@@ -14,14 +15,15 @@ const SHAPE_TYPES = new Set( [ 'Cube', 'Sphere', 'Cylinder', 'Cone', 'Capsule',
|
|
|
14
15
|
|
|
15
16
|
export class USDToGLTF {
|
|
16
17
|
|
|
17
|
-
constructor( stage, { log = () => {}, materialVariants = true, debug = false, imageDecoder = null } = {} ) {
|
|
18
|
+
constructor( stage, { log = () => {}, materialVariants = true, debug = false, imageDecoder = null, budget = new Budget() } = {} ) {
|
|
18
19
|
|
|
19
20
|
this.stage = stage;
|
|
21
|
+
this.budget = budget;
|
|
20
22
|
this.log = log;
|
|
21
23
|
this.debug = debug; // records usd paths and time mapping in extras, for verification
|
|
22
24
|
this.materialVariants = materialVariants;
|
|
23
|
-
this.builder = new GLTFBuilder();
|
|
24
|
-
this.materials = new MaterialConverter( stage, this.builder, { log, imageDecoder } );
|
|
25
|
+
this.builder = new GLTFBuilder( 'usd-gltf', budget );
|
|
26
|
+
this.materials = new MaterialConverter( stage, this.builder, { log, imageDecoder, budget } );
|
|
25
27
|
this.json = this.builder.json;
|
|
26
28
|
this.meshCache = new Map();
|
|
27
29
|
this.primNodes = new Map(); // prim path -> node index (first instance)
|
|
@@ -101,6 +103,7 @@ export class USDToGLTF {
|
|
|
101
103
|
|
|
102
104
|
async convertPrim( prim, parentPrim ) {
|
|
103
105
|
|
|
106
|
+
this.budget.check();
|
|
104
107
|
if ( ! this.isRenderable( prim ) ) return null;
|
|
105
108
|
|
|
106
109
|
const node = { name: prim.name };
|
|
@@ -199,6 +202,7 @@ export class USDToGLTF {
|
|
|
199
202
|
|
|
200
203
|
async convertMesh( prim, nodeIndex ) {
|
|
201
204
|
|
|
205
|
+
this.budget.check();
|
|
202
206
|
const node = this.json.nodes[ nodeIndex ];
|
|
203
207
|
const skel = this.skinningFor( prim );
|
|
204
208
|
|
|
@@ -289,7 +293,7 @@ export class USDToGLTF {
|
|
|
289
293
|
|
|
290
294
|
const indices = data.vertexCount < 65536 ? Uint16Array.from( group.indices ) : Uint32Array.from( group.indices );
|
|
291
295
|
const primitive = { attributes: { ...attributes }, indices: this.builder.addAccessor( indices, 'SCALAR', { target: 34963 } ) };
|
|
292
|
-
const material = ( group.subset && this.boundMaterial( group.subset ) ) || this.defaultMaterial( prim ) || this.boundMaterial( prim );
|
|
296
|
+
const material = ( group.subset && ( this.defaultMaterial( group.subset ) || this.boundMaterial( group.subset ) ) ) || this.defaultMaterial( prim ) || this.boundMaterial( prim );
|
|
293
297
|
|
|
294
298
|
if ( material ) primitive.material = await this.materials.convert( material, { doubleSided, vertexColors } );
|
|
295
299
|
else primitive.material = this.materials.defaultMaterial( data.constantColor, data.constantOpacity, vertexColors, doubleSided );
|