lecodes-assets 0.2.1 → 2.5.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 +203 -26
- package/dist/cli.js +18157 -11441
- package/dist/index.js +16603 -9878
- package/package.json +3 -1
- package/src/cli-kit/command.ts +100 -0
- package/src/cli-kit/help.ts +72 -0
- package/src/cli-kit/index.ts +4 -0
- package/src/cli-kit/parse.ts +72 -0
- package/src/cli-kit/run.ts +52 -0
- package/src/cli.ts +24 -1039
- package/src/commands/avatar.ts +77 -0
- package/src/commands/bvh.ts +26 -0
- package/src/commands/convert.ts +156 -0
- package/src/commands/doctor.ts +79 -0
- package/src/commands/geometry.ts +45 -0
- package/src/commands/index.ts +24 -0
- package/src/commands/marks.ts +38 -0
- package/src/commands/mirror.ts +35 -0
- package/src/commands/probe.ts +43 -0
- package/src/commands/retarget/donor.ts +63 -0
- package/src/commands/retarget/index.ts +169 -0
- package/src/commands/retarget/map.ts +81 -0
- package/src/commands/retarget/report.ts +77 -0
- package/src/commands/scene.ts +108 -0
- package/src/commands/shared.ts +140 -0
- package/src/commands/sky.ts +35 -0
- package/src/commands/terrainPack.ts +40 -0
- package/src/commands/unpack.ts +43 -0
- package/src/commands/unreal.ts +109 -0
- package/src/commands/unwrap.ts +56 -0
- package/src/doctor/cardTrim.ts +288 -0
- package/src/doctor/doctor.ts +94 -30
- package/src/doctor/impostor.ts +614 -0
- package/src/doctor/merge/accessors.ts +30 -0
- package/src/doctor/merge/atlasLayout.ts +94 -0
- package/src/doctor/merge/atlasPixels.ts +114 -0
- package/src/doctor/merge/bindSpace.ts +59 -0
- package/src/doctor/merge/classify.ts +136 -0
- package/src/doctor/merge/geometry.ts +167 -0
- package/src/doctor/merge/index.ts +98 -0
- package/src/doctor/merge/matrix.ts +60 -0
- package/src/doctor/merge/plan.ts +88 -0
- package/src/doctor/merge/textures.ts +60 -0
- package/src/doctor/merge/types.ts +77 -0
- package/src/doctor/transcodeKtx2.ts +186 -31
- package/src/doctor/transcodeTextures.ts +6 -1
- package/src/doctor/validateGlb.ts +2 -2
- package/src/doctor/vertexAo.ts +333 -0
- package/src/fbx/read.ts +2 -0
- package/src/fbx/toDocument.ts +227 -209
- package/src/geometry.ts +222 -0
- package/src/index.ts +2 -0
- package/src/retarget/heading.ts +238 -171
- package/src/retarget/humanoid.ts +572 -561
- package/src/retarget/retarget.ts +2375 -2272
- package/src/retarget/rig.ts +4 -1
- package/src/retarget/writeMarks.ts +134 -132
- package/src/sky/cmgen.ts +3 -3
- package/src/sky/index.ts +57 -8
- package/src/terrainPack.ts +61 -22
- package/src/unity/colliderFit.ts +287 -0
- package/src/unity/fbxModel.ts +194 -0
- package/src/unity/foliage.ts +63 -0
- package/src/unity/gameObjects.ts +90 -0
- package/src/unity/guid.ts +104 -0
- package/src/unity/image.ts +58 -0
- package/src/unity/kindGlb.ts +364 -0
- package/src/unity/lecodesLevel.ts +414 -0
- package/src/unity/levelTerrain.ts +125 -0
- package/src/unity/matFile.ts +179 -0
- package/src/unity/materialSlots.ts +53 -0
- package/src/unity/materialWriter.ts +316 -0
- package/src/unity/materials.ts +34 -429
- package/src/unity/math.ts +82 -0
- package/src/unity/prefabTree.ts +285 -0
- package/src/unity/prefabs.ts +10 -36
- package/src/unity/propertyPath.ts +43 -0
- package/src/unity/scene.ts +339 -304
- package/src/unity/serialized.ts +242 -0
- package/src/unity/terrain.ts +221 -0
- package/src/unity/triBudget.ts +77 -0
- package/src/unity/triplanar.ts +122 -0
- package/src/unity/unityMesh.ts +151 -0
- package/src/unity/unpack.ts +24 -5
- package/src/unity/yaml.ts +131 -0
- package/src/unreal/index.ts +6 -5
- package/src/unreal/materialGraph.ts +224 -0
- package/src/unreal/materials.ts +117 -9
- package/src/unreal/package.ts +30 -12
- package/src/unreal/resolve.ts +6 -4
- package/src/unreal/sound.ts +128 -0
- package/src/unreal/texture.ts +70 -3
- package/src/unreal/toDocument.ts +7 -0
- package/src/unreal/versions.ts +8 -0
- package/src/unwrap/index.ts +167 -0
- package/src/unwrap/lightmapUv.ts +761 -0
- package/src/wasm/fbxReader.js +1 -1
- package/src/wasm/lightmapUv.js +1 -1
- package/src/doctor/lightmapUv.ts +0 -452
package/README.md
CHANGED
|
@@ -4,6 +4,10 @@ FBX → GLB converter + GLB doctor, one package on one glTF-Transform `Document`
|
|
|
4
4
|
`lecodes assets …` (optional peer of the `lecodes` CLI) or `bun src/cli.ts …` in the monorepo.
|
|
5
5
|
Plan/rationale: [docs/animation-plan.md §2.9](../../docs/animation-plan.md).
|
|
6
6
|
|
|
7
|
+
`lecodes assets` lists the commands, `lecodes assets <command> --help` prints one command's flags
|
|
8
|
+
(`lecodes assets unreal tex --help` for a subcommand). The help is generated from the command
|
|
9
|
+
declarations, so it is always the full and current list — this README explains the workflows.
|
|
10
|
+
|
|
7
11
|
Install it the same way you installed the CLI — the peer is loaded by a bare `import` that resolves
|
|
8
12
|
upward from the CLI's own file, so a global `lecodes` only sees a global peer:
|
|
9
13
|
|
|
@@ -11,18 +15,22 @@ upward from the CLI's own file, so a global `lecodes` only sees a global peer:
|
|
|
11
15
|
npm install -g lecodes-assets
|
|
12
16
|
```
|
|
13
17
|
|
|
14
|
-
KTX2 (`--ktx2`) additionally needs KTX-Software
|
|
15
|
-
|
|
18
|
+
KTX2 (`--ktx2`) additionally needs [KTX-Software](https://github.com/KhronosGroup/KTX-Software/releases):
|
|
19
|
+
its `toktx`, or — where there is none, e.g. KTX-Software 5 / the macOS installer — its `ktx` tool
|
|
20
|
+
(`ktx create`, ≥ 4.4). Looked up at `KTX_TOKTX` / `KTX_TOOL`, the installer's folder
|
|
21
|
+
(`%LOCALAPPDATA%\Programs\KTX-Software\bin`, `/usr/local/bin`, `/opt/homebrew/bin`), then PATH.
|
|
22
|
+
`sharp` is an optional dependency used to normalize exotic texture formats.
|
|
16
23
|
|
|
17
24
|
```
|
|
18
25
|
lecodes assets convert "Erika Archer.fbx" --clips Idle.fbx Walking.fbx "Fast Run.fbx" --drop-model-clips -o hero.glb
|
|
19
26
|
lecodes assets convert hero.glb --clips Jump.fbx # add clips to an existing GLB
|
|
20
27
|
lecodes assets convert hero.fbx --fix # + doctor passes (bone cap, texture normalize)
|
|
21
|
-
lecodes assets convert hero.fbx --ktx2 # + textures → KTX2 (needs KTX-Software
|
|
28
|
+
lecodes assets convert hero.fbx --ktx2 # + textures → KTX2 (needs KTX-Software)
|
|
22
29
|
lecodes assets probe hero.fbx | hero.glb # skeleton / clips / meshes / textures / bind sanity / world size
|
|
23
30
|
lecodes assets doctor hero.glb [--fix] [--ktx2] [--dump] # the old tools/glb-reduce
|
|
24
31
|
lecodes assets doctor hero.glb --fix-scale # bake a unit fix (see Scale below)
|
|
25
|
-
lecodes assets
|
|
32
|
+
lecodes assets unwrap props/ # TEXCOORD_1 lightmap UVs (xatlas), in place (see Lightmap UVs below)
|
|
33
|
+
lecodes assets doctor hero.glb --merge [--atlas-size 4096] # ONE draw call: parts → one primitive, maps → atlases (see Merge below)
|
|
26
34
|
lecodes assets retarget fox.glb --from wolf.fbx --map auto # clips from a DIFFERENT skeleton (map from skeleton shape)
|
|
27
35
|
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
36
|
lecodes assets unpack pack.unitypackage -o out [--only re …] # stream a .unitypackage: unity-index.txt + assets (+ .meta)
|
|
@@ -31,9 +39,11 @@ lecodes assets convert Barrel_v1.FBX --keep Barrel_v1_LD1 -o barrel.glb # one
|
|
|
31
39
|
lecodes assets convert Barrel_v1.FBX --split props/ # every top-level node → its own GLB
|
|
32
40
|
lecodes assets convert Body.FBX --merge-skins Shirt.FBX Gloves.FBX --clips … -o hero.glb # modular character → ONE skeleton
|
|
33
41
|
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 →
|
|
42
|
+
lecodes assets scene Map_v8.unity -o level.json # Unity scene → kinds + placements as JSON
|
|
43
|
+
lecodes assets scene Outdoor.unity --project . --name school # …or built INTO a LeCodes project (GLBs + scene files)
|
|
35
44
|
lecodes assets unreal ls "Stairs Set/StairsSet" # what an UNREAL pack holds (see below)
|
|
36
45
|
lecodes assets unreal tex T_Manny_01_D.uasset -o manny_d.png # a texture's SOURCE image (needs Oodle, see below)
|
|
46
|
+
lecodes assets unreal wav EngineSfx/Sound -o assets/engine --ogg # every SoundWave's imported WAV → mono OGG (Oodle + ffmpeg)
|
|
37
47
|
lecodes assets convert SKM_Manny.uasset --clips Anims/**/*.uasset -o manny.glb # Unreal mesh + clips → GLB
|
|
38
48
|
```
|
|
39
49
|
|
|
@@ -63,20 +73,85 @@ renders a software-skinned PNG strip of every clip (`--clip` for one, `--frames
|
|
|
63
73
|
side|front|top|threequarter|lowfront`, `--bones` for the skeleton overlay, `--cell` px per frame);
|
|
64
74
|
`tools/filmstrip.ts` is the same renderer with reference frames from other files.
|
|
65
75
|
|
|
66
|
-
**Unity scenes.** `scene Map.unity
|
|
67
|
-
`PrefabInstance`
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
`
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
76
|
+
**Unity scenes.** `scene Map.unity` reads a text-serialised scene and expands it the way the Unity
|
|
77
|
+
editor shows it: nested `PrefabInstance`s cloned from their source prefabs (a model file is a prefab
|
|
78
|
+
too), their `m_Modifications` applied, stripped stubs resolved. Every placed prefab becomes a **kind**
|
|
79
|
+
— its mesh parts with the material GUIDs per submesh, LOD levels, colliders, lights — keyed by
|
|
80
|
+
CONTENT, so two instances of one prefab share a kind unless the scene overrode something inside one
|
|
81
|
+
of them (that one becomes `<Kind>_v2`). Kinds stop at nested prefab roots: a wall built from rock
|
|
82
|
+
prefabs is placements of the rocks. The terrain is read from its binary `TerrainData` (heights, splat
|
|
83
|
+
layers merged down to four by coverage; an HDRP layer's smoothness — the mask map's alpha × `m_Smoothness` —
|
|
84
|
+
becomes the roughness map in the albedo's alpha, `1 − smoothness`, which is where terrain.mat reads it),
|
|
85
|
+
its painted trees and detail meshes become placements too. A VEGETATION kind (the `--foliage` list, or the
|
|
86
|
+
importer's folder / name rule) keeps its vertex colours as DATA and gets its sky visibility baked into them at
|
|
87
|
+
import — `COLOR_0 = (ao, 1, 1, wind weight)`, what foliage.mat reads (`src/doctor/vertexAo.ts`); a material that
|
|
88
|
+
draws a DETAIL layer through another UV set (NatureManufacture's bark: `_DETALUSEUV3_ON` under a white
|
|
89
|
+
`_DetailMask`) takes that layer's textures on TEXCOORD_0 = that set × its tiling. Output without `--project` is
|
|
90
|
+
plain JSON: `{ kinds, instances: [ { kind, name, group, position, rotation, scale } ], lights,
|
|
91
|
+
cameras, terrain, stats }`.
|
|
92
|
+
|
|
93
|
+
`--project <dir> [--name map]` builds it INTO a LeCodes project instead: `assets/<map>/<Kind>.glb`
|
|
94
|
+
(meshes pulled from the FBX files, materials written from the `.mat` files the renderers name,
|
|
95
|
+
textures capped by `--max-size` and KTX2 unless `--png`), `src/<map>/<Kind>.scene.ts` (the model +
|
|
96
|
+
its colliders as `Shape` / `Physics` aspects), `assets/<map>/terrain/…` and `src/<map>.scene.ts`
|
|
97
|
+
placing everything, grouped like the Unity scene. GLBs are rebuilt only when their kind changed
|
|
98
|
+
(`assets/<map>/kinds.json`) or with `--force`. Everything is regenerated by the next import — a game
|
|
99
|
+
level instances `src/<map>.scene.ts` as a prefab and adds its gameplay there. **The command line is
|
|
100
|
+
written into that file's header: re-import with the same line**, because the flags carry decisions
|
|
101
|
+
the file cannot show — `--collider-fit <m>` (how far a pack's BOX collider face may stand off the
|
|
102
|
+
art before the kind takes a mesh collider instead, default 1.5 cm: the shooting range's wall module is
|
|
103
|
+
flat at ±0.100 m while its box was fitted to the ±0.138 m of its trim, so bullets stopped 3.8 cm in
|
|
104
|
+
front of the wall and their holes and sparks landed in mid-air), `--drop <re>` (props a pack sinks into the geometry they decorate), `--drop-at
|
|
105
|
+
"x,y,z;…"` (the placements standing at these points, in the frame the scene file prints: the one instance
|
|
106
|
+
of a kind the game moves while its namesakes stay — the range's five lane targets),
|
|
107
|
+
`--no-lightmap <re>` (lamp fixtures and anything the game moves take `lightmap: false`), `--foliage
|
|
108
|
+
<re>` / `--no-foliage` (which kinds wear the vegetation tier; the default verdict — a material in a
|
|
109
|
+
Vegetation/Plants/Grass/Trees folder or named so — is printed), `--no-sun`, `--no-trim-cards`,
|
|
110
|
+
`--max-tris <n>` (a kind over this many triangles is simplified at import, default 50 000, 0 = no cap:
|
|
111
|
+
a photogrammetry scan otherwise arrives as a million-triangle prop AND mesh collider — a faceted scan
|
|
112
|
+
loses its per-face normals to weld and gets smooth ones back; its detail is in the normal map).
|
|
113
|
+
Shader Graph materials whose textures go through a **Triplanar** node (URP walls and floors) get the
|
|
114
|
+
projection baked into their UVs — the node's own planes and Tile — so they stay stock PBR and can
|
|
115
|
+
take part in a lightmap bake.
|
|
116
|
+
Subsets: `--filter <re>` (kind / group / node name), `--group <name>`, `--region x0,z0,x1,z1` (Unity
|
|
117
|
+
metres), `--no-trees`, `--no-details`, `--include-inactive`.
|
|
118
|
+
|
|
119
|
+
Frame: Unity is left-handed and its FBX importer mirrors X, so everything read from Unity is
|
|
120
|
+
conjugated by that mirror — position `(−x, y, z)`, quaternion `(x, −y, −z, w)` — while mesh geometry
|
|
121
|
+
comes from the FBX itself, which already is right-handed (`--handedness none` keeps Unity's numbers).
|
|
122
|
+
Two importer rules matter for models whose FBX has a single top node that IS the mesh (the prefab's
|
|
123
|
+
root in Unity): its position and rotation are never the model's — every `PrefabInstance` overrides
|
|
124
|
+
them — and what does survive is the model importer's `bakeAxisConversion`, which bakes `RotY(180°) ·
|
|
125
|
+
R_node` into the mesh (a Blender export's Z-up→Y-up correction lives there; without it the doors of a
|
|
126
|
+
kit stand beside their doorways).
|
|
127
|
+
|
|
128
|
+
A re-import renames nodes, so a **baked lightmap is invalid after it** — re-bake the level. The
|
|
129
|
+
imported GLBs carry no lightmap UVs; `lecodes lightmap bake` unwraps them itself (below).
|
|
130
|
+
|
|
131
|
+
## Lightmap UVs — `unwrap`
|
|
132
|
+
|
|
133
|
+
```
|
|
134
|
+
lecodes assets unwrap assets/props # every GLB in the folder, in place
|
|
135
|
+
lecodes assets unwrap "assets/school/*.glb" --force # again, over the sets they have
|
|
136
|
+
lecodes assets unwrap assets/arena/arena.glb --groups 12 --zone "-30,0,-20,30,3,20" --zone-max 16
|
|
137
|
+
```
|
|
138
|
+
|
|
139
|
+
The lightmap bake traces a second UV set, `TEXCOORD_1`: every triangle of the file in ONE
|
|
140
|
+
non-overlapping unit square, cut into charts by xatlas (`src/unwrap/lightmapUv.ts`, the wasm in
|
|
141
|
+
`src/wasm/lightmapUv.js`). `unwrap` writes it **into the file itself** — scenes reference the
|
|
142
|
+
original, so there is no `.fixed.glb` to copy over it — through a temp file renamed over the target
|
|
143
|
+
(`-o` writes elsewhere). A file whose primitives all carry `TEXCOORD_1` is skipped (`--force` unwraps
|
|
144
|
+
it again); skinned meshes are left as they are; `<name>_LOD<n>` siblings take level 0's layout by
|
|
145
|
+
projection. Seam vertices are split (the report says how many); every other attribute, the
|
|
146
|
+
materials and the textures stay as they were.
|
|
147
|
+
|
|
148
|
+
`lecodes lightmap bake` runs the same code on every GLB the app references that has no
|
|
149
|
+
`TEXCOORD_1`, so a raw import needs no step of its own. By hand it is for what the bake cannot
|
|
150
|
+
decide: a whole level in one GLB wants `--groups <n>` (n unit squares = n atlas rects: one rect
|
|
151
|
+
cannot span pages) and `--zone` (what is far from where the player walks is cut and baked at a
|
|
152
|
+
coarser texel). The bake re-packs every file's charts at its own density (engines/bake, the
|
|
153
|
+
`<stem>-uv1.bin` sidecar), so only the CUTTING matters here: the layout padding is fixed, and
|
|
154
|
+
`--texel` (the bake's, 0.05) only sizes it for the files whose square the bake keeps as it is.
|
|
80
155
|
|
|
81
156
|
## Unity packs — `unpack` + materials from `.mat`
|
|
82
157
|
|
|
@@ -107,6 +182,18 @@ formats through `sharp`) — one metallic-roughness(+occlusion) texture per mate
|
|
|
107
182
|
keeps the FBX references; `--unity-mats <dir>` looks elsewhere. Only YAML-serialised `.mat` files are
|
|
108
183
|
readable (Unity "Force Text", the asset-store default since 2017); binary ones are listed and skipped.
|
|
109
184
|
|
|
185
|
+
**Emission and baked light.** A material that glows keeps Unity's answer to "does the glow light the
|
|
186
|
+
level": its inspector's *Global Illumination* setting, `m_LightmapFlags` in the `.mat` (bit 2 =
|
|
187
|
+
Baked, bit 1 = Realtime — which a project without realtime GI bakes too, neither = None). None
|
|
188
|
+
becomes the glTF material's extras `{"lecodes":{"emissiveBake":false}}` (merged with the foliage
|
|
189
|
+
`wind` profile when both are there) and the slot summary says `emissiveBake=false (Unity GI: None)`:
|
|
190
|
+
the surface still glows, but `lecodes lightmap bake` makes no lamp of it. That is how packs lit by
|
|
191
|
+
their own lamps arrive — the shooting range's `M_Lamp_long` glows (`_EmissiveColor` 4) with flags 4
|
|
192
|
+
(HDRP leaves EmissiveIsBlack on materials that do glow), and its rooms are lit by area lights. A
|
|
193
|
+
`.mat` without `m_LightmapFlags` keeps the default: it bakes. A level that wants none of its panels
|
|
194
|
+
in the bake says so once, `bake('…', { emissiveBake: false })` (the SDK's lightmap docs, "Emission:
|
|
195
|
+
three forms, one switch").
|
|
196
|
+
|
|
110
197
|
## Retarget — clips from a different skeleton
|
|
111
198
|
|
|
112
199
|
`convert --clips` binds tracks by BONE NAME and copies them verbatim; that only works when both
|
|
@@ -342,7 +429,7 @@ BIND=1 BONES=1 bun tools/filmstrip.ts fox.glb bind.png 1 - side
|
|
|
342
429
|
CELL=440 bun tools/filmstrip.ts fox-anim.glb strip.png 4 wolf_walk side "fox.glb|-|0" # rest pose first
|
|
343
430
|
```
|
|
344
431
|
|
|
345
|
-
For the real thing on the real engine, `
|
|
432
|
+
For the real thing on the real engine, `runtime/web` → `node anim-check/model-check.mjs`.
|
|
346
433
|
|
|
347
434
|
## Unreal packs — `.uasset` skeletons, clips, meshes
|
|
348
435
|
|
|
@@ -429,6 +516,13 @@ slot. The rest are listed in the report (`not in glTF: …`) rather than guessed
|
|
|
429
516
|
vector parameters (colour tints, roughness powers) are read but not applied for the same reason:
|
|
430
517
|
gear comes out in its neutral colours.
|
|
431
518
|
|
|
519
|
+
**Sounds.** A `SoundWave` keeps the WAV it was imported from in the same Oodle `FCompressedBuffer`
|
|
520
|
+
(cooked builds carry platform Vorbis/ADPCM instead — those are not read). `unreal wav <Sound dir> -o
|
|
521
|
+
<dir>` writes every one as `<name>.wav` exactly as imported; `--ogg` re-encodes through ffmpeg (PATH
|
|
522
|
+
or `LECODES_FFMPEG`) to mono Vorbis, a tenth of the size and what `Sound.load` decodes to anyway
|
|
523
|
+
(`--stereo` keeps the channels, `--quality` is libvorbis' `-q`, default 5). `SoundCue` packages next
|
|
524
|
+
to the waves are skipped. Loop points are untouched: an engine pack's RPM loops are cut to the sample.
|
|
525
|
+
|
|
432
526
|
**Retargeting from a UE rig.** `--from x.uasset` builds the rig directly. The UE5 mannequin weights
|
|
433
527
|
its limbs to the twist helpers (`thigh_twist_01_l`) and leaves `thigh_l` itself without a single
|
|
434
528
|
vertex, so "part of the body" is the skinned subtree's INTERIOR, not just the bones carrying
|
|
@@ -483,6 +577,72 @@ extension with unlit") and silently falls back to lit if it is left in place.
|
|
|
483
577
|
Both engines implement it: Filament through gltfio's `unlit_opaque` ubershader, web-lite through the
|
|
484
578
|
`UNLIT` branch of its glTF shader (`viewer-lite/src/gl/glb.ts`, covered by `bun run render:check`).
|
|
485
579
|
|
|
580
|
+
## Merge — one draw call per character
|
|
581
|
+
|
|
582
|
+
A modular character (or any multi-material prop) arrives as a dozen parts — head, hands, shirt,
|
|
583
|
+
pants, boots, vest, helmet, pouches — each its own mesh node, its own skin over a subset of the
|
|
584
|
+
joints, its own material with its own base / normal / ORM maps. Every material is a draw call and
|
|
585
|
+
three texture binds, so the character costs 16 draw calls and a squad of twelve costs 200.
|
|
586
|
+
`doctor --merge` (also `convert … --merge`) folds all of it into **one primitive over one material
|
|
587
|
+
per compatibility class**:
|
|
588
|
+
|
|
589
|
+
- **Classes.** Primitives are grouped by what one draw cannot mix: alpha mode (+ cutoff for MASK),
|
|
590
|
+
double-sidedness, unlit. A Military kit becomes `merged` (everything opaque) plus `merged.blend`
|
|
591
|
+
(the goggles' glass). A material with an extension the pass does not understand, a
|
|
592
|
+
`KHR_texture_transform`, or UVs that tile a texture more than 4× forms a class of its own — its
|
|
593
|
+
geometry still merges with its siblings, its textures are kept as they are.
|
|
594
|
+
- **Kinds.** A *skinned* part joins the union skin (joints in tree order, inverse bind matrices from
|
|
595
|
+
the skin that has most of them; a part bound in another bind space is moved into the canonical one
|
|
596
|
+
when its matrices differ by one rigid transform, and skipped when they disagree per joint). A
|
|
597
|
+
*rigid* part — a plain mesh sitting under a bone: a hat, a strap, a holstered gun — is skinned to
|
|
598
|
+
that bone with weight 1. A *static* part — a plain node — has its world transform baked and lands in
|
|
599
|
+
`merged.static`. Node-animated parts, morph targets and names matching `--merge-exclude <regex>`
|
|
600
|
+
stay as they are and are reported.
|
|
601
|
+
- **Atlases.** Per class, one texture per map kind the parts use: base colour, normal, packed
|
|
602
|
+
occlusion-roughness-metallic, emissive. Each material gets a tile (materials with the same maps and
|
|
603
|
+
factors share one); its FACTORS are baked into the tile — base colour factor in linear light,
|
|
604
|
+
roughness / metallic into the ORM channels, normal scale, emissive factor, occlusion strength — so
|
|
605
|
+
the merged material is factor-free and reads as the parts did; a material without a map gets a flat
|
|
606
|
+
tile of its factor; UVs are remapped into the tile; every tile is padded by `--atlas-pad` (8)
|
|
607
|
+
texels of edge replication so bilinear filtering and the first mips stay clean.
|
|
608
|
+
- **Tile sizes.** The atlas is the smallest power-of-two square (≤ `--atlas-size`, default 2048)
|
|
609
|
+
that holds every map at full size. When they do not fit, tiles shrink **by surface area**: each
|
|
610
|
+
material's area on the model (its triangles in bind / world space) over its UV coverage gives the
|
|
611
|
+
texel density its tile would have, and the tiles are sized so every part lands at the same
|
|
612
|
+
texels-per-metre, none past its native map — a torso keeps its map, a buckle gets the small tile
|
|
613
|
+
it needs. On the HorizonDemo hero at 2048 that is jacket and trousers at 89 %, straps and buckles
|
|
614
|
+
at 9 %; `--atlas-size 4096` keeps everything native. `--atlas-uniform` scales every tile by the
|
|
615
|
+
same factor instead. The report prints the range (`tiles 9–89% by area`).
|
|
616
|
+
- **Order.** The pass runs before the LOD / card passes (they see the merged primitive)
|
|
617
|
+
and before the texture passes: the atlases are written as PNG and go through `--ktx2` and the
|
|
618
|
+
format normalisation like any map — except that an atlas is capped by `--atlas-size`, not
|
|
619
|
+
`--max-size` (the per-part cap), so `--merge --ktx2 --max-size 1024` keeps a 2048² atlas.
|
|
620
|
+
- **Sources.** Run it from the source textures (PNG / JPEG) — a build that ends in the doctor just
|
|
621
|
+
adds the flag. A GLB that already carries KTX2 maps works too, decoded through KTX-Software's
|
|
622
|
+
`ktx extract` (`KTX_TOOL`, next to `KTX_TOKTX`, or found like the encoder), but that is a second lossy encode.
|
|
623
|
+
- **Attributes.** The merged primitive carries the union: positions, normals (computed for a part
|
|
624
|
+
without any), UVs, colours (white where a part had none), joints / weights; tangents only when every
|
|
625
|
+
part has them, a lightmap UV set (`TEXCOORD_1`) only when every part has one. Indices go 32-bit past
|
|
626
|
+
65 535 vertices.
|
|
627
|
+
|
|
628
|
+
What disappears: the parts' nodes (unless they still have children), meshes, skins, materials and
|
|
629
|
+
textures. Code that addressed a part by node name needs `--merge-exclude`; bones and empties
|
|
630
|
+
(`RH_WP`) are untouched, clips play as before. The merged skin's joint count is checked against the
|
|
631
|
+
bone cap (`--target`).
|
|
632
|
+
|
|
633
|
+
```
|
|
634
|
+
🔍 chr1.glb
|
|
635
|
+
── MERGE ──
|
|
636
|
+
• merge 21 primitive(s) over 15 material(s) → 2 draw call(s)
|
|
637
|
+
merged: 20 primitives, 14 material(s) → 1 (atlas 2048² base+normal+orm, 14 tiles (tiles 16–68% by area), pad 8), 61 joints in one skin
|
|
638
|
+
merge: 21 primitive(s) → 2 draw call(s)
|
|
639
|
+
merged: 20 primitives / 14 material(s) → 15129 vertices, 21150 triangles, atlas 2048² base+normal+orm (tiles 16–68% by area), 61 joints
|
|
640
|
+
💾 wrote chr1.glb (5880KB → 1510KB)
|
|
641
|
+
```
|
|
642
|
+
|
|
643
|
+
Without the flag the doctor still says what the merge would fold (`ℹ 21 primitive(s) over 15
|
|
644
|
+
material(s) → 2 draw call(s) with --merge`) and what it would skip.
|
|
645
|
+
|
|
486
646
|
## What `convert` does
|
|
487
647
|
|
|
488
648
|
- **Reads FBX with [ufbx](https://github.com/ufbx/ufbx)** compiled to wasm (`native/fbx_reader.c` +
|
|
@@ -517,7 +677,7 @@ Skipped in v1: cameras/lights, morph targets, NURBS.
|
|
|
517
677
|
|
|
518
678
|
## Verified with
|
|
519
679
|
|
|
520
|
-
`
|
|
680
|
+
`runtime/web` → `node anim-check/model-check.mjs anim-check/fixtures/<file>.glb --clip N`: loads the
|
|
521
681
|
GLB on the Filament wasm engine, binds every clip through the native Animator, screenshots the rest
|
|
522
682
|
pose and a playing clip (the mesh must draw and deform). `bun test` here covers the reader/converter
|
|
523
683
|
against the Mixamo sample set when `LECODES_FBX_SAMPLES` (or the default sample folder) exists.
|
|
@@ -530,7 +690,7 @@ with a reason, and the clips compared channel-by-channel against the pack's own
|
|
|
530
690
|
exists, plus format/math unit tests that always run. The texture path was checked against the
|
|
531
691
|
predecessor Python extractor with the same Oodle library: the images come out BYTE-IDENTICAL on
|
|
532
692
|
both packs (67 MB, 21 MB and 1 MB payloads), and the converted mannequin renders with its own
|
|
533
|
-
materials through `
|
|
693
|
+
materials through `runtime/web`'s Filament check.
|
|
534
694
|
|
|
535
695
|
Retargeting was tuned on the polyperfect *Low Poly Animated Animals* zoo (different authoring
|
|
536
696
|
conventions within one pack: AdvancedSkeleton `Root_M/Hip_L`, Maya `actor:skin_*` with ×100 bind
|
|
@@ -547,15 +707,32 @@ vendor/ufbx/ ufbx.h / ufbx.c (MIT)
|
|
|
547
707
|
src/wasm/fbxReader.js generated single-file Emscripten module (tracked; rebuild with build-wasm.sh)
|
|
548
708
|
src/fbx/ read.ts (wasm boundary + IR) · toDocument.ts · convert.ts (merge, write)
|
|
549
709
|
src/textures/ resolveImage.ts (sniff, TGA/BMP decode, PNG encode, external lookup)
|
|
550
|
-
src/doctor/ doctor.ts (analyze/apply/report) · reduceGlbBones · transcodeTextures · transcodeKtx2 · normalizeScale ·
|
|
710
|
+
src/doctor/ doctor.ts (analyze/apply/report) · reduceGlbBones · transcodeTextures · transcodeKtx2 · normalizeScale · cardTrim · lod · validateGlb (mirror of viewer's)
|
|
711
|
+
src/unwrap/ lightmapUv.ts (xatlas wasm: charts, groups, zone, LOD projection) · index.ts (whole files, in place — `unwrap` and `lecodes lightmap bake`)
|
|
712
|
+
src/doctor/merge/ --merge: classify (what can merge, as what) · bindSpace (one bind space for several skins) · atlasLayout (tiles by surface area) · atlasPixels (factors baked in) · geometry (the merged primitive + union skin) · plan · index (apply, cleanup)
|
|
551
713
|
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
714
|
tools/humanoid-survey.ts bind-pose survey of the humanoid zoo (arm drop / elbow / facing / proportions per rig)
|
|
553
715
|
tools/humanoid-phase3.ts every zoo clip onto every zoo target: planted frames, reach, twist, root motion (--strips for filmstrips)
|
|
554
716
|
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) ·
|
|
717
|
+
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) · sound.ts (SoundWave → WAV/OGG) · 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
|
|
718
|
+
src/unity/ unpack.ts (.unitypackage streaming) · guid.ts (GUID → file: unity-index.txt / .meta) · matFile.ts (.mat reader, binary fallback) · materialWriter.ts (one .mat → one glTF material, ORM packing) · materials.ts (the FBX-convert flow + prefab overrides) · image.ts · png.ts
|
|
719
|
+
scene import: yaml.ts · gameObjects.ts (the tree) · prefabTree.ts (nested prefabs + modifications) · fbxModel.ts (a model file as a prefab: ids, collapse, bakeAxisConversion) · propertyPath.ts · scene.ts (.unity → kinds + placements) · kindGlb.ts (one kind → one GLB) · materialSlots.ts (slot ↔ primitive by name) · lecodesLevel.ts + levelTerrain.ts (→ project files) · terrain.ts + serialized.ts (binary TerrainData) · foliage.ts · math.ts (the mirror-X frame)
|
|
557
720
|
src/props.ts --list-nodes / --keep / --split (subtree carving + re-origin)
|
|
558
721
|
src/mergeSkins.ts --merge-skins (skinned meshes onto one skeleton) · duplicateNodeNames
|
|
559
722
|
src/filmstrip.ts CPU-skinned PNG filmstrip renderer (probe --filmstrip; tools/filmstrip.ts wraps it)
|
|
560
|
-
src/
|
|
723
|
+
src/commands/ one file per command — a command is DATA: name, summary, flags, run (retarget/ is a folder); shared.ts = the flag groups several commands take (doctor passes, Unreal locations)
|
|
724
|
+
src/cli-kit/ defineCommand + the argv parser that reads a line AGAINST a command's flags + the generated help
|
|
725
|
+
src/probe.ts · src/cli.ts (runAssetsCli — what `lecodes assets` calls) · src/index.ts
|
|
561
726
|
```
|
|
727
|
+
|
|
728
|
+
## Adding or changing a command
|
|
729
|
+
|
|
730
|
+
A command is one `defineCommand({ name, summary, usage, description, flags, examples, run })` in
|
|
731
|
+
`src/commands/`, listed in `src/commands/index.ts`. Flags are declared with `str` / `num` / `int` /
|
|
732
|
+
`bool` / `list` (`bare:` lets a value flag stand alone — `--split` next to `--split dir`) and arrive
|
|
733
|
+
in `run` typed; `given` tells a bare flag from an absent one. There is no hand-written help and no
|
|
734
|
+
separate list of value-taking flags to keep in sync: a flag the code reads and the parser does not
|
|
735
|
+
know cannot exist, and an unknown flag on the line is an error (its value would otherwise be read
|
|
736
|
+
as an input file). `run` resolves to the exit code; throw `CliError` (1 = the work failed) /
|
|
737
|
+
`usageError` / `missing(…)` (2 = the line is wrong, the latter followed by the command's help).
|
|
738
|
+
`bun run build` afterwards — `lecodes assets` runs `dist/`.
|