usd-gltf 0.0.1 → 0.1.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 +6 -3
- package/package.json +10 -4
- package/src/convert/usd-material.js +58 -34
- package/src/image/index.js +77 -0
- package/src/index.d.ts +71 -0
- package/src/usd/usda/reader.js +17 -1
- package/src/usd/usda/writer.js +12 -6
package/README.md
CHANGED
|
@@ -2,8 +2,6 @@
|
|
|
2
2
|
|
|
3
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
4
|
|
|
5
|
-
> Work in progress, not yet published to npm.
|
|
6
|
-
|
|
7
5
|
```
|
|
8
6
|
npm install usd-gltf
|
|
9
7
|
```
|
|
@@ -48,10 +46,15 @@ Current status:
|
|
|
48
46
|
|
|
49
47
|
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
48
|
|
|
49
|
+
To run it locally (needs Python with `pip install usd-core`, in a `.venv` here or on `PATH`):
|
|
50
|
+
|
|
51
51
|
```
|
|
52
|
-
|
|
52
|
+
npm run fetch-corpus
|
|
53
|
+
npm run conformance
|
|
53
54
|
```
|
|
54
55
|
|
|
56
|
+
The [Conformance workflow](.github/workflows/conformance.yml) runs the same check in CI.
|
|
57
|
+
|
|
55
58
|
## License
|
|
56
59
|
|
|
57
60
|
MIT
|
package/package.json
CHANGED
|
@@ -1,12 +1,15 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "usd-gltf",
|
|
3
|
-
"version": "0.0
|
|
3
|
+
"version": "0.1.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
|
}
|
|
@@ -6,7 +6,7 @@
|
|
|
6
6
|
// and lets textures be referenced directly when glTF can express the result, or be repacked
|
|
7
7
|
// (baked) into a new image when it can't, e.g. separate roughness and metallic maps.
|
|
8
8
|
|
|
9
|
-
import { decodeImageAsync, encodePNG, imageMimeType } from '../image/index.js';
|
|
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
12
|
|
|
@@ -476,6 +476,20 @@ export class MaterialConverter {
|
|
|
476
476
|
|
|
477
477
|
}
|
|
478
478
|
|
|
479
|
+
async isGrey( file ) {
|
|
480
|
+
|
|
481
|
+
try {
|
|
482
|
+
|
|
483
|
+
return isGreyscaleImage( await this.stage.readAsset( file ) );
|
|
484
|
+
|
|
485
|
+
} catch {
|
|
486
|
+
|
|
487
|
+
return false;
|
|
488
|
+
|
|
489
|
+
}
|
|
490
|
+
|
|
491
|
+
}
|
|
492
|
+
|
|
479
493
|
decode( file ) {
|
|
480
494
|
|
|
481
495
|
if ( ! this.decoded.has( file ) ) {
|
|
@@ -562,8 +576,10 @@ export class MaterialConverter {
|
|
|
562
576
|
const tex = textured[ 0 ].term.tex;
|
|
563
577
|
const sameTexture = textured.every( ( s ) => s.term.tex.file === tex.file && s.term.tex.uv.name === tex.uv.name && JSON.stringify( s.term.tex.uv.transform ) === JSON.stringify( tex.uv.transform ) );
|
|
564
578
|
|
|
565
|
-
// direct use: each textured slot reads its own channel
|
|
566
|
-
|
|
579
|
+
// direct use: each textured slot reads its own channel (any colour channel of a grey image
|
|
580
|
+
// reads the same value), no bias, and a scale expressible as a factor
|
|
581
|
+
const grey = sameTexture && textured.some( ( s ) => s.term.channel !== s.out ) && await this.isGrey( tex.file );
|
|
582
|
+
const direct = sameTexture && textured.every( ( s ) => ( s.term.channel === s.out || ( grey && s.term.channel < 3 && s.out < 3 ) ) && Math.abs( s.term.bias ) < 1e-6 && ( allowFactor || Math.abs( s.term.scale - 1 ) < 1e-6 ) ) &&
|
|
567
583
|
slots.every( ( s ) => ! s || s.term.tex || s.ignoreConst || ( allowFactor && s.constOk ) );
|
|
568
584
|
|
|
569
585
|
if ( direct ) {
|
|
@@ -588,47 +604,64 @@ export class MaterialConverter {
|
|
|
588
604
|
|
|
589
605
|
}
|
|
590
606
|
|
|
607
|
+
const key = 'baked:' + slots.map( ( s ) => s ? `${ s.term.tex?.file }#${ s.term.channel }*${ s.term.scale }+${ s.term.bias }` : '-' ).join( ',' );
|
|
608
|
+
let imageIndex = this.images.get( key )?.index;
|
|
609
|
+
|
|
610
|
+
if ( imageIndex === undefined ) {
|
|
611
|
+
|
|
612
|
+
const image = this.bake( slots, textured, sources );
|
|
613
|
+
imageIndex = this.builder.addImage( await encodePNGAsync( image ), 'image/png', imageName( tex.file ) + '_packed' );
|
|
614
|
+
this.images.set( key, { index: imageIndex, mimeType: 'image/png' } );
|
|
615
|
+
|
|
616
|
+
}
|
|
617
|
+
|
|
618
|
+
return { info: this.textureInfo( imageIndex, tex, key ), factors: slots.map( () => 1 ) };
|
|
619
|
+
|
|
620
|
+
}
|
|
621
|
+
|
|
622
|
+
// Writes each slot's term into its output channel, resampling sources to the largest size.
|
|
623
|
+
// Only the channels glTF reads are stored: grey for a lone R, RGB, or RGBA when alpha is used.
|
|
624
|
+
bake( slots, textured, sources ) {
|
|
625
|
+
|
|
591
626
|
const width = Math.max( ...sources.map( ( s ) => s.width ) );
|
|
592
627
|
const height = Math.max( ...sources.map( ( s ) => s.height ) );
|
|
593
|
-
const
|
|
628
|
+
const used = slots.map( ( s, c ) => ( s ? c : - 1 ) ).filter( ( c ) => c >= 0 );
|
|
629
|
+
const channels = used.length === 1 && used[ 0 ] === 0 ? 1 : used.includes( 3 ) ? 4 : 3;
|
|
630
|
+
const out = new Uint8Array( width * height * channels );
|
|
594
631
|
const srcOf = new Map( textured.map( ( s, i ) => [ s, sources[ i ] ] ) );
|
|
595
632
|
|
|
596
|
-
for ( let c = 0; c <
|
|
633
|
+
for ( let c = 0; c < channels; c ++ ) {
|
|
597
634
|
|
|
598
635
|
const slot = slots.find( ( s ) => s && s.out === c );
|
|
599
636
|
|
|
600
|
-
if ( ! slot ) {
|
|
637
|
+
if ( ! slot || ! slot.term.tex ) {
|
|
601
638
|
|
|
602
|
-
|
|
639
|
+
const v = slot ? Math.round( clamp01( slot.term.bias ) * 255 ) : 255;
|
|
640
|
+
for ( let i = c; i < out.length; i += channels ) out[ i ] = v;
|
|
603
641
|
continue;
|
|
604
642
|
|
|
605
643
|
}
|
|
606
644
|
|
|
607
645
|
const { term } = slot;
|
|
608
|
-
|
|
609
|
-
if ( ! term.tex ) {
|
|
610
|
-
|
|
611
|
-
const v = Math.round( clamp01( term.bias ) * 255 );
|
|
612
|
-
for ( let i = c; i < out.length; i += 4 ) out[ i ] = v;
|
|
613
|
-
continue;
|
|
614
|
-
|
|
615
|
-
}
|
|
616
|
-
|
|
617
646
|
const src = srcOf.get( slot );
|
|
618
647
|
const max = src.bitDepth === 16 ? 65535 : 255;
|
|
619
|
-
const sc = Math.min( term.channel, src.channels - 1 );
|
|
620
648
|
const alphaMissing = term.channel === 3 && src.channels < 4 && src.channels !== 2;
|
|
621
|
-
const channel = src.channels === 2 && term.channel === 3 ? 1 : src.channels <= 2 && term.channel < 3 ? 0 :
|
|
649
|
+
const channel = src.channels === 2 && term.channel === 3 ? 1 : src.channels <= 2 && term.channel < 3 ? 0 : Math.min( term.channel, src.channels - 1 );
|
|
622
650
|
|
|
623
|
-
|
|
651
|
+
// output = clamp( raw / max * scale + bias ) * 255, as one multiply and add per pixel
|
|
652
|
+
const k = term.scale * 255 / max, b = term.bias * 255 + 0.5;
|
|
653
|
+
const xs = new Int32Array( width );
|
|
654
|
+
for ( let x = 0; x < width; x ++ ) xs[ x ] = Math.min( src.width - 1, Math.floor( x * src.width / width ) ) * src.channels + channel;
|
|
655
|
+
const data = src.data, sw = src.width * src.channels;
|
|
624
656
|
|
|
625
|
-
|
|
657
|
+
for ( let y = 0, o = c; y < height; y ++ ) {
|
|
626
658
|
|
|
627
|
-
|
|
659
|
+
const row = Math.min( src.height - 1, Math.floor( y * src.height / height ) ) * sw;
|
|
628
660
|
|
|
629
|
-
|
|
630
|
-
|
|
631
|
-
|
|
661
|
+
for ( let x = 0; x < width; x ++, o += channels ) {
|
|
662
|
+
|
|
663
|
+
const v = alphaMissing ? 255 * term.scale + b : data[ row + xs[ x ] ] * k + b;
|
|
664
|
+
out[ o ] = v <= 0 ? 0 : v >= 255 ? 255 : v | 0;
|
|
632
665
|
|
|
633
666
|
}
|
|
634
667
|
|
|
@@ -636,16 +669,7 @@ export class MaterialConverter {
|
|
|
636
669
|
|
|
637
670
|
}
|
|
638
671
|
|
|
639
|
-
|
|
640
|
-
let imageIndex = this.images.get( key )?.index;
|
|
641
|
-
if ( imageIndex === undefined ) {
|
|
642
|
-
|
|
643
|
-
imageIndex = this.builder.addImage( encodePNG( { width, height, channels: 4, data: out } ), 'image/png', imageName( tex.file ) + '_packed' );
|
|
644
|
-
this.images.set( key, { index: imageIndex, mimeType: 'image/png' } );
|
|
645
|
-
|
|
646
|
-
}
|
|
647
|
-
|
|
648
|
-
return { info: this.textureInfo( imageIndex, tex, key ), factors: slots.map( () => 1 ) };
|
|
672
|
+
return { width, height, channels, data: out };
|
|
649
673
|
|
|
650
674
|
}
|
|
651
675
|
|
package/src/image/index.js
CHANGED
|
@@ -37,6 +37,34 @@ export function imageMimeType( bytes ) {
|
|
|
37
37
|
|
|
38
38
|
}
|
|
39
39
|
|
|
40
|
+
/**
|
|
41
|
+
* Whether a PNG or JPEG stores a single grey channel, from its header.
|
|
42
|
+
* @param {Uint8Array} bytes
|
|
43
|
+
*/
|
|
44
|
+
export function isGreyscaleImage( bytes ) {
|
|
45
|
+
|
|
46
|
+
bytes = toBytes( bytes );
|
|
47
|
+
const type = imageMimeType( bytes );
|
|
48
|
+
if ( type === 'image/png' ) return bytes[ 25 ] === 0 || bytes[ 25 ] === 4; // IHDR colour type
|
|
49
|
+
|
|
50
|
+
if ( type === 'image/jpeg' ) {
|
|
51
|
+
|
|
52
|
+
// walk the markers to the frame header, whose component count follows the dimensions
|
|
53
|
+
for ( let o = 2; o + 9 < bytes.length; ) {
|
|
54
|
+
|
|
55
|
+
if ( bytes[ o ] !== 0xFF ) return false;
|
|
56
|
+
const marker = bytes[ o + 1 ];
|
|
57
|
+
if ( marker >= 0xC0 && marker <= 0xCF && marker !== 0xC4 && marker !== 0xC8 && marker !== 0xCC ) return bytes[ o + 9 ] === 1;
|
|
58
|
+
o += 2 + ( ( bytes[ o + 2 ] << 8 ) | bytes[ o + 3 ] );
|
|
59
|
+
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
return false;
|
|
65
|
+
|
|
66
|
+
}
|
|
67
|
+
|
|
40
68
|
/**
|
|
41
69
|
* Decodes a PNG, JPEG or WebP. The format is sniffed from the bytes; mimeType is only a fallback.
|
|
42
70
|
* @param {Uint8Array|ArrayBuffer} bytes
|
|
@@ -57,6 +85,55 @@ export function decodeImage( bytes, mimeType ) {
|
|
|
57
85
|
|
|
58
86
|
const hasNativeDecoder = typeof createImageBitmap === 'function' && typeof OffscreenCanvas === 'function';
|
|
59
87
|
|
|
88
|
+
/**
|
|
89
|
+
* Encodes a PNG, with the platform's encoder where there is one (much faster) unless the image has
|
|
90
|
+
* partial transparency, which canvases store premultiplied.
|
|
91
|
+
* @param {{ width: number, height: number, channels: number, data: Uint8Array }} image 8 bit
|
|
92
|
+
* @returns {Promise<Uint8Array>}
|
|
93
|
+
*/
|
|
94
|
+
export async function encodePNGAsync( image ) {
|
|
95
|
+
|
|
96
|
+
const { width, height, channels, data } = image;
|
|
97
|
+
let opaque = channels !== 2 && channels !== 4;
|
|
98
|
+
if ( ! opaque ) {
|
|
99
|
+
|
|
100
|
+
opaque = true;
|
|
101
|
+
for ( let i = channels - 1; i < data.length; i += channels ) if ( data[ i ] !== 255 ) { opaque = false; break; }
|
|
102
|
+
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
if ( hasNativeDecoder && opaque && ( image.bitDepth ?? 8 ) === 8 ) {
|
|
106
|
+
|
|
107
|
+
try {
|
|
108
|
+
|
|
109
|
+
const rgba = new Uint8ClampedArray( width * height * 4 );
|
|
110
|
+
for ( let i = 0, n = width * height; i < n; i ++ ) {
|
|
111
|
+
|
|
112
|
+
const s = i * channels;
|
|
113
|
+
rgba[ i * 4 ] = data[ s ];
|
|
114
|
+
rgba[ i * 4 + 1 ] = data[ channels >= 3 ? s + 1 : s ];
|
|
115
|
+
rgba[ i * 4 + 2 ] = data[ channels >= 3 ? s + 2 : s ];
|
|
116
|
+
rgba[ i * 4 + 3 ] = 255;
|
|
117
|
+
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
const canvas = new OffscreenCanvas( width, height );
|
|
121
|
+
canvas.getContext( '2d' ).putImageData( new ImageData( rgba, width, height ), 0, 0 );
|
|
122
|
+
const blob = await canvas.convertToBlob( { type: 'image/png' } );
|
|
123
|
+
return new Uint8Array( await blob.arrayBuffer() );
|
|
124
|
+
|
|
125
|
+
} catch {
|
|
126
|
+
|
|
127
|
+
// fall through to the JavaScript encoder
|
|
128
|
+
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
return encodePNG( image );
|
|
134
|
+
|
|
135
|
+
}
|
|
136
|
+
|
|
60
137
|
/**
|
|
61
138
|
* Decodes with the platform's image decoder where there is one (browsers and workers), which is
|
|
62
139
|
* faster and supports more formats, falling back to the JavaScript decoders (Node).
|
package/src/index.d.ts
ADDED
|
@@ -0,0 +1,71 @@
|
|
|
1
|
+
/** Loads a file referenced by a relative path or URI (glTF buffers and images, USD sublayers and references). */
|
|
2
|
+
export type Resolver = ( path: string ) => Uint8Array | ArrayBuffer | Promise<Uint8Array | ArrayBuffer>;
|
|
3
|
+
|
|
4
|
+
/** A decoded image: 8 or 16 bit samples, interleaved, top row first. */
|
|
5
|
+
export interface DecodedImage {
|
|
6
|
+
width: number;
|
|
7
|
+
height: number;
|
|
8
|
+
channels: 1 | 2 | 3 | 4;
|
|
9
|
+
bitDepth: 8 | 16;
|
|
10
|
+
data: Uint8Array | Uint16Array;
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
/** Replaces the built in image decoding (the platform decoder in browsers, JavaScript decoders in Node). */
|
|
14
|
+
export type ImageDecoder = ( bytes: Uint8Array, mimeType?: string ) => DecodedImage | Promise<DecodedImage>;
|
|
15
|
+
|
|
16
|
+
export interface USDToGLBOptions {
|
|
17
|
+
/** Name of the input file, used to resolve relative paths. */
|
|
18
|
+
identifier?: string;
|
|
19
|
+
/** Needed when the USD references files outside a usdz package. */
|
|
20
|
+
resolver?: Resolver;
|
|
21
|
+
/** Variant selections, by prim path: { '/Root': { shadingVariant: 'red' } }, or by set name for every prim. */
|
|
22
|
+
variants?: Record<string, string | Record<string, string>>;
|
|
23
|
+
/** Write material variant sets as KHR_materials_variants (default true). */
|
|
24
|
+
materialVariants?: boolean;
|
|
25
|
+
imageDecoder?: ImageDecoder;
|
|
26
|
+
/** Receives non fatal problems, such as textures that could not be read. */
|
|
27
|
+
log?: ( message: string ) => void;
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
export interface GLBToUSDOptions {
|
|
31
|
+
/** Output encoding: a usdz package (default), or a single usda / usdc layer without its textures. */
|
|
32
|
+
format?: 'usdz' | 'usda' | 'usdc';
|
|
33
|
+
/** Needed for .gltf files with external buffers or images. */
|
|
34
|
+
resolver?: Resolver;
|
|
35
|
+
imageDecoder?: ImageDecoder;
|
|
36
|
+
log?: ( message: string ) => void;
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
/** Converts USD (usda, usdc or usdz, detected from the bytes) to GLB. */
|
|
40
|
+
export function usdToGLB( data: Uint8Array | ArrayBuffer, options?: USDToGLBOptions ): Promise<Uint8Array>;
|
|
41
|
+
|
|
42
|
+
/** Converts glTF (GLB, or .gltf JSON with a resolver for its files) to USD. */
|
|
43
|
+
export function glbToUSD( data: Uint8Array | ArrayBuffer, options?: GLBToUSDOptions ): Promise<Uint8Array>;
|
|
44
|
+
|
|
45
|
+
/** Like glbToUSD for usda / usdc output, also returning the texture files the layer references. */
|
|
46
|
+
export function glbToUSDFiles( data: Uint8Array | ArrayBuffer, options?: Omit<GLBToUSDOptions, 'format'> & { format?: 'usda' | 'usdc' } ): Promise<{ layer: Uint8Array, files: Array<{ name: string, data: Uint8Array }> }>;
|
|
47
|
+
|
|
48
|
+
/** Identifies a file from its leading bytes. */
|
|
49
|
+
export function detectFormat( data: Uint8Array | ArrayBuffer ): 'glb' | 'gltf' | 'usda' | 'usdc' | 'usdz' | null;
|
|
50
|
+
|
|
51
|
+
/** Splits a GLB into its JSON and binary chunk. */
|
|
52
|
+
export function readGLB( data: Uint8Array | ArrayBuffer ): { json: any, bin: Uint8Array | null };
|
|
53
|
+
|
|
54
|
+
/** Packs glTF JSON and a binary buffer into a GLB. */
|
|
55
|
+
export function writeGLB( json: object, bin?: Uint8Array | null ): Uint8Array;
|
|
56
|
+
|
|
57
|
+
/** The uncomposed contents of one USD layer; see src/usd/model.js for the value representation. */
|
|
58
|
+
export interface Layer {
|
|
59
|
+
identifier: string;
|
|
60
|
+
metadata: Record<string, any>;
|
|
61
|
+
root: any;
|
|
62
|
+
getPrimAtPath( path: string ): any;
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
export function readUSDA( source: string | Uint8Array, identifier?: string ): Layer;
|
|
66
|
+
export function writeUSDA( layer: Layer ): string;
|
|
67
|
+
export function readUSDC( data: Uint8Array | ArrayBuffer, identifier?: string ): Layer;
|
|
68
|
+
export function writeUSDC( layer: Layer ): Uint8Array;
|
|
69
|
+
|
|
70
|
+
/** Opens and composes a USD stage (references, payloads, sublayers, inherits, variants). */
|
|
71
|
+
export function openStage( data: Uint8Array, options?: { identifier?: string, resolver?: Resolver, variants?: USDToGLBOptions[ 'variants' ] } ): Promise<any>;
|
package/src/usd/usda/reader.js
CHANGED
|
@@ -358,7 +358,7 @@ function typeValue( raw, typeName ) {
|
|
|
358
358
|
|
|
359
359
|
if ( typeName === 'asset' ) return new AssetPath( raw instanceof AssetRef ? raw.path : String( raw ) );
|
|
360
360
|
if ( typeName === 'bool' ) return raw === true || raw === 1;
|
|
361
|
-
if ( typeName === 'string' || typeName === 'token' ) return raw instanceof Ident ? raw.name : raw;
|
|
361
|
+
if ( typeName === 'string' || typeName === 'token' || typeName === 'pathExpression' ) return raw instanceof Ident ? raw.name : raw;
|
|
362
362
|
if ( /^quat[hfd]$/.test( typeName ) ) return [ raw[ 1 ], raw[ 2 ], raw[ 3 ], raw[ 0 ] ];
|
|
363
363
|
if ( /^matrix[234]d$|^frame4d$/.test( typeName ) ) return raw.flat();
|
|
364
364
|
if ( raw instanceof Tuple ) return Array.from( raw );
|
|
@@ -524,6 +524,22 @@ function parseMetadata( lx, metadata, kind ) {
|
|
|
524
524
|
metadata.subLayers = layers.map( ( r ) => r.assetPath );
|
|
525
525
|
metadata.subLayerOffsets = layers.map( ( r ) => r.layerOffset );
|
|
526
526
|
|
|
527
|
+
} else if ( key === 'relocates' ) {
|
|
528
|
+
|
|
529
|
+
// { <source>: <target>, ... }; layer relocates use the layerRelocates field, as in usdc
|
|
530
|
+
const pairs = [];
|
|
531
|
+
lx.expect( '{' );
|
|
532
|
+
while ( ! lx.eat( '}' ) ) {
|
|
533
|
+
|
|
534
|
+
const from = lx.path();
|
|
535
|
+
lx.expect( ':' );
|
|
536
|
+
pairs.push( [ from, lx.path() ] );
|
|
537
|
+
lx.eat( ',' );
|
|
538
|
+
|
|
539
|
+
}
|
|
540
|
+
|
|
541
|
+
metadata[ kind === 'layer' ? 'layerRelocates' : 'relocates' ] = pairs;
|
|
542
|
+
|
|
527
543
|
} else if ( key === 'variants' ) {
|
|
528
544
|
|
|
529
545
|
metadata.variants = parseDictionary( lx );
|
package/src/usd/usda/writer.js
CHANGED
|
@@ -56,7 +56,7 @@ function formatScalar( v, base ) {
|
|
|
56
56
|
|
|
57
57
|
if ( v === null || v === undefined ) return 'None';
|
|
58
58
|
if ( base === 'bool' ) return v ? 'true' : 'false';
|
|
59
|
-
if ( base === 'string' || base === 'token' ) return quote( String( v ) );
|
|
59
|
+
if ( base === 'string' || base === 'token' || base === 'pathExpression' ) return quote( String( v ) );
|
|
60
60
|
if ( base === 'asset' ) return formatAsset( v );
|
|
61
61
|
if ( base === 'dictionary' ) return formatDictionary( v, '' );
|
|
62
62
|
|
|
@@ -142,12 +142,12 @@ function formatDictionary( dict, indent ) {
|
|
|
142
142
|
|
|
143
143
|
}
|
|
144
144
|
|
|
145
|
-
function formatLayerOffset( offset ) {
|
|
145
|
+
function formatLayerOffset( offset, customData = null, indent = '' ) {
|
|
146
146
|
|
|
147
|
-
if ( ! offset ) return '';
|
|
148
147
|
const parts = [];
|
|
149
|
-
if ( offset
|
|
150
|
-
if ( offset
|
|
148
|
+
if ( offset?.offset ) parts.push( `offset = ${ offset.offset }` );
|
|
149
|
+
if ( offset?.scale !== undefined && offset.scale !== 1 ) parts.push( `scale = ${ offset.scale }` );
|
|
150
|
+
if ( customData && Object.keys( customData ).length ) parts.push( `customData = ${ formatDictionary( customData, indent ) }` );
|
|
151
151
|
return parts.length ? ` (${ parts.join( '; ' ) })` : '';
|
|
152
152
|
|
|
153
153
|
}
|
|
@@ -157,7 +157,7 @@ function formatReference( r ) {
|
|
|
157
157
|
let s = '';
|
|
158
158
|
if ( r.assetPath ) s += formatAsset( r.assetPath );
|
|
159
159
|
if ( r.primPath ) s += `<${ r.primPath }>`;
|
|
160
|
-
return s + formatLayerOffset( r.layerOffset );
|
|
160
|
+
return s + formatLayerOffset( r.layerOffset, r.customData );
|
|
161
161
|
|
|
162
162
|
}
|
|
163
163
|
|
|
@@ -218,6 +218,11 @@ function formatMetadataEntries( metadata, indent, kind ) {
|
|
|
218
218
|
const offsets = metadata.subLayerOffsets || [];
|
|
219
219
|
lines.push( `${ indent }subLayers = [\n${ value.map( ( p, i ) => `${ indent } ${ formatAsset( p ) }${ formatLayerOffset( offsets[ i ] ) }` ).join( ',\n' ) }\n${ indent }]` );
|
|
220
220
|
|
|
221
|
+
} else if ( key === 'layerRelocates' || key === 'relocates' ) {
|
|
222
|
+
|
|
223
|
+
const pairs = value.map( ( [ a, b ] ) => `${ indent } <${ a }>: <${ b }>` );
|
|
224
|
+
lines.push( `${ indent }relocates = {\n${ pairs.join( ',\n' ) }\n${ indent }}` );
|
|
225
|
+
|
|
221
226
|
} else if ( key === 'subLayerOffsets' ) {
|
|
222
227
|
|
|
223
228
|
continue;
|
|
@@ -309,6 +314,7 @@ function writePrimContents( out, spec, indent ) {
|
|
|
309
314
|
|
|
310
315
|
for ( const prop of spec.properties.values() ) writeProperty( out, prop, indent );
|
|
311
316
|
|
|
317
|
+
if ( spec.metadata.propertyOrder ) out.push( `${ indent }reorder properties = [${ spec.metadata.propertyOrder.map( quote ).join( ', ' ) }]` );
|
|
312
318
|
if ( spec.metadata.primOrder ) out.push( `${ indent }reorder nameChildren = [${ spec.metadata.primOrder.map( quote ).join( ', ' ) }]` );
|
|
313
319
|
|
|
314
320
|
for ( const child of spec.children.values() ) {
|