@hypersoniclabs/helix-mcp 0.2.4 → 0.2.12

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.
Files changed (98) hide show
  1. package/README.md +81 -11
  2. package/dist/continuumCanary.d.ts +17 -0
  3. package/dist/continuumCanary.js +17 -0
  4. package/dist/continuumCanary.js.map +1 -0
  5. package/dist/server.d.ts +14 -1
  6. package/dist/server.js +4058 -162
  7. package/dist/server.js.map +1 -1
  8. package/dist/tsconfig.build.tsbuildinfo +1 -1
  9. package/dist/vehicleTools.d.ts +86 -0
  10. package/dist/vehicleTools.js +229 -0
  11. package/dist/vehicleTools.js.map +1 -0
  12. package/docs/avatar-face.md +115 -0
  13. package/docs/bridge.md +98 -0
  14. package/docs/bring-your-world.md +117 -0
  15. package/docs/catalog.md +69 -1
  16. package/docs/character-animation.md +442 -0
  17. package/docs/character-attachments.md +166 -0
  18. package/docs/character-world.md +785 -130
  19. package/docs/continuum.md +153 -0
  20. package/docs/items.md +73 -0
  21. package/docs/lighting-world.md +667 -0
  22. package/docs/locomotion-clip-spec.md +294 -0
  23. package/docs/manifest.md +31 -6
  24. package/docs/multiplayer-logic.md +460 -17
  25. package/docs/multiplayer-templates/chrono-orchard.md +36 -22
  26. package/docs/multiplayer-templates/collect-a-thon.md +28 -38
  27. package/docs/multiplayer-templates/collections.md +24 -24
  28. package/docs/multiplayer-templates/hangout.md +124 -111
  29. package/docs/multiplayer-templates/npc-wave.md +310 -0
  30. package/docs/multiplayer-templates/obby.md +13 -16
  31. package/docs/multiplayer-templates/persistent-progress.md +218 -0
  32. package/docs/multiplayer-templates/physics-bumper.md +27 -9
  33. package/docs/multiplayer-templates/physics-football.md +22 -8
  34. package/docs/multiplayer-templates/relic-bearers.md +12 -15
  35. package/docs/multiplayer-templates/server-motion.md +16 -19
  36. package/docs/multiplayer-templates/shooter-range.md +275 -0
  37. package/docs/multiplayer-templates/team-control.md +28 -15
  38. package/docs/multiplayer-templates/turn-arena.md +31 -22
  39. package/docs/multiplayer-templates/voice-radio.md +166 -0
  40. package/docs/multiplayer-templates/wave-survival.md +7 -8
  41. package/docs/multiplayer-templates/world-shop.md +240 -0
  42. package/docs/multiplayer-world.md +222 -129
  43. package/docs/npc-world.md +623 -0
  44. package/docs/publishing.md +108 -28
  45. package/docs/purchases.md +223 -0
  46. package/docs/scene-performance.md +64 -0
  47. package/docs/screenshots.md +140 -0
  48. package/docs/sdk.md +324 -5
  49. package/docs/shooter-worlds.md +537 -0
  50. package/docs/terrain.md +173 -0
  51. package/docs/upgrades.md +324 -0
  52. package/docs/vehicles.md +727 -0
  53. package/docs/world-inspect.md +156 -0
  54. package/docs/world-look.md +241 -0
  55. package/docs/world-recipe.md +65 -6
  56. package/package.json +15 -4
  57. package/skills/README.md +91 -0
  58. package/skills/helix-assets/SKILL.md +491 -0
  59. package/skills/helix-assets/references/asset-sources.md +143 -0
  60. package/skills/helix-assets/references/vault-api.md +105 -0
  61. package/skills/helix-avatar-qa/SKILL.md +85 -0
  62. package/skills/helix-avatars/SKILL.md +206 -0
  63. package/skills/helix-avatars/references/contract.md +166 -0
  64. package/skills/helix-avatars/references/dynamics.md +367 -0
  65. package/skills/helix-avatars/references/face.md +50 -0
  66. package/skills/helix-avatars/references/publish.md +76 -0
  67. package/skills/helix-avatars/references/qa.md +251 -0
  68. package/skills/helix-avatars/references/rigging.md +88 -0
  69. package/skills/helix-avatars/references/source-generated.md +190 -0
  70. package/skills/helix-avatars/references/source-model.md +90 -0
  71. package/skills/helix-avatars/references/source-rigid.md +90 -0
  72. package/skills/helix-avatars/references/source-vrm.md +61 -0
  73. package/skills/helix-gauntlet/SKILL.md +128 -0
  74. package/skills/helix-multiplayer/SKILL.md +150 -0
  75. package/skills/helix-multiplayer/references/dsl-capability-map.md +107 -0
  76. package/skills/helix-vehicles/SKILL.md +218 -0
  77. package/skills/helix-vehicles/references/addons.md +212 -0
  78. package/skills/helix-vehicles/references/appearance.md +339 -0
  79. package/skills/helix-vehicles/references/audio-import.md +138 -0
  80. package/skills/helix-vehicles/references/audio.md +580 -0
  81. package/skills/helix-vehicles/references/cabin.md +225 -0
  82. package/skills/helix-vehicles/references/host-manifest.md +174 -0
  83. package/skills/helix-vehicles/references/physics-from-specs.md +463 -0
  84. package/skills/helix-vehicles/references/publish.md +214 -0
  85. package/skills/helix-vehicles/references/qa.md +177 -0
  86. package/skills/helix-vehicles/references/reference-package.json +3481 -0
  87. package/skills/helix-vehicles/references/reference-package.md +69 -0
  88. package/skills/helix-vehicles/references/source-beamng.md +167 -0
  89. package/skills/helix-vehicles/references/source-concept.md +38 -0
  90. package/skills/helix-vehicles/references/source-model.md +100 -0
  91. package/skills/helix-vehicles/references/source-scratch.md +60 -0
  92. package/skills/helix-world-build/SKILL.md +376 -0
  93. package/skills/helix-world-build/references/config-gates.md +104 -0
  94. package/skills/helix-world-director/SKILL.md +210 -0
  95. package/skills/helix-world-qa/SKILL.md +371 -0
  96. package/skills/helix-world-qa/references/perf-budgets.md +240 -0
  97. package/skills/helix-world-qa/references/perf-handle.md +125 -0
  98. package/skills/helix-world-qa/references/visual-scorecard.md +85 -0
