lecodes-assets 0.2.0 → 0.2.1
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 +561 -449
- package/dist/cli.js +5632 -521
- package/dist/index.js +5647 -526
- package/package.json +13 -3
- package/src/bvh/fromDocument.ts +136 -0
- package/src/bvh/toDocument.ts +202 -0
- package/src/cli.ts +1039 -0
- package/src/doctor/doctor.ts +241 -0
- package/src/doctor/lightmapUv.ts +452 -0
- package/src/doctor/lod.ts +177 -0
- package/src/doctor/normalizeScale.ts +220 -0
- package/src/doctor/reduceGlbBones.ts +218 -0
- package/src/doctor/shading.ts +95 -0
- package/src/doctor/transcodeKtx2.ts +124 -0
- package/src/doctor/transcodeTextures.ts +101 -0
- package/src/doctor/validateGlb.ts +264 -0
- package/src/fbx/convert.ts +327 -0
- package/src/fbx/read.ts +109 -0
- package/src/fbx/toDocument.ts +209 -0
- package/src/filmstrip.ts +263 -0
- package/src/index.ts +17 -0
- package/src/mergeSkins.ts +117 -0
- package/src/probe.ts +167 -0
- package/src/props.ts +137 -0
- package/src/retarget/automap.ts +276 -0
- package/src/retarget/ghost.ts +23 -0
- package/src/retarget/heading.ts +171 -0
- package/src/retarget/humanoid.ts +561 -0
- package/src/retarget/inPlace.ts +130 -0
- package/src/retarget/marks.ts +581 -0
- package/src/retarget/mirror.ts +0 -0
- package/src/retarget/retarget.ts +2272 -0
- package/src/retarget/rig.ts +542 -0
- package/src/retarget/unityAnim.ts +171 -0
- package/src/retarget/writeMarks.ts +132 -0
- package/src/sky/analyze.ts +136 -0
- package/src/sky/cmgen.ts +71 -0
- package/src/sky/index.ts +151 -0
- package/src/terrainPack.ts +169 -0
- package/src/textures/resolveImage.ts +202 -0
- package/src/unity/materials.ts +593 -0
- package/src/unity/png.ts +75 -0
- package/src/unity/prefabs.ts +80 -0
- package/src/unity/scene.ts +363 -0
- package/src/unity/unpack.ts +130 -0
- package/src/unreal/anim.ts +284 -0
- package/src/unreal/clipSource.ts +25 -0
- package/src/unreal/convert.ts +67 -0
- package/src/unreal/convertAsset.ts +81 -0
- package/src/unreal/index.ts +114 -0
- package/src/unreal/list.ts +113 -0
- package/src/unreal/materials.ts +291 -0
- package/src/unreal/mesh.ts +205 -0
- package/src/unreal/oodle.ts +153 -0
- package/src/unreal/package.ts +659 -0
- package/src/unreal/probe.ts +144 -0
- package/src/unreal/resolve.ts +51 -0
- package/src/unreal/rig.ts +149 -0
- package/src/unreal/sequencer.ts +193 -0
- package/src/unreal/skeleton.ts +74 -0
- package/src/unreal/texture.ts +202 -0
- package/src/unreal/toDocument.ts +360 -0
- package/src/unreal/versions.ts +100 -0
- package/src/wasm/fbxReader.js +14 -0
- package/src/wasm/lightmapUv.js +14 -0
package/README.md
CHANGED
|
@@ -1,449 +1,561 @@
|
|
|
1
|
-
# lecodes-assets — the LeCodes asset pipeline
|
|
2
|
-
|
|
3
|
-
FBX → GLB converter + GLB doctor, one package on one glTF-Transform `Document`. Reachable as
|
|
4
|
-
`lecodes assets …` (optional peer of the `lecodes` CLI) or `bun src/cli.ts …` in the monorepo.
|
|
5
|
-
Plan/rationale: [docs/animation-plan.md §2.9](../../docs/animation-plan.md).
|
|
6
|
-
|
|
7
|
-
Install it the same way you installed the CLI — the peer is loaded by a bare `import` that resolves
|
|
8
|
-
upward from the CLI's own file, so a global `lecodes` only sees a global peer:
|
|
9
|
-
|
|
10
|
-
```sh
|
|
11
|
-
npm install -g lecodes-assets
|
|
12
|
-
```
|
|
13
|
-
|
|
14
|
-
KTX2 (`--ktx2`) additionally needs KTX-Software's `toktx` on PATH; `sharp` is an optional dependency
|
|
15
|
-
used to normalize exotic texture formats.
|
|
16
|
-
|
|
17
|
-
```
|
|
18
|
-
lecodes assets convert "Erika Archer.fbx" --clips Idle.fbx Walking.fbx "Fast Run.fbx" --drop-model-clips -o hero.glb
|
|
19
|
-
lecodes assets convert hero.glb --clips Jump.fbx # add clips to an existing GLB
|
|
20
|
-
lecodes assets convert hero.fbx --fix # + doctor passes (bone cap, texture normalize)
|
|
21
|
-
lecodes assets convert hero.fbx --ktx2 # + textures → KTX2 (needs KTX-Software toktx)
|
|
22
|
-
lecodes assets probe hero.fbx | hero.glb # skeleton / clips / meshes / textures / bind sanity / world size
|
|
23
|
-
lecodes assets doctor hero.glb [--fix] [--ktx2] [--dump] # the old tools/glb-reduce
|
|
24
|
-
lecodes assets doctor hero.glb --fix-scale # bake a unit fix (see Scale below)
|
|
25
|
-
lecodes assets doctor props/*.glb --lightmap-uv -o props/ # + TEXCOORD_1 lightmap UVs (xatlas) for `lecodes lightmap bake`
|
|
26
|
-
lecodes assets retarget fox.glb --from wolf.fbx --map auto # clips from a DIFFERENT skeleton (map from skeleton shape)
|
|
27
|
-
lecodes assets avatar girl.glb --meta SK_Girl.fbx.meta # a humanoid's bones in the Unity/VRM canon (vs Unity's own map)
|
|
28
|
-
lecodes assets unpack pack.unitypackage -o out [--only re …] # stream a .unitypackage: unity-index.txt + assets (+ .meta)
|
|
29
|
-
lecodes assets convert Barrel_v1.FBX --list-nodes # multi-variant prop file: what is in it
|
|
30
|
-
lecodes assets convert Barrel_v1.FBX --keep Barrel_v1_LD1 -o barrel.glb # one variant, re-origined on the ground
|
|
31
|
-
lecodes assets convert Barrel_v1.FBX --split props/ # every top-level node → its own GLB
|
|
32
|
-
lecodes assets convert Body.FBX --merge-skins Shirt.FBX Gloves.FBX --clips … -o hero.glb # modular character → ONE skeleton
|
|
33
|
-
lecodes assets probe hero.glb --filmstrip # a CPU-skinned PNG strip per clip (no GPU)
|
|
34
|
-
lecodes assets scene Map_v8.unity -o level.json # Unity scene → placed prefabs with world TRS
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
`
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
`
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
lecodes assets
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
`
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
`
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
lecodes assets retarget fox
|
|
119
|
-
lecodes assets retarget
|
|
120
|
-
lecodes assets retarget
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
`
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
"
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
the
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
|
|
231
|
-
|
|
232
|
-
|
|
233
|
-
|
|
234
|
-
|
|
235
|
-
|
|
236
|
-
|
|
237
|
-
|
|
238
|
-
lecodes assets
|
|
239
|
-
|
|
240
|
-
|
|
241
|
-
|
|
242
|
-
|
|
243
|
-
|
|
244
|
-
|
|
245
|
-
|
|
246
|
-
|
|
247
|
-
|
|
248
|
-
|
|
249
|
-
|
|
250
|
-
|
|
251
|
-
|
|
252
|
-
|
|
253
|
-
|
|
254
|
-
|
|
255
|
-
|
|
256
|
-
|
|
257
|
-
|
|
258
|
-
|
|
259
|
-
|
|
260
|
-
|
|
261
|
-
|
|
262
|
-
|
|
263
|
-
|
|
264
|
-
|
|
265
|
-
|
|
266
|
-
|
|
267
|
-
|
|
268
|
-
|
|
269
|
-
|
|
270
|
-
|
|
271
|
-
|
|
272
|
-
|
|
273
|
-
|
|
274
|
-
|
|
275
|
-
|
|
276
|
-
|
|
277
|
-
|
|
278
|
-
|
|
279
|
-
|
|
280
|
-
|
|
281
|
-
|
|
282
|
-
|
|
283
|
-
|
|
284
|
-
the
|
|
285
|
-
|
|
286
|
-
|
|
287
|
-
|
|
288
|
-
|
|
289
|
-
|
|
290
|
-
|
|
291
|
-
|
|
292
|
-
|
|
293
|
-
|
|
294
|
-
|
|
295
|
-
|
|
296
|
-
|
|
297
|
-
|
|
298
|
-
|
|
299
|
-
|
|
300
|
-
|
|
301
|
-
|
|
302
|
-
|
|
303
|
-
|
|
304
|
-
|
|
305
|
-
|
|
306
|
-
|
|
307
|
-
|
|
308
|
-
|
|
309
|
-
|
|
310
|
-
|
|
311
|
-
|
|
312
|
-
|
|
313
|
-
|
|
314
|
-
|
|
315
|
-
|
|
316
|
-
|
|
317
|
-
|
|
318
|
-
|
|
319
|
-
|
|
320
|
-
|
|
321
|
-
|
|
322
|
-
|
|
323
|
-
|
|
324
|
-
|
|
325
|
-
|
|
326
|
-
|
|
327
|
-
|
|
328
|
-
|
|
329
|
-
|
|
330
|
-
|
|
331
|
-
|
|
332
|
-
|
|
333
|
-
|
|
334
|
-
|
|
335
|
-
|
|
336
|
-
|
|
337
|
-
|
|
338
|
-
|
|
339
|
-
|
|
340
|
-
|
|
341
|
-
|
|
342
|
-
|
|
343
|
-
|
|
344
|
-
|
|
345
|
-
|
|
346
|
-
|
|
347
|
-
|
|
348
|
-
|
|
349
|
-
|
|
350
|
-
|
|
351
|
-
|
|
352
|
-
|
|
353
|
-
|
|
354
|
-
|
|
355
|
-
|
|
356
|
-
|
|
357
|
-
|
|
358
|
-
|
|
359
|
-
|
|
360
|
-
|
|
361
|
-
|
|
362
|
-
|
|
363
|
-
|
|
364
|
-
|
|
365
|
-
|
|
366
|
-
|
|
367
|
-
|
|
368
|
-
|
|
369
|
-
|
|
370
|
-
|
|
371
|
-
|
|
372
|
-
|
|
373
|
-
|
|
374
|
-
|
|
375
|
-
|
|
376
|
-
|
|
377
|
-
|
|
378
|
-
|
|
379
|
-
|
|
380
|
-
|
|
381
|
-
|
|
382
|
-
|
|
383
|
-
|
|
384
|
-
|
|
385
|
-
|
|
386
|
-
|
|
387
|
-
|
|
388
|
-
|
|
389
|
-
|
|
390
|
-
|
|
391
|
-
|
|
392
|
-
|
|
393
|
-
|
|
394
|
-
|
|
395
|
-
|
|
396
|
-
|
|
397
|
-
|
|
398
|
-
|
|
399
|
-
|
|
400
|
-
|
|
401
|
-
|
|
402
|
-
|
|
403
|
-
|
|
404
|
-
|
|
405
|
-
|
|
406
|
-
|
|
407
|
-
|
|
408
|
-
|
|
409
|
-
|
|
410
|
-
|
|
411
|
-
|
|
412
|
-
|
|
413
|
-
|
|
414
|
-
|
|
415
|
-
|
|
416
|
-
|
|
417
|
-
|
|
418
|
-
|
|
419
|
-
|
|
420
|
-
|
|
421
|
-
|
|
422
|
-
|
|
423
|
-
|
|
424
|
-
|
|
425
|
-
|
|
426
|
-
|
|
427
|
-
|
|
428
|
-
|
|
429
|
-
|
|
430
|
-
|
|
431
|
-
|
|
432
|
-
|
|
433
|
-
|
|
434
|
-
|
|
435
|
-
|
|
436
|
-
|
|
437
|
-
|
|
438
|
-
|
|
439
|
-
|
|
440
|
-
|
|
441
|
-
|
|
442
|
-
|
|
443
|
-
|
|
444
|
-
|
|
445
|
-
|
|
446
|
-
|
|
447
|
-
|
|
448
|
-
|
|
449
|
-
|
|
1
|
+
# lecodes-assets — the LeCodes asset pipeline
|
|
2
|
+
|
|
3
|
+
FBX → GLB converter + GLB doctor, one package on one glTF-Transform `Document`. Reachable as
|
|
4
|
+
`lecodes assets …` (optional peer of the `lecodes` CLI) or `bun src/cli.ts …` in the monorepo.
|
|
5
|
+
Plan/rationale: [docs/animation-plan.md §2.9](../../docs/animation-plan.md).
|
|
6
|
+
|
|
7
|
+
Install it the same way you installed the CLI — the peer is loaded by a bare `import` that resolves
|
|
8
|
+
upward from the CLI's own file, so a global `lecodes` only sees a global peer:
|
|
9
|
+
|
|
10
|
+
```sh
|
|
11
|
+
npm install -g lecodes-assets
|
|
12
|
+
```
|
|
13
|
+
|
|
14
|
+
KTX2 (`--ktx2`) additionally needs KTX-Software's `toktx` on PATH; `sharp` is an optional dependency
|
|
15
|
+
used to normalize exotic texture formats.
|
|
16
|
+
|
|
17
|
+
```
|
|
18
|
+
lecodes assets convert "Erika Archer.fbx" --clips Idle.fbx Walking.fbx "Fast Run.fbx" --drop-model-clips -o hero.glb
|
|
19
|
+
lecodes assets convert hero.glb --clips Jump.fbx # add clips to an existing GLB
|
|
20
|
+
lecodes assets convert hero.fbx --fix # + doctor passes (bone cap, texture normalize)
|
|
21
|
+
lecodes assets convert hero.fbx --ktx2 # + textures → KTX2 (needs KTX-Software toktx)
|
|
22
|
+
lecodes assets probe hero.fbx | hero.glb # skeleton / clips / meshes / textures / bind sanity / world size
|
|
23
|
+
lecodes assets doctor hero.glb [--fix] [--ktx2] [--dump] # the old tools/glb-reduce
|
|
24
|
+
lecodes assets doctor hero.glb --fix-scale # bake a unit fix (see Scale below)
|
|
25
|
+
lecodes assets doctor props/*.glb --lightmap-uv -o props/ # + TEXCOORD_1 lightmap UVs (xatlas) for `lecodes lightmap bake`
|
|
26
|
+
lecodes assets retarget fox.glb --from wolf.fbx --map auto # clips from a DIFFERENT skeleton (map from skeleton shape)
|
|
27
|
+
lecodes assets avatar girl.glb --meta SK_Girl.fbx.meta # a humanoid's bones in the Unity/VRM canon (vs Unity's own map)
|
|
28
|
+
lecodes assets unpack pack.unitypackage -o out [--only re …] # stream a .unitypackage: unity-index.txt + assets (+ .meta)
|
|
29
|
+
lecodes assets convert Barrel_v1.FBX --list-nodes # multi-variant prop file: what is in it
|
|
30
|
+
lecodes assets convert Barrel_v1.FBX --keep Barrel_v1_LD1 -o barrel.glb # one variant, re-origined on the ground
|
|
31
|
+
lecodes assets convert Barrel_v1.FBX --split props/ # every top-level node → its own GLB
|
|
32
|
+
lecodes assets convert Body.FBX --merge-skins Shirt.FBX Gloves.FBX --clips … -o hero.glb # modular character → ONE skeleton
|
|
33
|
+
lecodes assets probe hero.glb --filmstrip # a CPU-skinned PNG strip per clip (no GPU)
|
|
34
|
+
lecodes assets scene Map_v8.unity -o level.json # Unity scene → placed prefabs with world TRS
|
|
35
|
+
lecodes assets unreal ls "Stairs Set/StairsSet" # what an UNREAL pack holds (see below)
|
|
36
|
+
lecodes assets unreal tex T_Manny_01_D.uasset -o manny_d.png # a texture's SOURCE image (needs Oodle, see below)
|
|
37
|
+
lecodes assets convert SKM_Manny.uasset --clips Anims/**/*.uasset -o manny.glb # Unreal mesh + clips → GLB
|
|
38
|
+
```
|
|
39
|
+
|
|
40
|
+
## Props, modular characters, scenes — the Unity-pack workflow
|
|
41
|
+
|
|
42
|
+
**Multi-variant prop files.** Packs put every LOD, colour set and arrangement of a prop in one FBX
|
|
43
|
+
(`Barrel_v1.FBX` = 15 meshes). `--list-nodes [depth]` prints the scene's nodes with mesh/triangle
|
|
44
|
+
counts and world sizes; `--keep <node> …` keeps only those subtrees (ancestors stay as the path,
|
|
45
|
+
descendants come along) and **re-origins** the result — footprint centre at X/Z = 0, lowest vertex at
|
|
46
|
+
Y = 0 — so `Model.load` places the prop exactly where the node is (`--no-ground` keeps the file's
|
|
47
|
+
own placement); `--split [dir]` writes one re-origined GLB per top-level node. Both run before the
|
|
48
|
+
doctor passes, so `--keep X --ktx2 --max-size 1024` is the whole prop pipeline.
|
|
49
|
+
|
|
50
|
+
**Modular characters.** Body, shirt, gloves and boots each arrive as their own skinned FBX over the
|
|
51
|
+
same skeleton. `--merge-skins b.fbx c.glb …` moves the donors' skinned meshes onto the model's skin
|
|
52
|
+
(joints matched by name — exact, then the part after the last `:`/`|`, the Animator's own rule) and
|
|
53
|
+
drops their skeleton copies. That last part matters: a hand merge that leaves the donor skeleton in
|
|
54
|
+
gives every bone name a plain-node twin, and a name-bound `Animator` drives the twin while the mesh
|
|
55
|
+
freezes in its rest pose. `probe` and `doctor` now flag duplicate node names, and the doctor marks
|
|
56
|
+
the joint-shadowing case as BLOCKING. Donor bones the base lacks are reported with the skin weight
|
|
57
|
+
that sat on them (it moves to the root).
|
|
58
|
+
|
|
59
|
+
**Checking a clip without a GPU.** `probe` prints, per clip and skeleton root, how far the root
|
|
60
|
+
travels (root motion) and how far its first frame is from the rest pose — a root that starts ~90°
|
|
61
|
+
off is the double-axis-turn symptom of a clip from another export. `probe --filmstrip [out.png]`
|
|
62
|
+
renders a software-skinned PNG strip of every clip (`--clip` for one, `--frames N`, `--view
|
|
63
|
+
side|front|top|threequarter|lowfront`, `--bones` for the skeleton overlay, `--cell` px per frame);
|
|
64
|
+
`tools/filmstrip.ts` is the same renderer with reference frames from other files.
|
|
65
|
+
|
|
66
|
+
**Unity scenes.** `scene Map.unity [-o level.json]` reads a text-serialised scene: every
|
|
67
|
+
`PrefabInstance` (source prefab GUID, the position/rotation/scale/name/active overrides, its parent)
|
|
68
|
+
and every `MeshFilter` object, composes the scene's own `Transform` hierarchy on top (groups like
|
|
69
|
+
`Static/Roads`), and writes the instances with their **world** TRS plus a prefab table — path, use
|
|
70
|
+
count, and a guess at the FBX + `--keep` node that renders it (the FBX from the prefab's folder /
|
|
71
|
+
`Source/` whose stem prefixes the prefab name; most packs ship their prefabs binary-serialised, so
|
|
72
|
+
the prefab's own contents are not read). Frame: Unity is left-handed and its FBX importer mirrors X,
|
|
73
|
+
so the default undoes that — position `(−x, y, z)`, quaternion `(x, −y, −z, w)` — `--handedness z`
|
|
74
|
+
mirrors Z instead (content authored inside Unity), `none` keeps Unity's numbers. Subsets:
|
|
75
|
+
`--filter <re>` (prefab path / name / group), `--group <name>`, `--region x0,z0,x1,z1` (Unity
|
|
76
|
+
metres), `--include-inactive`. A parent that sits inside a binary prefab resolves to that prefab's
|
|
77
|
+
root and the instance is marked `approximate`. The output is plain JSON:
|
|
78
|
+
`{ prefabs: { guid: { path, name, fbx, node, count } }, instances: [ { name, prefab, group,
|
|
79
|
+
position, rotation, scale } ] }` — one `Model.load` per prefab, one `clone()` per instance.
|
|
80
|
+
|
|
81
|
+
## Unity packs — `unpack` + materials from `.mat`
|
|
82
|
+
|
|
83
|
+
```
|
|
84
|
+
lecodes assets unpack "Tactical FPS Animations.unitypackage" -o D:/Unity/tactical # everything
|
|
85
|
+
lecodes assets unpack pack.unitypackage -o out --only 'Meshes/' 'Animations/' 'Materials/' # regex subset
|
|
86
|
+
lecodes assets unpack pack.unitypackage -o out --list --only '\.wav$' # just list
|
|
87
|
+
lecodes assets convert out/Assets/Pack/Weapons/SKM_TR-15.FBX --clips … --ktx2 -o tr15.glb # .mat-aware
|
|
88
|
+
```
|
|
89
|
+
|
|
90
|
+
A `.unitypackage` is a gzip'd tar of one folder per asset GUID. `unpack` streams it twice (flat
|
|
91
|
+
memory on multi-GB packs): pass 1 writes `<out>/unity-index.txt` (GUID → path), pass 2 writes the
|
|
92
|
+
matching assets + their `.meta` sidecars under `<out>/Assets/…`.
|
|
93
|
+
|
|
94
|
+
`convert` then reads the **Unity `.mat` files** found next to (or one folder above) the FBX and
|
|
95
|
+
rewrites the glTF materials from them — an FBX exported for Unity usually references only a legacy
|
|
96
|
+
base colour and a normal map, while metallic / roughness / occlusion / emission and the alpha mode
|
|
97
|
+
live in the material. Matching is by name modulo `M_`/`MI_`/`MAT_` prefixes, then by containment
|
|
98
|
+
(`MI_Tshirt` ↔ `M_Shirt`), then by a shared base texture (`MI_Skin` ↔ `M_Body` through
|
|
99
|
+
`TX_Body_BaseColor`). Texture GUIDs resolve through `unity-index.txt` (found walking up from the
|
|
100
|
+
FBX, or `--unity-index`), else through `.meta` sidecars of a real Unity project. Slots understood:
|
|
101
|
+
`_BaseMap/_MainTex/_BaseColor/_BC`, `_BumpMap/_Normal`, a ready `_ORM`, HDRP/URP `_MaskMap`
|
|
102
|
+
(R metal, G AO, A smooth), Standard `_MetallicGlossMap` (R metal, A smooth), separate `_Metallic`,
|
|
103
|
+
`_Roughness` / `_Smoothness`, `_OcclusionMap`, `_EmissionMap` (+ `_EMISSION` keyword / colour),
|
|
104
|
+
`_Surface`/`_Mode`/`_AlphaClip`/`_Cutoff`/`_Cull`. Everything that is not already one texture is
|
|
105
|
+
**packed the glTF way** (R = AO, G = roughness, B = metallic; PNG/TGA/BMP decoded in-process, other
|
|
106
|
+
formats through `sharp`) — one metallic-roughness(+occlusion) texture per material. `--no-unity`
|
|
107
|
+
keeps the FBX references; `--unity-mats <dir>` looks elsewhere. Only YAML-serialised `.mat` files are
|
|
108
|
+
readable (Unity "Force Text", the asset-store default since 2017); binary ones are listed and skipped.
|
|
109
|
+
|
|
110
|
+
## Retarget — clips from a different skeleton
|
|
111
|
+
|
|
112
|
+
`convert --clips` binds tracks by BONE NAME and copies them verbatim; that only works when both
|
|
113
|
+
files came from the same rig (the Mixamo case). `retarget` drives one rig with a completely
|
|
114
|
+
different rig's clips — different bone names, different chain lengths, different proportions,
|
|
115
|
+
different local axis conventions, even different facing:
|
|
116
|
+
|
|
117
|
+
```
|
|
118
|
+
lecodes assets retarget fox.glb --from Wolf_Animation.fbx --map auto --clips wolf_walk wolf_run -o fox-anim.glb
|
|
119
|
+
lecodes assets retarget giraffe.glb --from Deer_Animation.fbx --auto-map # writes giraffe-from-Deer_Animation.map.json to edit
|
|
120
|
+
lecodes assets retarget giraffe.glb --from Deer_Animation.fbx --map giraffe-from-Deer_Animation.map.json -o giraffe-anim.glb
|
|
121
|
+
lecodes assets retarget fox-anim.glb --from Wolf_Howl.fbx --map … --keep-clips --clip-names howl
|
|
122
|
+
lecodes assets retarget fox.glb --from Wolf_Animation.fbx --skeletons # both bone lists, to write a map by hand
|
|
123
|
+
lecodes assets retarget fox.glb --from Cat.fbx --map auto --anim Cat_Sleep.anim # donor clips that live outside the FBX
|
|
124
|
+
```
|
|
125
|
+
|
|
126
|
+
**The map** is target-bone → donor-bone; `root` names the bone that receives root motion. `--map auto`
|
|
127
|
+
/ `--auto-map` derive it from the two skeletons' SHAPE — root, spine, neck, head, jaw, ears, tail and
|
|
128
|
+
the four legs (side + front/back) are read off the bind pose, then paired chain by chain with bones
|
|
129
|
+
matched by arc length (names only break ties when they agree: `backAnkle`↔`Ankle_L`). It works on
|
|
130
|
+
`joint1…joint42` rigs and prints exactly what it paired — read it before trusting it. Optional keys:
|
|
131
|
+
|
|
132
|
+
```json
|
|
133
|
+
{ "root": "Hips",
|
|
134
|
+
"bones": { "Hips": "Root_M", "chest": "Chest_M", "backleg": "Hip_L", "backleg0": "Knee_L", "…": "…" },
|
|
135
|
+
"align": { "chest": "keep", "tail": "direction", "head": [ "headend" ] }, // per-bone reference policy (see below)
|
|
136
|
+
"legs": [ "backleg2", "R_backleg2", "frontleg2", "R_frontleg2" ], // feet, when auto-detection picks wrong
|
|
137
|
+
"donorRef": "auto" | "bind" | { "clip": "wolf_idle", "time": 0 } }
|
|
138
|
+
```
|
|
139
|
+
|
|
140
|
+
**How it works.** Everything happens in WORLD space, where a rotation means the same thing in both
|
|
141
|
+
rigs: `ΔR(t) = Rdonor(t)·Rdonor_ref⁻¹` is applied to the target's own reference orientation and
|
|
142
|
+
converted back to local against the already-retargeted parent. That transfer is exact — the thing
|
|
143
|
+
that decides quality is what the two REFERENCE poses are and what happens where proportions differ:
|
|
144
|
+
|
|
145
|
+
- **Donor reference** (`donorRef`, default `auto`). An exporter's bind is often a posture the clips
|
|
146
|
+
never visit — the polyperfect wolf binds upright on vertical thighs and walks 10% lower with its
|
|
147
|
+
thighs 30° forward; measured from that bind every clip reads as "crouch and lean". When the two
|
|
148
|
+
binds put the legs differently (>12°: different conventions), the donor is referenced to the MEAN
|
|
149
|
+
pose of its walk (else idle, else the longest clip) — its actual neutral posture. When the binds
|
|
150
|
+
agree, bind-to-bind is the faithful transfer (an identity retarget reproduces the donor exactly).
|
|
151
|
+
`--donor-ref bind|auto|<clip>` overrides.
|
|
152
|
+
- **Target reference**, per bone: `keep` its bind orientation, or `direction` — turn it so the bone
|
|
153
|
+
points the way the donor's does (an auto-T-pose step, measured to the nearest mapped descendant).
|
|
154
|
+
A target that binds in a STANCE (feet level, legs hanging) keeps everything: that is its natural
|
|
155
|
+
posture and the donor's deltas ride on it — the horse stays tall under wolf clips, the giraffe's
|
|
156
|
+
neck stays up under deer clips. A target that binds posed (T-pose, folded legs) keeps its core
|
|
157
|
+
(root, pelvis, chest, head — artists put those joints anywhere, so their joint lines are not
|
|
158
|
+
comparable) and direction-aligns its chains. `align` overrides per bone.
|
|
159
|
+
- **Feet** (`--ik legs`, the default). Directions transfer, positions do not: with different
|
|
160
|
+
front/back leg ratios the feet float or sink, which the eye catches first. Legs are detected from
|
|
161
|
+
the map (lateral chains reaching the ground; a foot is what stands on the donor's ground, a horse's
|
|
162
|
+
wrist-ended front leg is not), the reference pose is planted (root lowered/raised, every foot bent
|
|
163
|
+
onto the ground), then every frame each foot gets a goal built as THE TARGET'S OWN STANCE plus the
|
|
164
|
+
donor's departure from ITS stance, scaled by that leg's length ratio, on all three axes — height
|
|
165
|
+
above each rig's own stance level, and the horizontal offset each rig plants its foot at under its
|
|
166
|
+
own hip — and a FABRIK pass from the FK pose bends the chain onto it. Both builds are therefore
|
|
167
|
+
preserved: a target that tucks its feet inboard of its hips keeps doing so against a donor that
|
|
168
|
+
stands on vertical legs. (Copying the donor's absolute foot-under-hip offset instead planted this
|
|
169
|
+
repo's fox — which tucks each foot ~8% of body height inboard, while every polyperfect animal
|
|
170
|
+
stands vertical — bow-legged in every clip.) When the legs are scaled copies the goal IS the FK
|
|
171
|
+
pose, so IK only ever spends the proportion residual; goals out of reach are reported per leg.
|
|
172
|
+
- **Root motion** is scaled by the mean leg-length ratio (a stride is leg length, not body height),
|
|
173
|
+
so stance feet that were still on the donor stay still on the target.
|
|
174
|
+
- **Chain lengths.** A longer donor chain collapses for free (the target's `chest` mapped to
|
|
175
|
+
`Chest_M` picks up Spine1+Spine2, because a world rotation already contains everything above it).
|
|
176
|
+
A longer target chain — unmapped bones between two mapped ones — interpolates the delta along the
|
|
177
|
+
chain (a 6-bone spine against a 2-bone one bends everywhere, not at one joint).
|
|
178
|
+
- **Spaces.** The anchor is the BIND pose (inverse of the skin's inverse-bind matrices), never the TRS
|
|
179
|
+
on the nodes — the wolf's node pose is 0.37 m from its bind. A skin whose bind space is scaled
|
|
180
|
+
against node space (Maya "actor" exports, ×100) is rescaled; a donor facing the other way is turned
|
|
181
|
+
(facing is read from the skeleton shape, so `joint1…` rigs work too); non-uniform scales are refused.
|
|
182
|
+
|
|
183
|
+
- **Second pass on proportions** (humanoid plan phase 3, 2026-08-27; the quadruped path shares the
|
|
184
|
+
planting and the root-motion switch):
|
|
185
|
+
- **A humanoid foot = contact + orientation** (2026-08-27). The leg's IK effector is the ankle,
|
|
186
|
+
but its goal comes from the BALL of the foot (the canonical `Toes` bone): the ball goes where
|
|
187
|
+
the donor's ball goes (leg and foot each scaled by their own ratio, stance-corrected), the foot
|
|
188
|
+
keeps the donor's rotation on the target's own flat bind, and the ankle follows from the two.
|
|
189
|
+
Rigs put the ankle at different places over the ball — a boot's foot bone dives 56° to the ball,
|
|
190
|
+
X-bot's 27°, both soles flat: driving the ball with the foot as an IK link let the solver pick
|
|
191
|
+
the foot's pitch (50° off the donor in the swing), transferring the ankle's position AND the
|
|
192
|
+
foot's rotation lifted the boot 10 cm off the ground at toe-off. Now the stance foot is flat,
|
|
193
|
+
toe-off has the heel up and the toes on the ground, and the report says `contact <ball>` when
|
|
194
|
+
it differs from the effector. What the two feet's shapes cost at full extension (a few cm) is
|
|
195
|
+
silent; `reach` reports what the LEG cannot do.
|
|
196
|
+
- **Foot planting** (default; `--no-plant`). While a donor foot stands still on its ground (under
|
|
197
|
+
15 % of a leg length per second, within 6 % of a leg length of its stance height, ≥ 3 frames),
|
|
198
|
+
the target's foot goal is held at ONE world point — the mean of where the FK would have put it
|
|
199
|
+
over the span, which splits the slide between landing and lift-off; a 4-frame ease at each end
|
|
200
|
+
hides both halves, a span across a cycle's seam is one plant held modulo the clip's travel. A
|
|
201
|
+
span whose mean sits more than 5 % of a leg length from some frame's FK goal is a shuffle, not a
|
|
202
|
+
plant, and is left alone. Per-leg ratios and the hips' own sway otherwise slide a planted foot a
|
|
203
|
+
few cm per step. The planted point is the contact (the ball rolls under a rising heel; the ankle
|
|
204
|
+
moves). Reported per leg (`planted N frames`) and per clip as spans.
|
|
205
|
+
- **Where the travel goes** (`--root-motion auto|hips|root|none`). A rig with a root ABOVE the hips
|
|
206
|
+
gets the donor's horizontal travel on the root and the vertical bob on the hips (`root`, the
|
|
207
|
+
default when such a root exists — a character controller reads the root's travel and must not
|
|
208
|
+
bob with it); `hips` puts everything on the hips as the donor had it; `none` strips the travel
|
|
209
|
+
for an in-place clip (feet still planted in the travelling frame, so they slide back under the
|
|
210
|
+
body like a treadmill). A donor whose hips bob relative to their own root (MoCap Central:
|
|
211
|
+
`root` travels, `pelvis` bobs) has that bob re-applied on the target's hips.
|
|
212
|
+
- **Hands as effectors** (`--ik hands`): the same ratio-scaled goal as a foot, measured from the
|
|
213
|
+
upper arm, chain capped at upper + lower arm, for clips where the hands meet or hold.
|
|
214
|
+
- **Twist helpers** (`--twist N`, default 0.5; `off` = ride rigidly). A helper at fraction `t`
|
|
215
|
+
along its limb bone takes `f·(t·twistOut − (1 − t)·twistIn)` about the bone's axis on top of
|
|
216
|
+
riding it — near the joint it counter-rotates the bone's own axial turn (the shoulder skin stays
|
|
217
|
+
with the clavicle), near the far end it follows the child's (the wrist follows the hand's
|
|
218
|
+
pronation). Helpers come from the avatar's `twist` map (shape classifier or `.avatar.json`).
|
|
219
|
+
- The stance correction inside a foot goal (where THIS rig rests its foot under its hip, against
|
|
220
|
+
where the donor does) is shrunk — never the donor's vector — when it would put the goal past the
|
|
221
|
+
leg's length (a running push-off on a rig whose hips sit higher above its feet); that removed
|
|
222
|
+
the "out of reach" residue on Humanoid_F.
|
|
223
|
+
|
|
224
|
+
What it does not do yet: stride warping when a leg is far too short for the donor's reach (reported
|
|
225
|
+
as "out of reach"), torso pitch to compensate for mismatched front/hind leg ratios; for humanoids
|
|
226
|
+
the knee/elbow bend-plane roll of the UPPER arm and the legs —
|
|
227
|
+
[docs/humanoid-retarget-plan.md](../../docs/humanoid-retarget-plan.md).
|
|
228
|
+
|
|
229
|
+
### Humanoids — avatars
|
|
230
|
+
|
|
231
|
+
A humanoid rig is described by its **avatar**: its bones named in the Unity/VRM humanoid canon
|
|
232
|
+
(`Hips`, `Spine`/`Chest`/`UpperChest`, `Neck`, `Head`, `Jaw`, `LeftEye`…, `LeftShoulder`/`LeftUpperArm`/
|
|
233
|
+
`LeftLowerArm`/`LeftHand`, `LeftUpperLeg`/`LeftLowerLeg`/`LeftFoot`/`LeftToes`, `Left Thumb Proximal`…
|
|
234
|
+
— 55 bones). An avatar belongs to ONE rig; a humanoid map is two avatars joined on the canon, so
|
|
235
|
+
each rig is mapped once and any pair retargets.
|
|
236
|
+
|
|
237
|
+
```
|
|
238
|
+
lecodes assets avatar girl.glb # read the avatar off the skeleton's shape
|
|
239
|
+
lecodes assets avatar girl.glb --meta SK_Girl.fbx.meta -o girl.avatar.json # check vs Unity's map, write Unity's
|
|
240
|
+
lecodes assets retarget girl.glb --from Swat.fbx --auto-map # both rigs' avatars from shape → map.json
|
|
241
|
+
lecodes assets retarget girl.glb --from Swat.fbx --avatar girl.avatar.json --map auto --clips Walking
|
|
242
|
+
```
|
|
243
|
+
|
|
244
|
+
`avatar` reads the structure off the bind pose (`src/retarget/humanoid.ts`): the hips are the joint
|
|
245
|
+
two chains hang from that reach the ground on opposite sides; the hands are the lateral joints fanning
|
|
246
|
+
out into short chains (fingers) — a weapon socket or IK target on the hand is shed; the spine is the
|
|
247
|
+
centre path from the hips to where the arms meet; the head is the last branch above the arms (hair
|
|
248
|
+
rigs branch below it, so a `head` name breaks that tie); the toes say which way is forward, and so
|
|
249
|
+
which side is left. Spine bones: 1 → `Spine`, 2 → `+Chest`, 3 → `+UpperChest`, more → first /
|
|
250
|
+
arc-middle / arm-attach with the rest **bridged**; a second neck bone and finger metacarpals bridge
|
|
251
|
+
too. **Twist/roll helpers** (leaf children on their parent's axis — `upperarm_twist_01_l`,
|
|
252
|
+
`calf_twist_02_r`) are recorded as `twist: { bone: canonical }`, never mapped. Names only break ties
|
|
253
|
+
(thumb vs index when both are named; a `jaw` child) and the report says when they did. Everything
|
|
254
|
+
else (hair, cloth, `ik_*`, bag, breasts) is listed as "not part of the body" and keeps its bind.
|
|
255
|
+
|
|
256
|
+
**A person's word rides in the avatar** (`.avatar.json` `transfer`, written by the character studio,
|
|
257
|
+
read by `retarget --avatar`): `align: { <canonical bone>: "keep" | "align" }` — this bone is BUILT
|
|
258
|
+
that way (keep the bind, whatever the angle) or POSED (align to the donor, the 8° threshold
|
|
259
|
+
notwithstanding; a standing leg too); `roll: { Left?, Right? }` — degrees added to the measured palm
|
|
260
|
+
roll per arm, for a finger that lands almost right; and the transfer settings (`rootMotion`,
|
|
261
|
+
`plant`, `hands`, `twist`) the CLI takes as defaults — flags still win. The report lists
|
|
262
|
+
`under 8° kept` bones with their angle so the decision can be checked, and `avatar transfer: …`
|
|
263
|
+
says what the file asked for.
|
|
264
|
+
|
|
265
|
+
**The humanoid transfer** (`retarget` with both avatars — `--map auto`, `--avatar`, or a map.json
|
|
266
|
+
that carries `avatars`) keeps the world-delta machinery above and decides the reference by canonical
|
|
267
|
+
ROLE: hips, spine, neck, head, face and (standing) legs keep their bind; shoulder → upper arm →
|
|
268
|
+
forearm → hand and every phalanx align to the donor's bind directions with explicit references (the
|
|
269
|
+
hand to its finger bases), but only past an **8° threshold** — under it a mismatch is joint placement
|
|
270
|
+
and stays, over it is posture (an A-pose against a T-pose, a bent elbow) and turns. Direction fixes
|
|
271
|
+
two of three degrees of freedom; the **palm roll** — where the palm faces, the thing no direction can
|
|
272
|
+
see — is read off the knuckle line (index base → little base) across the forearm, and the forearm
|
|
273
|
+
chain turns until the target's line lies where the donor's does. Metacarpals ride the hand rigidly.
|
|
274
|
+
The donor is referenced to its bind (its clips are authored on it); a target `root` above the pelvis
|
|
275
|
+
that shares the donor's hips takes the hips' travel only. An identity retarget is exact
|
|
276
|
+
(`tests/humanoidRetarget.test.ts`, key reduction off); Mixamo Walking on the zoo lands arm directions
|
|
277
|
+
within 3° and palms within 10° on every frame.
|
|
278
|
+
|
|
279
|
+
A Unity model importer's `.meta` carries Unity's own humanoid map: `--meta` diffs the shape result
|
|
280
|
+
against it — the zoo in the plan agrees on every bone but the Chest/UpperChest picks on a 5-bone spine
|
|
281
|
+
— and `-o` with `--meta` writes Unity's map as the `.avatar.json`. `retarget --avatar` / `--donor-avatar`
|
|
282
|
+
accept either file; without them both rigs classify from shape (a biped↔quadruped pair is refused).
|
|
283
|
+
|
|
284
|
+
Bind space: IBMs live in the MESH's geometry space. When an exporter parks the Z-up→Y-up turn on a
|
|
285
|
+
geometry helper node (Female Runner's `Humanoid_F`) or ×100 on the mesh node (Maya "actor"), `rig.ts`
|
|
286
|
+
folds that node's world transform in front of `inv(IBM)` — kept only when it brings the bind closer to
|
|
287
|
+
the node rest pose.
|
|
288
|
+
|
|
289
|
+
### Donors whose clips are not in the FBX
|
|
290
|
+
|
|
291
|
+
Asset packs routinely ship the rig as FBX and the animations as Unity `.anim` assets — the
|
|
292
|
+
polyperfect cat and all three dogs have EMPTY FBX files and eight `.anim` clips each. `--anim` reads
|
|
293
|
+
those directly (`src/retarget/unityAnim.ts`, YAML only — a project saved with binary serialisation
|
|
294
|
+
has to be re-exported with *Asset Serialization = Force Text*) and attaches them to the donor rig as
|
|
295
|
+
clips, bound by the LAST segment of each curve's transform path:
|
|
296
|
+
|
|
297
|
+
```
|
|
298
|
+
lecodes assets retarget fox.glb --from Cat.fbx --map fox-from-cat.map.json --anim Cat_Walk.anim Cat_Idle.anim Cat_Sleep.anim Cat_StandUp.anim --clips Cat_Sleep Cat_StandUp --clip-names sleep stand_up -o fox-cat.glb
|
|
299
|
+
```
|
|
300
|
+
|
|
301
|
+
Pass the donor's locomotion clips even when you do not want them in the output: `--donor-ref auto`
|
|
302
|
+
takes the donor's neutral posture from a walk/idle, and without one it falls back to the bind pose.
|
|
303
|
+
|
|
304
|
+
**Skeleton-only clip files** — the other pack convention (Female Runner, MoCap Central, Mixamo
|
|
305
|
+
"without skin"): each clip is an FBX of nodes + curves and NO mesh, so it has no skin to classify
|
|
306
|
+
and no bind to measure from. `--donor-rig <model.fbx|.glb>` names the rig it was animated on; the
|
|
307
|
+
clips bind to it by bone name (exact, then without the `namespace:` prefix — `rebindClips` in
|
|
308
|
+
`rig.ts`) and the model's bind, skin and avatar carry the motion:
|
|
309
|
+
|
|
310
|
+
```
|
|
311
|
+
lecodes assets retarget vex.glb --from MCU_af_Loco_Walk_Fwd.FBX --donor-rig rig_MCUnity.fbx --map auto -o vex-mc-walk.glb
|
|
312
|
+
```
|
|
313
|
+
|
|
314
|
+
Whether a clip file IS the character's own rig (copy the tracks by name, the Mixamo flow) or only
|
|
315
|
+
shares its naming convention (two Unreal-style rigs — retarget) is decided by `sameSkeleton`:
|
|
316
|
+
≥ 80 % of the names must match AND every matched bone ≥ 5 cm must keep its local offset's
|
|
317
|
+
DIRECTION (the child in its parent's frame — fixed by the rig, unlike the node rotations, which
|
|
318
|
+
a clip file carries posed at its first frame). A Mixamo character against another's clip stays
|
|
319
|
+
within ~20°; MoCap Central's rig against IdaFaber's turns the pelvis by 88° — a copied clip would
|
|
320
|
+
lay the character down.
|
|
321
|
+
|
|
322
|
+
### What a short spine cannot borrow
|
|
323
|
+
|
|
324
|
+
Rotations transfer; positions follow from the TARGET's own bones. Leg IK covers that for feet, but
|
|
325
|
+
nothing covers the spine, so a clip whose whole point is a spine CURL does not survive a target with
|
|
326
|
+
fewer spine bones. The cat sits by curving a 7-bone spine to hold its chest up over a dropping
|
|
327
|
+
pelvis; this fox has two spine bones and no way to curve, so its chest is simply carried down by the
|
|
328
|
+
pelvis and the head ends up on the floor (4% of body height, against the cat's 79%). Hanging the
|
|
329
|
+
pelvis on a mid-spine bone instead recovers part of it (27%) but then breaks lying-down clips, whose
|
|
330
|
+
heads go through the floor. Clips that END lying (`Sleep`) or start there (`StandUp`) transfer fine —
|
|
331
|
+
they need no upward curl.
|
|
332
|
+
|
|
333
|
+
### Seeing the result
|
|
334
|
+
|
|
335
|
+
A bone map is judged by eye. `tools/filmstrip.ts` skins a clip on the CPU and writes a PNG strip —
|
|
336
|
+
no browser, no GPU, no engine, so it runs on every iteration:
|
|
337
|
+
|
|
338
|
+
```
|
|
339
|
+
bun tools/filmstrip.ts fox-anim.glb strip.png 6 wolf_walk threequarter
|
|
340
|
+
BONES=1 CELL=360 bun tools/filmstrip.ts fox-anim.glb strip.png 5 wolf_walk side # skeleton overlay
|
|
341
|
+
BIND=1 BONES=1 bun tools/filmstrip.ts fox.glb bind.png 1 - side # the bind pose itself
|
|
342
|
+
CELL=440 bun tools/filmstrip.ts fox-anim.glb strip.png 4 wolf_walk side "fox.glb|-|0" # rest pose first
|
|
343
|
+
```
|
|
344
|
+
|
|
345
|
+
For the real thing on the real engine, `packages/viewer` → `node anim-check/model-check.mjs`.
|
|
346
|
+
|
|
347
|
+
## Unreal packs — `.uasset` skeletons, clips, meshes
|
|
348
|
+
|
|
349
|
+
An Unreal asset-store pack is a Content folder of **uncooked editor packages**: one `.uasset` per
|
|
350
|
+
asset with everything inline. `convert`, `probe` and `retarget` read them directly — no Unreal
|
|
351
|
+
install, no FBX export step:
|
|
352
|
+
|
|
353
|
+
```
|
|
354
|
+
lecodes assets unreal ls "Stairs Set/StairsSet" # triage: classes, rigs, what is unreadable and why
|
|
355
|
+
lecodes assets probe .../A_Stairs_WalkFwd_Up_Loop.uasset # engine, data model, skeleton, keys, root travel
|
|
356
|
+
lecodes assets convert .../SKM_Manny.uasset --clips .../Animations/Mannequin/**/*.uasset -o manny.glb
|
|
357
|
+
lecodes assets convert .../A_Stairs_WalkFwd_Up_Loop.uasset -o walk.glb # clip-only GLB (its skeleton + the clip)
|
|
358
|
+
lecodes assets convert hero.glb --clips .../A_Stairs_WalkFwd_Up_Loop.uasset # merge one onto YOUR character by bone name
|
|
359
|
+
lecodes assets retarget vex.glb --from .../SKM_Manny.uasset --anim .../Walk/*.uasset --map auto --root-motion hips -o vex-stairs.glb
|
|
360
|
+
```
|
|
361
|
+
|
|
362
|
+
**Both data models are read.** An `AnimSequence` keeps its raw motion in one of two places, and a
|
|
363
|
+
pack's choice is not yours: `AnimDataModel` is bone tracks (what an FBX import writes), while a clip
|
|
364
|
+
authored or edited in **Sequencer** (UE 5.2+, through a Control Rig) keeps the same motion as
|
|
365
|
+
MovieScene channels — one FK control per bone, nine `FMovieSceneFloatChannel`s each (3 translation,
|
|
366
|
+
3 rotation as an `FRotator` in degrees, 3 scale). `sequencer.ts` evaluates those onto the clip's own
|
|
367
|
+
frame grid, so callers never learn which model a file used. The grid lands exactly on the authored
|
|
368
|
+
keys (checked across a pack's 48 Sequencer clips: 0 ms offset, 0 value error), and the channels'
|
|
369
|
+
constant values reproduce the skeleton's reference pose to 1e-4 cm and 0.001° — which is what pins
|
|
370
|
+
the channel order and the `FRotator` convention.
|
|
371
|
+
|
|
372
|
+
**What is read.** `SkeletalMesh` (LOD geometry, UVs, skin weights, bone list — from the editor
|
|
373
|
+
import data the FBX importer left behind), `Skeleton` (bones + reference pose), `AnimSequence`
|
|
374
|
+
(the raw uncompressed bone tracks of its `AnimDataModel`, plus fps, length, root-motion flag,
|
|
375
|
+
additive type, sync markers / notifies), and `Texture2D` — the SOURCE image the artist imported,
|
|
376
|
+
not the compressed mips a cooked build ships (see **Textures** below).
|
|
377
|
+
|
|
378
|
+
**Where a clip's skeleton comes from.** An AnimSequence names it only as an object path
|
|
379
|
+
(`/Game/StairsSet/Demo/Mannequins/Meshes/SK_Mannequin`), where `/Game/` is the pack's Content
|
|
380
|
+
folder — which nothing on disk marks. The path is turned into a relative file name and looked for
|
|
381
|
+
walking up from the clip; the first directory that answers IS the Content root. `--skeleton
|
|
382
|
+
<SK_*.uasset>` names it outright, `--content-root <dir>` names the folder. A `SKM_*.uasset` works
|
|
383
|
+
as a skeleton too (its own bone list is the bind reference).
|
|
384
|
+
|
|
385
|
+
**Coordinates.** UE is centimetres, X-forward/Y-right/Z-up and left-handed; glTF is metres, Y-up
|
|
386
|
+
and right-handed. A `.uasset` GLB has to be interchangeable with one made from the same pack's FBX
|
|
387
|
+
(both feed `--clips` and the retargeter), so the conversion reproduces what UE's FBX exporter plus
|
|
388
|
+
ufbx do: mirror Y on every local transform, and turn the ROOT bone −90° about X. Checked against
|
|
389
|
+
the Stairs pack's own `SourceFiles.zip`: all 267 channels of a clip match by bone name to
|
|
390
|
+
**1e-5 m / 0.02°** (`tests/unreal.test.ts`), and the recipe's composition is pinned by a unit test —
|
|
391
|
+
the per-bone rules must add up to the global `(x, z, y)·0.01` map on world points.
|
|
392
|
+
|
|
393
|
+
**Limits** (each one says so at the point it bites, rather than producing a broken GLB):
|
|
394
|
+
|
|
395
|
+
| Not supported | What you get |
|
|
396
|
+
|---|---|
|
|
397
|
+
| UE 5.4+ / UE 5.5+ packages, UE4 packages, cooked assets | refused by version, with the engine that saved the file |
|
|
398
|
+
| meshes whose LODs were generated in-engine (no import data — the Stairs pack's UE4 mannequin) | refused; only the skeleton of such a mesh can be read |
|
|
399
|
+
| morph targets, curves, sockets, physics assets, Oodle-compressed MESH bulk | reported, not guessed at |
|
|
400
|
+
|
|
401
|
+
Twelve bones per vertex become four (top by weight, renormalised) — the trim is reported as a
|
|
402
|
+
percentage of redistributed weight. Triangle winding is decided from the data (the mirror reverses
|
|
403
|
+
handedness) rather than hidden behind a double-sided material.
|
|
404
|
+
|
|
405
|
+
**Textures.** A texture asset keeps its source image (usually the original PNG) inside an
|
|
406
|
+
`FCompressedBuffer`, compressed with **Oodle** — proprietary, so nothing is shipped here. The
|
|
407
|
+
decoder is loaded from a library the machine already has:
|
|
408
|
+
|
|
409
|
+
```sh
|
|
410
|
+
export LECODES_OODLE=".../Engine/Binaries/ThirdParty/Oodle/Win64/oo2core_9_win64.dll" # any UE install
|
|
411
|
+
# or drop the file into ~/.lecodes/oodle/ — Unreal installs are also found automatically
|
|
412
|
+
lecodes assets unreal tex T_Manny_01_D.uasset -o manny_d.png # one texture, out
|
|
413
|
+
lecodes assets convert SKM_Manny.uasset -o manny.glb # textures wired to the materials
|
|
414
|
+
```
|
|
415
|
+
|
|
416
|
+
Without it the conversion still runs and says so once; only the materials stay grey. With it, each
|
|
417
|
+
slot's `MaterialInstanceConstant` is read and its `TextureParameterValues` are wired to glTF:
|
|
418
|
+
base colour, normal, and ORM (UE packs occlusion/roughness/metallic into R/G/B, which is exactly
|
|
419
|
+
what glTF reads). Two fixes are applied on the way, both required and both verified against the
|
|
420
|
+
render: a `TSF_BGRA8` source has its red and blue channels the other way round (without the swap the
|
|
421
|
+
mannequin comes out blue), and UE authors normal maps DirectX-style, so the green channel is flipped
|
|
422
|
+
for glTF (`--no-normal-flip` keeps them as they were). Textures are capped at `--max-size` (2048)
|
|
423
|
+
and re-encoded — JPEG unless the alpha channel is actually used.
|
|
424
|
+
|
|
425
|
+
What is NOT wired: everything the material graph would have to be evaluated to understand. The UE5
|
|
426
|
+
mannequin's material names seven textures — a bent normal, an aniso/AO/paint mask, a clear-coat
|
|
427
|
+
mask, an MSR pack, a tangent map — and only the base colour and the normal map have an honest glTF
|
|
428
|
+
slot. The rest are listed in the report (`not in glTF: …`) rather than guessed into one. Scalar and
|
|
429
|
+
vector parameters (colour tints, roughness powers) are read but not applied for the same reason:
|
|
430
|
+
gear comes out in its neutral colours.
|
|
431
|
+
|
|
432
|
+
**Retargeting from a UE rig.** `--from x.uasset` builds the rig directly. The UE5 mannequin weights
|
|
433
|
+
its limbs to the twist helpers (`thigh_twist_01_l`) and leaves `thigh_l` itself without a single
|
|
434
|
+
vertex, so "part of the body" is the skinned subtree's INTERIOR, not just the bones carrying
|
|
435
|
+
weights — otherwise the humanoid classifier finds no legs and refuses the rig. Unskinned ends
|
|
436
|
+
(`ik_foot_root`, `ik_hand_gun`, `weapon_l`, `interaction`, `center_of_mass`, `root`) stay out of the
|
|
437
|
+
body, which is what they should be.
|
|
438
|
+
|
|
439
|
+
Stair clips carry vertical root travel (0.40 m per loop on the Stairs pack's walk-up). `--root-motion
|
|
440
|
+
hips` keeps the climb on the character; `auto`/`root` treat vertical motion as hips bob and would
|
|
441
|
+
swallow it. Feet below the origin plane on a walk-DOWN clip are reported as "toe under the ground" —
|
|
442
|
+
expected for stair content, where the ground is not flat.
|
|
443
|
+
|
|
444
|
+
|
|
445
|
+
## Scale
|
|
446
|
+
|
|
447
|
+
`probe` and `doctor` always **report** the model's rendered world size; nothing is changed without a
|
|
448
|
+
flag. A skinned primitive ignores its node's transform (glTF spec), so the size is measured through
|
|
449
|
+
the skin's bind space (`world(joint₀)·IBM₀`) — measuring via the mesh node reports garbage on exactly
|
|
450
|
+
the assets that are mis-scaled.
|
|
451
|
+
|
|
452
|
+
| flag | what it does |
|
|
453
|
+
|---|---|
|
|
454
|
+
| `--fix-scale` | guess the unit and bake it: cm / mm / feet / inches / the Blender-FBX `0.01` armature quirk. Picks the **smallest** correction that lands the largest dimension in 0.1–100 m |
|
|
455
|
+
| `--scale N` | bake an explicit factor (works on clip-only files too) |
|
|
456
|
+
| `--scale-to M` | bake whatever makes the largest dimension `M` metres |
|
|
457
|
+
|
|
458
|
+
The factor is **baked into the data** — positions, node translations, IBM offsets and translation
|
|
459
|
+
tracks all × s — not parked on a wrapper node. No node is added and no local scale is rewritten, so
|
|
460
|
+
every node path an app addresses stays what it was. Rotation and scale tracks are untouched by
|
|
461
|
+
construction: scaling only translations gives `world' = S·world·S⁻¹`, and the skinning identity then
|
|
462
|
+
needs `IBM' = S·IBM·S⁻¹`, which is just the IBM's translation column × s.
|
|
463
|
+
|
|
464
|
+
Quantized (`KHR_mesh_quantization`) positions are refused rather than silently corrupted.
|
|
465
|
+
|
|
466
|
+
## Shading — why a downloaded model renders too bright
|
|
467
|
+
|
|
468
|
+
`doctor` always reports two material quirks that make an asset look washed out, and neither one is
|
|
469
|
+
the usual suspect (metallic, or the texture):
|
|
470
|
+
|
|
471
|
+
- **self-lit** — the base colour texture is *also* wired into `emissiveTexture` with
|
|
472
|
+
`emissiveFactor = [1,1,1]`, so the shader draws `lit(albedo) + albedo`: roughly twice as bright as
|
|
473
|
+
intended, with the shading flattened out. Common from Sketchfab downloads and several FBX
|
|
474
|
+
exporters. `--fix-emissive` drops it and leaves the material otherwise PBR.
|
|
475
|
+
- **specular boost** — `KHR_materials_specular` with a `specularColorFactor` above 1.
|
|
476
|
+
|
|
477
|
+
`--unlit` goes further and converts the material to **`KHR_materials_unlit`**: flat
|
|
478
|
+
`baseColorFactor × baseColorTexture`, no lights, no IBL, no exposure — the shading model hand-painted
|
|
479
|
+
stylised assets are usually authored for. It also clears the emissive and drops specular/ior/normal/
|
|
480
|
+
occlusion/MR inputs, because gltfio *refuses* to combine specular with unlit ("Cannot use specular
|
|
481
|
+
extension with unlit") and silently falls back to lit if it is left in place.
|
|
482
|
+
|
|
483
|
+
Both engines implement it: Filament through gltfio's `unlit_opaque` ubershader, web-lite through the
|
|
484
|
+
`UNLIT` branch of its glTF shader (`viewer-lite/src/gl/glb.ts`, covered by `bun run render:check`).
|
|
485
|
+
|
|
486
|
+
## What `convert` does
|
|
487
|
+
|
|
488
|
+
- **Reads FBX with [ufbx](https://github.com/ufbx/ufbx)** compiled to wasm (`native/fbx_reader.c` +
|
|
489
|
+
`vendor/ufbx`, built by `build-wasm.sh` into `src/wasm/fbxReader.js` — a single-file module, no
|
|
490
|
+
native builds, runs in Bun/Node/browsers). Everything is converted to glTF conventions in C:
|
|
491
|
+
meters, right-handed Y-up (`space_conversion = MODIFY_GEOMETRY`), geometry transforms / inherit
|
|
492
|
+
modes as helper nodes, triangulated + welded primitives, top-4 skin weights renormalized, UV v
|
|
493
|
+
flipped, animations **baked** (30 fps, key-reduced, trimmed to start at 0). One JSON description +
|
|
494
|
+
one binary arena cross the wasm boundary; `src/fbx/toDocument.ts` builds the Document.
|
|
495
|
+
- **Textures** embedded: PNG/JPEG pass through, TGA/BMP decoded here (→ PNG), everything else via
|
|
496
|
+
`sharp` when installed; duplicates de-duplicated by content hash; external references resolved
|
|
497
|
+
next to the FBX / `textures/`.
|
|
498
|
+
- **Materials**: ufbx's PBR view of FBX Phong/Lambert → metallic-roughness (opacity map → MASK,
|
|
499
|
+
opacity factor < 1 → BLEND, double-sided honored). Separate metallic/roughness maps are not
|
|
500
|
+
packed (factors used).
|
|
501
|
+
- **`--clips`**: each clip file's animations are re-targeted onto the model **by bone name** (exact,
|
|
502
|
+
then the part after the last `:`/`|` — the same rule the SDK `Animator` uses); clip meshes/skins
|
|
503
|
+
dropped; unbound tracks counted + warned. Naming: a single-clip file → its stack name unless generic
|
|
504
|
+
(`mixamo.com`, `Take 001`, `Unreal Take`) → the file's basename; `--clip-names` overrides.
|
|
505
|
+
**Parent-space correction**: a track is a LOCAL transform, and two exports of one rig need not
|
|
506
|
+
agree on what sits above a bone — Unreal/Unity clip exports put `root` straight under the scene
|
|
507
|
+
(rest −90°X, the Z-up→Y-up turn) while the skinned-mesh export parks that turn on an armature node
|
|
508
|
+
and leaves `root` at identity; copied verbatim the turn applies twice and the character plays lying
|
|
509
|
+
on the floor (KINEMATION's Tactical Shooter Pack, every UE5 Mannequin export). For each bound bone
|
|
510
|
+
the converter compares the world transform of its parent in both files — with the clip's first
|
|
511
|
+
frame written onto every bone it animates, so only the unanimated STRUCTURE above differs, never
|
|
512
|
+
the hair-width rest-pose drift between re-exports — and re-expresses the track in the model's
|
|
513
|
+
parent space (`local_model = inv(Pmodel)·Pclip·local_clip`). Reported as an ℹ line per clip file.
|
|
514
|
+
- Output goes through the same validation as the doctor (`validateGlb`) and a `probe` summary.
|
|
515
|
+
|
|
516
|
+
Skipped in v1: cameras/lights, morph targets, NURBS.
|
|
517
|
+
|
|
518
|
+
## Verified with
|
|
519
|
+
|
|
520
|
+
`packages/viewer` → `node anim-check/model-check.mjs anim-check/fixtures/<file>.glb --clip N`: loads the
|
|
521
|
+
GLB on the Filament wasm engine, binds every clip through the native Animator, screenshots the rest
|
|
522
|
+
pose and a playing clip (the mesh must draw and deform). `bun test` here covers the reader/converter
|
|
523
|
+
against the Mixamo sample set when `LECODES_FBX_SAMPLES` (or the default sample folder) exists.
|
|
524
|
+
|
|
525
|
+
The Unreal reader was verified against two real packs — *Stairs Set UE 5.0-5.4* (UE 5.0: 159
|
|
526
|
+
packages, 84 AnimSequences on two mannequins, a 26 MB zlib-chunked mesh bulk) and a UE 5.3 modular
|
|
527
|
+
character pack (796 packages, raw bulk, all 48 clips Sequencer-authored) — every package parsed or refused
|
|
528
|
+
with a reason, and the clips compared channel-by-channel against the pack's own FBX sources.
|
|
529
|
+
`tests/unreal.test.ts` runs those fixtures when `LECODES_UE_PACK` (or the default pack folder)
|
|
530
|
+
exists, plus format/math unit tests that always run. The texture path was checked against the
|
|
531
|
+
predecessor Python extractor with the same Oodle library: the images come out BYTE-IDENTICAL on
|
|
532
|
+
both packs (67 MB, 21 MB and 1 MB payloads), and the converted mannequin renders with its own
|
|
533
|
+
materials through `packages/viewer`'s Filament check.
|
|
534
|
+
|
|
535
|
+
Retargeting was tuned on the polyperfect *Low Poly Animated Animals* zoo (different authoring
|
|
536
|
+
conventions within one pack: AdvancedSkeleton `Root_M/Hip_L`, Maya `actor:skin_*` with ×100 bind
|
|
537
|
+
space and −Z facing, unnamed `joint1…joint42`): wolf → Meshy fox, lion → cat, wolf → horse / golden
|
|
538
|
+
retriever, deer → giraffe, lion → elephant, all through `--map auto`, judged on `BONES=1` filmstrips
|
|
539
|
+
next to the donor's own walk. `tests/retarget.test.ts` pins the invariants on toy rigs (exact
|
|
540
|
+
direction transfer, keep/direction policies, bridges, leg detection + IK goals, donor reference).
|
|
541
|
+
|
|
542
|
+
## Layout
|
|
543
|
+
|
|
544
|
+
```
|
|
545
|
+
native/fbx_reader.c ufbx → JSON + arena (compiled to wasm)
|
|
546
|
+
vendor/ufbx/ ufbx.h / ufbx.c (MIT)
|
|
547
|
+
src/wasm/fbxReader.js generated single-file Emscripten module (tracked; rebuild with build-wasm.sh)
|
|
548
|
+
src/fbx/ read.ts (wasm boundary + IR) · toDocument.ts · convert.ts (merge, write)
|
|
549
|
+
src/textures/ resolveImage.ts (sniff, TGA/BMP decode, PNG encode, external lookup)
|
|
550
|
+
src/doctor/ doctor.ts (analyze/apply/report) · reduceGlbBones · transcodeTextures · transcodeKtx2 · normalizeScale · lightmapUv (xatlas wasm) · validateGlb (mirror of viewer's)
|
|
551
|
+
src/retarget/ rig.ts (hierarchy + bind pose + clip sampling) · retarget.ts (reference poses, world-delta transfer, leg IK, bridges) · automap.ts (quadruped classification + chain pairing) · humanoid.ts (humanoid canon, biped classifier, Unity .meta avatars, avatar↔avatar map) · unityAnim.ts (Unity .anim YAML reader)
|
|
552
|
+
tools/humanoid-survey.ts bind-pose survey of the humanoid zoo (arm drop / elbow / facing / proportions per rig)
|
|
553
|
+
tools/humanoid-phase3.ts every zoo clip onto every zoo target: planted frames, reach, twist, root motion (--strips for filmstrips)
|
|
554
|
+
src/retarget/ghost.ts a rig's skeleton alone with one clip — the character studio's donor "ghost"
|
|
555
|
+
src/unreal/ package.ts (uncooked UE5 .uasset: summary/names/imports/exports/tagged properties/bulk data) · skeleton.ts · anim.ts (both data models) · sequencer.ts (MovieScene ControlRig channels) · mesh.ts (FSkeletalMeshImportData) · convert.ts (the UE→glTF coordinate recipe) · toDocument.ts · clipSource.ts (--clips merging) · rig.ts (retargeter) · resolve.ts (object path → file) · texture.ts (FCompressedBuffer + FTextureSource) · oodle.ts (the machine's own oo2core, via bun:ffi / koffi) · materials.ts (material instances → glTF slots) · probe.ts · list.ts (unreal ls) · convertAsset.ts · index.ts
|
|
556
|
+
src/unity/ unpack.ts (.unitypackage streaming) · materials.ts (.mat → glTF materials, ORM packing) · png.ts · scene.ts (.unity → level JSON)
|
|
557
|
+
src/props.ts --list-nodes / --keep / --split (subtree carving + re-origin)
|
|
558
|
+
src/mergeSkins.ts --merge-skins (skinned meshes onto one skeleton) · duplicateNodeNames
|
|
559
|
+
src/filmstrip.ts CPU-skinned PNG filmstrip renderer (probe --filmstrip; tools/filmstrip.ts wraps it)
|
|
560
|
+
src/probe.ts · src/cli.ts · src/index.ts
|
|
561
|
+
```
|