usd-gltf 0.0.1 → 0.2.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 CHANGED
@@ -1,8 +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.
4
-
5
- > Work in progress, not yet published to npm.
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.
6
4
 
7
5
  ```
8
6
  npm install usd-gltf
@@ -37,21 +35,46 @@ USD to glTF composes the stage first (sublayers, references, payloads, inherits,
37
35
  - Material variant sets as `KHR_materials_variants`, animation variant sets as separate glTF animations
38
36
  - Cameras, UsdLux lights as `KHR_lights_punctual`, PointInstancers
39
37
 
40
- 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`) and `KHR_mesh_quantization` are decoded. Draco (`KHR_draco_mesh_compression`) and KTX2 textures (`KHR_texture_basisu`) are not: when an asset lists them in `extensionsRequired` the conversion fails with an error naming them, and when they are optional the uncompressed fallback data is used.
41
+
42
+ ## Limits
43
+
44
+ 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:
45
+
46
+ ```js
47
+ const usdz = await glbToUSD( glb, {
48
+ maxDecodedBytes: 512 * 2 ** 20, // decoded data held during the conversion
49
+ maxOutputBytes: 256 * 2 ** 20, // any output buffer: the layer, the usdz or glb, as it grows
50
+ signal: AbortSignal.timeout( 30000 ), // cancellation
51
+ } );
52
+ ```
53
+
54
+ - `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.
55
+ - `maxOutputBytes` is checked as the usdc, usda and glb writers grow and before the final usdz or glb buffer is allocated.
56
+ - `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.
57
+
58
+ 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.
41
59
 
42
60
  ## Conformance
43
61
 
44
62
  Current status:
45
63
 
46
64
  - 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.)
47
- - 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.
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) and embedded `.gltf` (32 models) where the sample set has those versions: 302 conversions in all.
48
66
 
49
67
  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.
50
68
 
69
+ To run it locally (needs Python with `pip install usd-core`, in a `.venv` here or on `PATH`):
70
+
51
71
  ```
52
- node scripts/fetch-corpus.js
72
+ npm run fetch-corpus
73
+ npm run conformance
53
74
  ```
54
75
 
76
+ The [Conformance workflow](.github/workflows/conformance.yml) runs the same check in CI.
77
+
55
78
  ## License
56
79
 
57
80
  MIT
package/package.json CHANGED
@@ -1,12 +1,15 @@
1
1
  {
2
2
  "name": "usd-gltf",
3
- "version": "0.0.1",
3
+ "version": "0.2.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",
7
7
  "module": "src/index.js",
8
8
  "exports": {
9
- ".": "./src/index.js"
9
+ ".": {
10
+ "types": "./src/index.d.ts",
11
+ "default": "./src/index.js"
12
+ }
10
13
  },
11
14
  "bin": {
12
15
  "usd-gltf": "bin/usd-gltf.js"
@@ -16,7 +19,9 @@
16
19
  "bin"
17
20
  ],
18
21
  "scripts": {
19
- "test": "node --test test/*.test.js"
22
+ "test": "node --test test/*.test.js",
23
+ "conformance": "node scripts/conformance.js",
24
+ "fetch-corpus": "node scripts/fetch-corpus.js"
20
25
  },
21
26
  "engines": {
22
27
  "node": ">=18"
@@ -45,5 +50,6 @@
45
50
  "devDependencies": {
46
51
  "gltf-validator": "^2.0.0-dev.3.10",
47
52
  "meshoptimizer": "^1.3.0"
48
- }
53
+ },
54
+ "types": "src/index.d.ts"
49
55
  }
@@ -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
- return ext.KHR_texture_basisu?.source ?? ext.EXT_texture_webp?.source ?? ext.EXT_texture_avif?.source ?? texture.source;
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
- mesh.primitives.forEach( ( primitive, p ) => {
682
+ groups.forEach( ( group ) => {
670
683
 
671
- const name = sanitize( mesh.primitives.length > 1 ? `${ mesh.name || 'mesh' }_${ p }` : mesh.name, `mesh_${ node.mesh }` );
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 && ! skinned ) {
688
+ if ( instancing ) {
674
689
 
675
- this.convertInstancer( prim, name, primitive, instancing, nodeIndex, p );
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
- this.meshPrims.set( `${ nodeIndex }/${ p }`, meshPrim );
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
- * Writes one glTF primitive as a Mesh, Points or BasisCurves prim.
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
- let tris = indices;
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
- const out = [];
758
- for ( let i = 0; i + 2 < indices.length; i ++ ) {
800
+ if ( parts.length > 1 ) {
759
801
 
760
- if ( i % 2 === 0 ) out.push( indices[ i ], indices[ i + 1 ], indices[ i + 2 ] );
761
- else out.push( indices[ i + 1 ], indices[ i ], indices[ i + 2 ] );
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
- tris = tris.subarray( 0, tris.length - ( tris.length % 3 ) );
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[]', positions );
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( positions ) );
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, null, 'vertex' );
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 ( joints ) this.writeSkinWeights( prim, attrs, joints );
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.meshPrims ) {
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 ];