@@ -0,0 +1,251 @@
1
+ # Avatar QA: what to measure, what to look at
2
+
3
+ **The rule:** nothing ships until you have looked at the avatar **moving** in the real HELIX runtime,
4
+ close up. Every gate passed Cream Hoodie 1.0.0, and in that version:
5
+ - the chest dented with arm swing;
6
+ - a hem edge stretched 16 cm into a spike at the hip.
7
+
8
+ Every gate also passed a dozen Dreamer drafts that rendered flat white. A rest-pose render proves
9
+ almost nothing: **a T-pose or A-pose hides wrong skin indices**, because every bone sits at its bind.
10
+
11
+ Run every browser headless, on your own profile directory, with `--use-mock-keychain
12
+ --password-store=basic`. Record the GPU adapter you rendered with.
13
+
14
+ ## 1. Structure (offline, before upload)
15
+
16
+ Read the final GLB with `@gltf-transform/core` and assert each of these. Every one is a defect that
17
+ has already shipped.
18
+
19
+ | Check | Pass |
20
+ | --- | --- |
21
+ | Scenes vs skins | One skin per LOD scene, each scene self-contained. Normally 3 skins and 6 meshes. |
22
+ | Joint names | Identical in every LOD skin, including every auxiliary joint. |
23
+ | Weighted joint names per region | Head vertices on `head`/`neck_*`, hands on `hand_*`, no foot or thigh weight above the pelvis, no arm weight on the central torso. Read the **names**, not the indices. |
24
+ | Influences | ≤ 4 per vertex, summing to 1 ± 1e-3. No influence from a bone more than 0.3 m from the vertex. |
25
+ | Emissive | No material has an `emissiveFactor` above 0 without an `emissiveTexture`. |
26
+ | Metallic | No `metallicFactor` of 1 without a metallic map; glTF's default turns untextured parts black chrome. |
27
+ | Manifests | `helixDynamics`, `helixFace` and `helixAvatar` are identical on every scene. `helixDynamics` obeys every rule in `dynamics`. |
28
+ | Budgets | The Character Package's avatar validation passes: ≤ 60k triangles and ≤ 12 draws across all LODs, ≤ 10 MiB, ≤ 8 materials, ≤ 2048 px. |
29
+ | Short body | Height ≥ 1.35 m, or (pelvis − foot) / 0.877 < 0.65. |
30
+
31
+ ## 2. Static render (offline)
32
+
33
+ Render in headless three.js:
34
+ - `GLTFLoader` with `KTX2Loader` and the basis transcoder;
35
+ - a neutral environment;
36
+ - a ground plane at Y = 0.
37
+
38
+ Look at:
39
+ - a turnaround (front, back, both sides, both three-quarters);
40
+ - face, hands and feet close-ups;
41
+ - LOD1 and LOD2 at their switch distances (8 m and 15 m), checking that no part goes missing and
42
+ nothing becomes a shard or a smear.
43
+
44
+ Compare against the reference images at matched angles.
45
+
46
+ ## 3. Motion against a control body (offline, no GPU)
47
+
48
+ This is the check that catches rig bugs, and the one an agent most often skips. Run it before the
49
+ upload: once a version is published it cannot be deleted.
50
+
51
+ The control is the stock body on the same skeleton, played through the platform's own clips and the
52
+ engine's own dynamics solver. All three come from a throwaway character world:
53
+
54
+ ```
55
+ helix init /tmp/avatar-harness --kind character && cd /tmp/avatar-harness
56
+ npm install && helix install # the engine module lands in public/helix_modules/humanoid-character/
57
+ npm i --no-save @gltf-transform/core @gltf-transform/extensions
58
+ ```
59
+
60
+ `helix.lock.json` then names the asset pack: `systems[0].assetBaseUrl` for `humanoid-character`.
61
+ Download these into one directory:
62
+ - `<assetBaseUrl>/body-f-default.web.glb` and `<assetBaseUrl>/body-m-default.web.glb`, the control
63
+ bodies;
64
+ - `<assetBaseUrl>/anims/clip-meta.json`;
65
+ - `<assetBaseUrl>/anims/locomotion.idle.glb`, `locomotion.walk.glb` and `locomotion.run.glb`.
66
+
67
+ Use a browser user agent; a bare request can be refused. Then run this probe from the harness
68
+ directory, with the avatar, the control, and the clip directory as arguments:
69
+
70
+ ```js
71
+ // node probe.mjs <avatar.web.glb> <control.web.glb> <clipDir> [framesOut.json] (run inside the harness world)
72
+ import { readFileSync, writeFileSync } from 'node:fs';
73
+ import * as THREE from 'three';
74
+ import { NodeIO } from '@gltf-transform/core';
75
+ import { ALL_EXTENSIONS } from '@gltf-transform/extensions';
76
+ import { GLTFLoader } from 'three/examples/jsm/loaders/GLTFLoader.js';
77
+ const { DynamicsLayer } = await import('./public/helix_modules/humanoid-character/index.js');
78
+
79
+ async function load(path) { // Node cannot decode KTX2: drop textures, keep skins + extras
80
+ const io = new NodeIO().registerExtensions(ALL_EXTENSIONS);
81
+ const doc = await io.readBinary(new Uint8Array(readFileSync(path)));
82
+ for (const t of doc.getRoot().listTextures()) t.dispose();
83
+ for (const e of doc.getRoot().listExtensionsUsed()) if (/texture|basisu|webp/i.test(e.extensionName)) e.dispose();
84
+ const glb = await io.writeBinary(doc);
85
+ return new GLTFLoader().parseAsync(glb.buffer.slice(glb.byteOffset, glb.byteOffset + glb.byteLength), '');
86
+ }
87
+ const [avatarPath, controlPath, clipDir, framesOut] = process.argv.slice(2);
88
+ const avatar = (await load(avatarPath)).scenes[0], control = (await load(controlPath)).scenes[0]; // LOD0
89
+ const LIMBS = ['spine_03', 'head', ...['upperarm', 'lowerarm', 'hand', 'thigh', 'calf', 'foot'].flatMap((b) => [`${b}_l`, `${b}_r`])];
90
+ const bone = (root, n) => { let b = null; root.traverse((o) => { if (!b && o.isBone && o.name === n) b = o; }); return b; };
91
+ const canonical = new Set(); control.traverse((o) => o.isBone && canonical.add(o.name));
92
+ const meta = JSON.parse(readFileSync(`${clipDir}/clip-meta.json`, 'utf8'));
93
+ const report = {}, frames = {};
94
+ for (const name of ['locomotion.idle', 'locomotion.walk', 'locomotion.run']) {
95
+ const clip = (await load(`${clipDir}/${name}.glb`)).animations[0];
96
+ const speed = meta.find((m) => m.name === name)?.strideSpeed ?? 0, dt = 1 / 60;
97
+ const mixers = [avatar, control].map((m) => { const x = new THREE.AnimationMixer(m); x.clipAction(clip).play(); return x; });
98
+ avatar.position.set(0, 0, 0);
99
+ const dyn = new DynamicsLayer(avatar, { isLocal: true });
100
+ const aux = []; avatar.traverse((o) => { if (o.isBone && !canonical.has(o.name)) aux.push(o); });
101
+ // Each aux bone's aim, in its own local frame: toward its child bone, or the chain's endpoint.
102
+ const endpoints = new Map();
103
+ for (const sc of [avatar]) for (const ch of sc.userData.helixDynamics?.chains ?? []) {
104
+ let b = bone(avatar, ch.root); while (b && b.children.filter((c) => c.isBone).length === 1) b = b.children.find((c) => c.isBone);
105
+ if (b && ch.endpoint) endpoints.set(b, new THREE.Vector3(...ch.endpoint));
106
+ }
107
+ const aim = new Map(aux.map((b) => [b, (b.children.find((c) => c.isBone)?.position.clone() ?? endpoints.get(b) ?? new THREE.Vector3(-1, 0, 0)).normalize()]));
108
+ const rest = new Map(aux.map((b) => [b, b.quaternion.clone()]));
109
+ let worstAt = null;
110
+ const skinned = []; avatar.traverse((o) => o.isSkinnedMesh && skinned.push(o));
111
+ const edges = skinned.map((m) => { // unique edges with their bind lengths
112
+ const idx = m.geometry.index.array, pos = m.geometry.attributes.position, seen = new Set(), out = [];
113
+ for (let t = 0; t < idx.length; t += 3) for (const [a, b] of [[idx[t], idx[t + 1]], [idx[t + 1], idx[t + 2]], [idx[t + 2], idx[t]]]) {
114
+ const k = Math.min(a, b) * 1e7 + Math.max(a, b); if (seen.has(k)) continue; seen.add(k);
115
+ out.push([a, b, new THREE.Vector3().fromBufferAttribute(pos, a).distanceTo(new THREE.Vector3().fromBufferAttribute(pos, b))]);
116
+ }
117
+ return out;
118
+ });
119
+ let limb = 0, stretch = 0; const swing = {};
120
+ for (let i = 0; i < Math.round((2 * clip.duration) / dt); i++) {
121
+ mixers.forEach((x) => x.update(dt));
122
+ avatar.position.z += speed * dt; // carry the body forward so springs feel the anchor's velocity
123
+ avatar.updateMatrixWorld(true); control.updateMatrixWorld(true);
124
+ dyn.update({ dt, cameraDistance: null, gate: 1 }, 1);
125
+ avatar.updateMatrixWorld(true);
126
+ for (const n of LIMBS) {
127
+ const a = bone(avatar, n), c = bone(control, n); if (!a || !c) continue;
128
+ limb = Math.max(limb, THREE.MathUtils.radToDeg(a.getWorldQuaternion(new THREE.Quaternion()).angleTo(c.getWorldQuaternion(new THREE.Quaternion()))));
129
+ }
130
+ if (i * dt < 1) continue; // skip the spawn warm-up
131
+ for (const b of aux) swing[b.name] = Math.max(swing[b.name] ?? 0, +THREE.MathUtils.radToDeg(aim.get(b).clone().applyQuaternion(b.quaternion).angleTo(aim.get(b).clone().applyQuaternion(rest.get(b)))).toFixed(1));
132
+ if (framesOut && i % 6 === 0) (frames[name] ??= []).push(Object.fromEntries(aux.concat(LIMBS.map((n) => bone(avatar, n)).filter(Boolean))
133
+ .map((b) => [b.name, b.quaternion.toArray().map((x) => +x.toFixed(5))])));
134
+ if (i % 15) continue; // 4 Hz: worst edge stretch of the skinned surface
135
+ skinned.forEach((m, k) => {
136
+ const cache = new Map(), at = (v) => cache.get(v) ?? cache.set(v, m.getVertexPosition(v, new THREE.Vector3())).get(v);
137
+ for (const [a, b, len] of edges[k]) {
138
+ const d = at(a).distanceTo(at(b)) - len;
139
+ if (d > stretch) { stretch = d; worstAt = { mesh: m.name, vertex: a, bindY: +m.geometry.attributes.position.getY(a).toFixed(2), bindX: +m.geometry.attributes.position.getX(a).toFixed(2) }; }
140
+ }
141
+ });
142
+ }
143
+ report[name] = { limbDegVsControl: +limb.toFixed(2), stretchCm: +(stretch * 100).toFixed(1), worstEdgeAt: worstAt, dynamics: { active: dyn.active, rejected: dyn.rejected, ...dyn.cost }, swingDeg: swing };
144
+ }
145
+ if (framesOut) writeFileSync(framesOut, JSON.stringify(frames)); // local quaternions every 0.1 s, for rendering
146
+ console.log(JSON.stringify(report, null, 2));
147
+ ```
148
+
149
+ Read the report for each clip:
150
+
151
+ | Field | Meaning | Pass |
152
+ | --- | --- | --- |
153
+ | `limbDegVsControl` | Worst world-rotation difference of any limb bone against the control. | ≤ 2°. The stock body against itself reads 0.01 / 0.48 / 0.8° (idle / walk / run). That is the noise floor. More than 5° means a rest-frame or bone-map error. |
154
+ | `stretchCm`, `worstEdgeAt` | Worst growth of any mesh edge over its bind length, and where: the mesh, vertex and bind x/y. | Close to the control. Stock Base Female reads 1.3 / 2.1 / 4.4 cm. Loose cloth stretches more: Cream Hoodie's armpit and sleeve cloth shipped at 5.8 cm walking and 8.9 cm running, by the lane's own probe. **10 cm or more:** look up where it is. If it sits between two body parts (inner thighs, cuff to hip, chest to arm), it is a weight bridge, so fix the weights (`source-generated` step 5). If it is cloth stretching over a joint, render that frame and judge it. |
155
+ | `dynamics.active`, `rejected` | Whether the engine accepted the manifest. | `active: true` and `rejected: []`. A rejected manifest means **no physics anywhere**. |
156
+ | `swingDeg` | Peak deflection of each auxiliary bone's aim (toward its child, or its chain's `endpoint`) from rest, after warm-up. | Inside its cone, and enough to read. Restrained hood hair read 6–9°; visible long hair 10–25° walking. A value pinned at the cone limit while walking means too little inertia. Near 0° walking means too stiff to notice. |
157
+
158
+ The probe cannot see two things: a strand passing through the body or collar, and the look of it.
159
+ **Render the frames it solved.**
160
+ 1. Pass a fourth argument, for example `frames.json`. The probe writes every auxiliary and limb
161
+ bone's local quaternion every 0.1 s, per clip.
162
+ 2. In a **headless** three.js page (`GLTFLoader` with `KTX2Loader` and the basis transcoder), load the
163
+ avatar's LOD0 scene.
164
+ 3. For each frame, copy those quaternions onto the bones by name, then render a side, back and
165
+ three-quarter view of the head and shoulders.
166
+ 4. Look at the running frames for hair passing through the hood, collar or shoulders.
167
+
168
+ The live surfaces in step 5 are the final word.
169
+
170
+ ## 4. Platform visual QA
171
+
172
+ Run `visual_qa_item_hosted({ itemId })` (or `visual_qa_item` with a QA sign-in). It must pass, with
173
+ no override. What each assert measures, and how to fix a failure, is in
174
+ `read_skill({ name: "helix-avatar-qa" })`.
175
+
176
+ The gate has two stages. Stage 1 is the Try Now walk in the world. Stage 2 (route `avatar@3`) is the
177
+ Marketplace item page's own walk, seen from the true sides and back: it blocks arms bound too close to
178
+ the body (the walk adducts ~24° from the clip's A-pose, so bind them near the standard 36°), forearms
179
+ buried in the torso, a surface that opens at the side as an arm swings, stray strands, and a hung
180
+ silhouette or leaning posture. A `not measured` result fails the run. Your own run of step 3 does not
181
+ replace it: the probe's controls measure rig noise, the gate measures what a buyer sees.
182
+
183
+ **Open the contact sheet anyway**, especially the `motion.preview.*` side and back frames. A pass says
184
+ the avatar renders, walks and does not tear in those frames. It says nothing about:
185
+ - dynamics;
186
+ - the face;
187
+ - first-person hide;
188
+ - how close the avatar is to the reference.
189
+
190
+ Cream Hoodie 1.0.0 passed an earlier version of the gate with a 9 cm hip spike, so keep measuring
191
+ `stretchCm` yourself. Cream Hoodie Anime Girl 1.0.6 passed `avatar@2` with arms bound at 13-15° that
192
+ walked inside the hoodie, a torn side and a rigid hair slab; that is what stage 2 now blocks, and the
193
+ hair slab is still the critic's call on the side frames, not a number.
194
+
195
+ ## 5. Live close-ups, in both surfaces
196
+
197
+ Capture both. Each one uses a different code path.
198
+
199
+ - **The Marketplace item preview.** It runs every dynamics chain at 60 Hz with no budget, on the
200
+ WebGPU renderer.
201
+ - Headless macOS Chromium drew nothing for the earlier lanes: Tint "swizzle view instruction still
202
+ has usages after lowering". Cream Hoodie rendered headless on a real Metal adapter.
203
+ - Record the adapter. If you get no frames, render on a machine with a discrete GPU instead.
204
+ - **In-world, through Try Now.** Use a signed-in QA account in the test grounds.
205
+ - Check that the GLB sha256 the world downloaded matches your file.
206
+ - Record the ready time, distance walked, idle arm angle and sole range.
207
+ - Capture close-ups: face, hands, feet, and hair or tail at idle, at mid-stride walking, and
208
+ running.
209
+ - **Headless camera gotchas:**
210
+ - The camera starts behind the character, and pointer-lock orbit does not work headless. Walk
211
+ toward the camera for front shots, or re-aim the world camera from a render hook.
212
+ - Take walk frames at ≥ 92 % of the stride's own maximum foot separation. A mid-hop frame decided
213
+ one critic round.
214
+ - Record the engine version the world resolved while you test (the served `…/runtime/^0.3/…` redirect and the
215
+ bundle's `VERSION`). Worlds pick up the live `^0.3` range, so an engine release mid-run can change the result —
216
+ compare runs by that recorded version. Do NOT exact-pin helix.json to freeze it: publish refuses exact engine
217
+ pins. For a one-off local comparison use the visual-QA engine override (`HELIX_VQA_ENGINE_RUNTIME=<version>`).
218
+
219
+ ## 6. Blind critic against the reference
220
+
221
+ The bar is the real reference: photographs of the real robot, actor or prop, the source game's own
222
+ renders, or the concept art.
223
+
224
+ 1. Render a fixed set of 11–12 frozen frames per candidate, deterministic so that two runs diff to
225
+ 0. Include a turnaround, face, hands, feet and the dressed close-ups, plus a walk frame with
226
+ dynamics running.
227
+ 2. Give a fresh critic agent that never saw the build an X/Y pair with the labels shuffled. Keep the
228
+ key outside the packet.
229
+ 3. The critic must **pick a side before scoring**, with no ties.
230
+ 4. Keep the incumbent unless the challenger wins head-to-head. Always re-anchor on the true
231
+ incumbent: one lane paired two losers by mistake.
232
+
233
+ Every avatar lane exited on a **plateau** of 6–7/10, not a win. Generated sources plateau lower.
234
+ Report which exit you took.
235
+
236
+ ## What eyes caught that numbers missed
237
+
238
+ - A flat white avatar: an emissive factor left without its map.
239
+ - A smeared mouth and blocky hair: the importer's rebake.
240
+ - Forearms walking inside the hoodie, the side torn open as an arm swung back, back hair standing off
241
+ as a slab, a strand at the waist: seen only from the side in the Marketplace walk. Arms bound at
242
+ 13-15°, fused sleeve/torso triangles cut into holes, hair weighted to arm and clavicle bones.
243
+ - A dented chest with arm swing, and a spike at the hip: auto-rig weights. The verifier, the bind
244
+ check and visual QA all passed it.
245
+ - A 60 cm chest spike, invisible in the game's own clips: chest vertices weighted to a foot.
246
+ - Feet sinking 2–5.7 cm on a short robot: the short-body gate skipped retargeting.
247
+ - Hair smearing into the hood while running, but fine while walking.
248
+ - Ears swinging 100°+ at a sprint: loose inertia and no limit.
249
+ - Chrome rendering near black: metallic 1.0 under the world sky.
250
+ - A normal map reading as hammered foil: ETC1S on the normal map.
251
+ - Hard-surface LODs tearing into shards: collapse decimation.
@@ -0,0 +1,88 @@
1
+ # Rigging: getting any mesh onto `helix-humanoid@1`
2
+
3
+ The importer, `helix character import` (or MCP `import_character`), does the last step. It maps the
4
+ source skeleton onto the canonical 68 joints, conforms them to rest, builds LODs and KTX2, and splits
5
+ `FaceMesh`. Your job is to hand it a source it can map: a skinned mesh in the canonical frame, close
6
+ to the A-pose, with bones it can name.
7
+
8
+ ## Bone maps
9
+
10
+ - `--map <json>` takes `{ "name", "version", "comment", "map": { "<sourceBone>": "<helixBone>" } }`.
11
+ - Without `--map`, the importer picks MetaHuman when it recognises it, or else the best of its
12
+ built-in Meshy, Mixamo and Mixamo-FBX maps. A Meshy or Dreamer rig needs no map.
13
+ - If you author the rig yourself (in Blender, from a CAD tree or a game rig), **name the bones with
14
+ the canonical helix names** and pass an identity map. Leave fingers and twists unmapped if you did
15
+ not build them. The importer then leaves them at rest, and fingers ride rigidly on `hand_*`.
16
+ - `--fit source` keeps the model's own proportions and embeds the canonical skeleton at its joints.
17
+ Every lane used it. `--fit canonical` re-poses the model to canonical proportions instead.
18
+
19
+ ## The canonical rest, as data
20
+
21
+ To aim limbs at the A-pose, you need the canonical rest transforms: per bone, the parent, the
22
+ position and the rotation, at the 1.7 m canonical body's metre scale. Read them from a base avatar's
23
+ LOD0 skin: the stock `body-m-default.web.glb` or `body-f-default.web.glb`. Download them from the
24
+ asset pack of a throwaway character world; `qa` step 3 gives the commands.
25
+
26
+ Aim these at the canonical directions:
27
+ - **upper arm, forearm, thigh and calf:** by bone direction plus the elbow or knee hinge plane;
28
+ - **hands:** by direction plus the palm axis;
29
+ - **feet:** by **yaw only**, so the soles stay flat.
30
+
31
+ Then bake the pose into the mesh. If you skip this, `conformed.restAimed` does the aiming, and a
32
+ hard-surface part bound to a moved bone will shear.
33
+
34
+ ## Pick the binding that matches the source
35
+
36
+ | Source | Binding | Lane |
37
+ | --- | --- | --- |
38
+ | Rigid mechanical parts (a robot, CAD, armour plates) | **Rigid 1:1.** Every part is weighted 1.0 to exactly one bone. Joints sit at the mechanical pivots. Parts spanning two bones are split, or become **pistons**: each half is rigid to its own bone, so they telescope. Thin cables keep blended weights. | Unitree G1, C-3PO, T-800 |
39
+ | A properly skinned game character on an Epic- or Valve-style skeleton | **Keep its weights.** Rename the canonical joints and **fold every helper joint** into its nearest canonical ancestor. Helper joints are twist helpers, `deform_*`, `Dyn_*` physics joints, `attach_*`, `*_end`, metacarpals. The game's weights survive. | Daft Punk (Fortnite), Iron Man (Valve biped, shoulder pads split 50/50 between clavicle and upper arm) |
40
+ | Skinned, but posed in the mesh, or carrying bad weights | **Aim to the A-pose, then bind per part:** rigid if one bone carries at least 90 % of the part, piston if it spans two bones, blended otherwise. | T-800 |
41
+ | Meshy or Dreamer auto-rig | **Keep the weights, then repair them.** The auto-rig bleeds arm weight into the torso and hips. See `source-generated`. | Cream Hoodie |
42
+ | VRM | Do not rig. The bridge reads the file's own humanoid map. | See `source-vrm` |
43
+
44
+ **Weight sanity.** Apply these on every non-trivial source:
45
+ - **Drop any influence from a bone whose segment (from the joint to its child) is more than 0.3 m
46
+ from the vertex.** Measure to the segment, not the joint head: a long thigh's own vertices sit
47
+ further than 0.3 m from its head. MK11's chest vertices weighted
48
+ to the right foot drew a 60 cm spike, and it was invisible in the game's own clips.
49
+ - Hard-surface plates dominated by one bone at 75 % or more get one constant blend. Otherwise they
50
+ bend like rubber.
51
+
52
+ ## T-pose hides skin-index bugs
53
+
54
+ Wrong `JOINTS_0` indices look perfect at bind, because every joint is at its bind pose and the error
55
+ multiplies out to identity. They show only when a clip moves the bones. So read back, for each LOD:
56
+ - **the names** of the joints that carry weight on each region;
57
+ - whether any vertex is weighted to an implausible bone (a foot on the chest, a hand on the hip);
58
+ - the worst edge stretch over walk and run frames, compared against a base body playing the same clip
59
+ (see `qa`).
60
+
61
+ Most bad weights get past the platform gates:
62
+ - the backend verifier passed Cream Hoodie 1.0.0;
63
+ - the engine bind check passed it;
64
+ - visual QA passed it too, with a 9 cm spike at the hip while walking (16 cm running).
65
+
66
+ Route `avatar@3` now catches three rig faults in the Marketplace walk: **arms bound too near straight
67
+ down** (under 17°; standard 36°), **a surface that opens at the seams** as a limb swings, and **forearms
68
+ sunk in the torso**. Bind arms near the canonical A-pose, and never cut fused sleeve/torso triangles to
69
+ hide a bad weight; fix the weight (`source-generated` step 5). Spike and stretch faults still need your
70
+ own probe.
71
+
72
+ ## Proportion edits
73
+
74
+ - Scale to the real height: Unitree G1 1.32 m, C-3PO 1.67 m, T-800 1.88 m, Iron Man 1.98 m.
75
+ - Widen girdles only from measured reference proportions, and move whole limb chains rigidly. T-800's
76
+ shoulders were widened 12 % and its pelvis 8 %.
77
+ - A 5 % head scale-up read as stocky and lost the critic round.
78
+
79
+ ## Tooling
80
+
81
+ - Use headless Blender for posing, binding and baking.
82
+ - Use `@gltf-transform/core` for JSON and accessor surgery.
83
+ - Use `meshoptimizer` or the importer for LODs. **Hard-surface plates decimated with Blender's
84
+ collapse tear into shards.** Two fixes worked:
85
+ - per-region meshoptimizer ratios (T-800);
86
+ - voxel-remeshed closed shells (12 mm / 20 mm) for LOD1 and LOD2, baked the same way as LOD0
87
+ (Iron Man).
88
+ - Brackets that decimate badly can be replaced with convex hulls (Unitree G1).
@@ -0,0 +1,190 @@
1
+ # Source: generated characters (Dreamer image-to-3D, or any Meshy-rigged GLB)
2
+
3
+ Generated characters are the fastest source and the most deceptive. Every gate passed the first
4
+ Cream Hoodie build while it:
5
+ - glowed flat white in drafts;
6
+ - had a smeared mouth;
7
+ - had a dented chest;
8
+ - had a 9 cm spike at the hip.
9
+
10
+ Plan for every step below. The template lane is Cream Hoodie Anime Girl (helix3, 2026-10-03), the
11
+ first avatar through the visual-QA avatar route with no override.
12
+
13
+ ## 1. The input image
14
+
15
+ - Image-to-3D wants a clean **four-view A-pose turnaround**: front, back, left and right. A single
16
+ small illustration does not work; 214 × 395 px was too small. Make or edit a turnaround first.
17
+ - **Layout: 2×2, 1×4 or 4×1, in the order front, back, left, right** (row left to right, column top
18
+ to bottom, grid reading order), with **exactly one figure per view**. Dreamer finds each figure
19
+ against the background, cuts the sheet through the empty gutters between them, and **refuses the
20
+ upload before anything is created or charged** when it cannot: the error says how many figure
21
+ columns and rows it found (`found 3 figure column(s) …`) or which panel is empty or holds two
22
+ figures. A figure that touches or crosses its neighbour leaves no gutter, so keep a visible gap and
23
+ turn the two profiles to face opposite ways.
24
+ - **Make the figures stand out from the background.** Detection works from the colour distance to a
25
+ flat background; a cream outfit on light grey is only about 40 levels away, white on white is
26
+ invisible. Use a **mid or dark neutral grey** (or a clean flat colour unlike the clothes), no
27
+ gradients, no frames, no captions touching the figures. A clean sheet reads fine on light grey; a
28
+ pale one on a pale background does not.
29
+ - **Open the sheet and look at it before you generate:** four panels, one figure each, a visible gap
30
+ between them. Passing the layout check is not a promise the semantic gate agrees on which view is
31
+ which.
32
+ - Why this is written down: on 2026-10-06 a 1536×1024 four-in-a-row sheet of a cream figure on light
33
+ grey was read as a 2×2 grid, Meshy was given the front and back figures as one view and built a
34
+ two-figure mesh (123 LIX). The detector has been fixed; the same sheet is now read as 1×4. When you
35
+ cannot get a clean row, re-lay the figures yourself as a 2×2 on a darker grey (that is what worked).
36
+ - Lower legs, boots and the back that the source does not show are inferred. Say so in the item
37
+ description if they matter.
38
+ - **Generative variance is real.** A second generation from a corrected sheet lost to the first, 5.0
39
+ vs 7.0. Do not re-run hoping for luck: fix what you have.
40
+
41
+ ## 2. Generate
42
+
43
+ ```
44
+ generate_asset({ kind: "character", prompt, title, referenceSheetPath: "/abs/sheet.png", visibility: "private" })
45
+ # CLI: helix assets generate-reference sheet.png "<prompt>" --visibility private
46
+ ```
47
+
48
+ The server runs mesh, texture, rig, LOD, thumbnail and publication; 123 LIX in the lanes. Two
49
+ problems with the draft it publishes:
50
+
51
+ - **Visibility.** Pass `visibility: "private"` for a draft you will rebuild. Until 2026-10-03 the
52
+ server ignored it and listed drafts publicly. That is fixed, but confirm with `search_items` after
53
+ generation anyway. A raw draft that did go public gets `delist_item` before anyone buys it.
54
+ - **The draft is not your avatar.** The server imported it with the automatic rebake. Treat the draft
55
+ as a source.
56
+ - For a hero result, rebuild from the **raw rigged GLB** with the steps below.
57
+ - Dreamer does not keep the raw rig; a platform operator can re-fetch it by the job's rigging task.
58
+ - If you cannot get it, repair the draft in place, applying steps 4–6 to every LOD. The rebake
59
+ damage stays.
60
+
61
+ ## 3. Strip the emissive before import
62
+
63
+ The raw material carries a **black emissive map with `emissiveFactor [1, 1, 1]`**.
64
+
65
+ - An importer that drops the map but keeps the factor renders the **whole avatar glowing flat
66
+ white**. This was a stop-ship.
67
+ - That is fixed in the importer: CLI `0.1.18-helix3.351` and later never keep an emissive factor
68
+ whose map was dropped, and strip flat-black emissive maps. The visual-QA gate's emissive wash-out
69
+ assert also fails it now.
70
+ - Strip both from the source anyway. It costs nothing, and it keeps the material honest for any tool
71
+ you run before the importer.
72
+
73
+ After import, check that no material keeps an `emissiveFactor` without an `emissiveTexture`.
74
+
75
+ ## 4. Import without the rebake
76
+
77
+ ```
78
+ helix character import raw.glb -o out --name <slug> --lod-tris 30000,15000,5000 --tex-size 2048 --no-bake --ktx-mode etc1s
79
+ ```
80
+
81
+ - `import_character` passes `bake: false`, `ktxMode` and `lodTris`. The default Meshy bone map is
82
+ picked automatically.
83
+ - The source's micro-chart atlas **triggers the automatic weld, unwrap and rebake**. On Cream Hoodie
84
+ that smeared the mouth into the chin and turned hair strands into blocks. `--no-bake` keeps the
85
+ source UVs, and it beat the rebake 6.5 to 5.0 in the blind critic round.
86
+ - ETC1S at 2048 kept Cream Hoodie at 7.06 MB, normal map included. That is fine for soft cloth
87
+ and skin. The "hammered foil" ETC1S artefact (`source-model`) shows on glossy hard surfaces, so
88
+ judge it in close-ups.
89
+ - A generated source has no external URL or licence. Leave `--provenance` off, and record the Dreamer
90
+ job id in your ledger.
91
+
92
+ Texture retouching (colour grading cloth or irises, projecting a clean face) is done on the source
93
+ atlas before import. Gain-match it to the skin, and judge it in the critic loop. Colour-only tweaks
94
+ oscillated between "too warm" and "too cold".
95
+
96
+ ## 5. Repair the auto-rig weights
97
+
98
+ Three Meshy defects only show in the Marketplace walk, seen from the side and back
99
+ (`avatar.bind_pose`, `avatar.preview.*`, see `helix-avatar-qa`):
100
+ - **Arms bound near straight down** (13-15° on the hoodie; standard is 36°). The walk adducts the arm
101
+ ~24° from the clip's A-pose, so the forearms walk inside the body. Re-bind the arms near Base Female's
102
+ A-pose (the rework used +6° for oversized sleeves): a rotation-only re-bind of upperarm/lowerarm,
103
+ baked into POSITION/NORMAL/TANGENT, with IBMs and node rotations rewritten on every LOD.
104
+ - **Arm and clavicle weights in the hair.** Hair belongs to the head and the hair chains only. A big share
105
+ of the mesh on a few stiff joints reads as a rigid slab behind the back.
106
+ - **Neck and head bound off canonical**, so the head droops or pushes forward with the walk.
107
+
108
+ The auto-rig blends arm weight into the torso and fuses sleeve cuffs to the hips. Cream Hoodie's raw
109
+ rig had:
110
+ - 4,121 central-torso vertices with more than 10 % upper-arm weight, so the chest dented with every
111
+ arm swing;
112
+ - hem vertices weighted to the forearm and thigh, one at 0.51 / 0.32, which stretched an edge 9 cm
113
+ walking and 16 cm running. It showed as a thin line out of the hip in the Marketplace preview.
114
+
115
+ The fix that worked:
116
+ 1. Change only vertices that carry **both** arm weight (upper arm, lower arm, hand, fingers) **and**
117
+ body weight (spine, pelvis, legs, clavicle).
118
+ 2. Classify each one by **geometry**. It is arm surface if it lies within sleeve radius of its side's
119
+ arm bones (0.095 m for the upper arm, 0.08 m for the forearm and hand) **and** its normal faces
120
+ away from the arm axis. Otherwise it is body.
121
+ 3. Classify per **welded vertex group**, not per vertex. Seam duplicates have different normals, so
122
+ classifying them separately leaves 0.5 / 0.5 blends. Then diffuse the arm share over the welded
123
+ mesh so the boundary does not crease, but **only above the hem**. Diffusing across the cuff-to-hip
124
+ contact spread the bridge and made the stretch worse: 17 / 28 cm. The cut-off was about
125
+ y = 1.2 m on a 1.75 m body; find yours by sweeping it against the probe.
126
+ 4. Keep the old relative weights inside each group.
127
+ 5. **Do not cut the fused sleeve/torso surface.** The 1.0.x lane cut the cuff-to-hip bridge below the armpit
128
+ (364 / 203 / 79 triangles per LOD). It was real cloth: the cut left holes, and the hoodie's side tore open as
129
+ an arm swung back in the Marketplace walk (`avatar.preview.surface_closed`, 36 cm). Keep every triangle and
130
+ fix the **weights** so the sleeve follows the arm and the hem the hip. Hard or geodesic arm/body splits and
131
+ cut-and-cap also tore the shared armpit and hem (10-23 cm); the rework that passed kept the surface whole,
132
+ re-bound the arms (above) and left a residual cuff/hem stretch of ~10 cm, hidden behind the arm.
133
+ 6. Rewrite `JOINTS_0` and `WEIGHTS_0` **in place**, identically on every LOD. Leave `POSITION` alone.
134
+
135
+ **Hands fused to the thighs.** Meshy generates in an A-pose with the hands resting on the thighs, and
136
+ its mesher merges the two surfaces. A strip of triangles then runs from the inside of the hand and
137
+ forearm to the outside of the thigh, and the rigger spreads thigh weight over the hand and hand weight
138
+ over the thigh. Standing, it looks fine. In the walk the arm swings forward as the leg swings back, and
139
+ the strip tears into a sheet from the wrist to the knee, or into a "sleeve sheet" from the forearm.
140
+ - Measure it before trusting a render. Count triangles with one corner owned (>= 0.5 weight) by an arm
141
+ and another by a leg. Clean Meshy rigs have 0–8. Dreamer item 4400fe58 had 342: skin_intact read
142
+ 4.5% stretched (limit 1.5%), and the critic saw a smeared sheet from the arm to the rear leg.
143
+ - The importer repairs it. `helix character import` cuts the bridge, closes both holes, and strips the
144
+ other limb's weight from each side, on the full mesh and again on every LOD. It is on by default
145
+ (`--limb-bridges repair`); `--limb-bridges report` logs what it would change and changes nothing.
146
+ It sides contact vertices by geometry, because the bled weights cannot tell the thigh under the hand
147
+ from the hand. It keeps the cut only when a two-stride stretch measure falls. It never cuts any other
148
+ pair, so skirts, pockets, capes and sleeves are left alone.
149
+ - A Dreamer draft built before the importer had this step keeps the defect. Re-fetch the raw rig and
150
+ re-import it. 4400fe58 re-imported this way fell from 4.5% to 0.09% edge stretch and passed visual
151
+ QA.
152
+ - The caps are flat patches under the arm, in one cloth colour. They are hidden in normal play. Look
153
+ at them close up anyway: a cap that folds or takes a seam texel reads as a dark shard at the hip.
154
+
155
+ **The legs fuse too.** Inner-thigh vertices often carry both `thigh_l` and `thigh_r` weight. That
156
+ gave 14 cm of stretch at a run after the arms were fixed. Snap each welded group to the leg on its
157
+ side of the mid-plane, and cut the crotch bridge triangles. Check the front of the crotch in a
158
+ render afterwards: the cut can leave a small notch.
159
+
160
+ Measure the result against a base body; see `qa`. Torso vertices with upper-arm weight fell from
161
+ 4,121 to 169. Worst stretch fell from 9.1 to 5.8 cm walking and from 16.1 to 8.9 cm running, against
162
+ Base Female's 0 and 4.4 cm on the same probe.
163
+
164
+ ## 6. Dynamics
165
+
166
+ A generated character is **one fused shell**: hair, hood, face and body are welded together. So:
167
+ - select hair by its baked colour per LOD, and blend weights through a diffused field;
168
+ - anchor back hair on the bone it rests on (`spine_05` on a hood);
169
+ - **drop front locks on hooded or high-collar characters**. They smeared into the collar while
170
+ running.
171
+
172
+ The full method and values are in `dynamics`.
173
+
174
+ ## 7. What you cannot fix, so say so
175
+
176
+ - **Some outfits come out pale and glossy.** The material ships roughness around 0.45, so a
177
+ cream or white hoodie reads washed out and near-glowing under the engine's lights, against its own
178
+ albedo. The critic fails `avatar.textures` for it, and its verdict varies run to run. Check the
179
+ roughness the import kept before blaming the lights.
180
+ - **Thumbnails may come back flat-shaded.** The server thumbnail renderer may draw triangles
181
+ flat-shaded, which makes a smooth avatar look faceted in listings. That is still being checked.
182
+ Judge shading in the visual-QA frames, not in the thumbnail.
183
+
184
+ - The face carries no blink or viseme channels: `face: none`. The avatar is mute in voice chat. A
185
+ projected, calm closed-mouth face texture reads better than a smeared one.
186
+ - Generated texture flaws (smeared eye or brow texels, ribbed hair, melted laces, fringe texels)
187
+ survive every tuning pass. Both generated lanes exited their critic loop on a plateau of 5.8–7.0,
188
+ not a win. Report that honestly.
189
+ - Toes usually point about 25° off forward because of where the foot joint was placed. It is a
190
+ visual-QA **warning**, not a failure. Check the feet in the walk frames.
@@ -0,0 +1,90 @@
1
+ # Source: a rigged game or downloaded character (rips, mods, Sketchfab, FBX/GLB)
2
+
3
+ Lanes: Daft Punk (Fortnite rips), T-800 (a Mortal Kombat 11 model from the SFM workshop), Iron Man
4
+ Mark III (a Garry's Mod player model).
5
+
6
+ ## Acquire, and score the candidates
7
+
8
+ - **Prefer a skinned, textured source on an Epic- or Valve-style biped.** The deciding features are:
9
+ - real skinning;
10
+ - textures at 1–2k;
11
+ - the right variant (outfit, mark, era);
12
+ - enough triangles to decimate from: 100k–450k worked.
13
+ - **Reject** these:
14
+ - a low-poly console rip when a better source exists (Iron Man passed over a 10k PS3 rip);
15
+ - a model of the wrong variant;
16
+ - a model you cannot download.
17
+ - **Tools that worked:**
18
+ - Workshop items: `steamcmd +login anonymous +workshop_download_item <app> <id>`.
19
+ - Source-engine `.mdl`: Blender with SourceIO.
20
+ - Textures: `srctools` to decode VTF.
21
+ - A viewer-only web model: hook WebGL in a headless page and decode its buffers. This reproduced
22
+ C-3PO's 120,988 triangles exactly.
23
+ - **Record what is missing.** Daft Punk's sequin outfit was separate geometry the rip lacked, so the
24
+ leather outfit shipped. Do not invent parts.
25
+
26
+ ## Pose and rig
27
+
28
+ Choose by what the source gives you. The full table is in `rigging`.
29
+
30
+ | Source | Approach |
31
+ | --- | --- |
32
+ | Clean game skinning on a mannequin-like skeleton (Fortnite) | Keep the weights. Rename the 68 canonical joints, and fold every helper (twist helpers, `deform_*`, `Dyn_*`, `attach_*`, metacarpals, `*_end`) into its nearest canonical ancestor. |
33
+ | A posed mesh with dubious weights (MK11) | Aim the limbs at the canonical A-pose, then bind **per part**. Drop influences from bones more than 0.3 m away. |
34
+ | A Valve biped (GMod) | Re-pose to the A-pose: aim each segment, roll the hands onto the canonical palm axis (60 % forearm twist, 40 % wrist), and **yaw the feet only**. Then build a fresh rig with the Valve weights renamed. Split shoulder pads 50/50 between clavicle and upper arm, and make plates rigid at a 75 % dominant share. |
35
+
36
+ Before import, lift the soles to just above Y = 0. T-800 used 1 cm and Iron Man 8 mm, measured
37
+ against a canonical idle.
38
+
39
+ ## Materials
40
+
41
+ Convert the game material model to glTF PBR and keep it to **1–2 materials**, because draws are
42
+ paid once per LOD.
43
+
44
+ | Source maps | glTF |
45
+ | --- | --- |
46
+ | Diffuse | Base colour |
47
+ | Spec/gloss or a packed S map (R = spec, G = roughness, B = metallic) | ORM |
48
+ | DirectX-convention normals | **Flip green** to the OpenGL convention |
49
+ | Glowing parts: visors, eyes, reactors, displays | An emissive map. Paint animated displays the export lost, through a UV-to-surface probe. |
50
+
51
+ Lessons from the critic rounds:
52
+ - Fully metallic 1.0 chrome renders **near black** under the world sky. 0.9–0.92 beat 1.0.
53
+ - A slightly darker chrome lost its round. Keep the source's value unless the reference says
54
+ otherwise.
55
+ - **The normal map needs UASTC.** An ETC1S normal or base map reads as hammered foil under the
56
+ marketplace's room lighting. ETC1S is fine for ORM, emissive, and every LOD1/LOD2 map.
57
+
58
+ ## Geometry and LODs
59
+
60
+ - Cull what can never be seen. An embree visibility pass kept 53 % of Iron Man's triangles.
61
+ - Close see-through gaps between plates with a dark undersuit shell. A near-black underlayer read as
62
+ holes in the critic round; gunmetal won.
63
+ - LOD0 can be a baked low-poly. Use Blender collapse with area-weighted normals, then a Cycles bake
64
+ of base colour, normal, ORM and emissive onto an xatlas atlas.
65
+ - **Hard-surface LOD1 and LOD2 tear into shards when decimated directly.** Use either:
66
+ - per-region meshoptimizer ratios (T-800: head 0.55, hands 0.6, feet 0.3, body 0.3); or
67
+ - voxel-remeshed closed shells at 12 mm and 20 mm, decimated and baked the same way (Iron Man).
68
+ - **The importer's own LODs scramble a many-chart atlas.** On Iron Man they kept only 49 % and 37 %
69
+ UV coherence. Fixes:
70
+ - pass `--no-bake`;
71
+ - pass `--lock-border` to stop seams opening;
72
+ - or build LOD1/LOD2 yourself and swap them into the imported file by joint name, then run the
73
+ importer's own finishing checks again (self-contained scenes, canonical geometry, split face).
74
+
75
+ ## Import
76
+
77
+ ```
78
+ helix character import src.glb --map identity-map.json --fit source -o imp --name <slug> \
79
+ --lod-tris 48000,8000,3900 --tex-size 2048 --ktx-mode etc1s --ktx-quality 255 --no-bake --lock-border
80
+ ```
81
+
82
+ `import_character` exposes these flags too: `map`, `fit`, `bake`, `lockBorder`, `ktxMode`. Stay under
83
+ 60k triangles across all LODs, 12 draws and 10 MiB. Iron Man shipped at 9.75 MiB with a UASTC normal
84
+ map.
85
+
86
+ ## A helmet or a mask
87
+
88
+ Keep a helmet as its own mesh if it needs its own material, and list it in
89
+ `helixAvatar.firstPersonHide` on **every scene**, for example `["HelmetFaceMesh", "FaceMesh"]`.
90
+ Otherwise the first-person camera looks out through the inside of the helmet.