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 +29 -6
- package/package.json +10 -4
- package/src/convert/gltf-to-usd.js +166 -46
- package/src/convert/usd-material.js +65 -36
- package/src/convert/usd-to-gltf.js +8 -4
- package/src/gltf/document.js +107 -18
- package/src/gltf/glb.js +2 -2
- package/src/image/index.js +129 -1
- package/src/image/inflate.js +14 -12
- package/src/image/png.js +1 -1
- package/src/index.d.ts +92 -0
- package/src/index.js +33 -14
- package/src/limits.js +101 -0
- package/src/usd/stage.js +13 -9
- package/src/usd/usda/reader.js +17 -1
- package/src/usd/usda/writer.js +28 -7
- 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,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
|
-
|
|
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
|
|
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
|
-
".":
|
|
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
|
-
|
|
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 ];
|