HumankindAssetFramework

Model Factory — User Manual

How to put your own 3D model onto a Humankind unit, step by step. This is the practical guide.

The Factory is a Unity editor window (Tools ▸ Model Factory). You give it a model file and a target unit, set a few options, press Bake, then rebuild the mod. The in-game plugin reads what you baked and renders it.

Shipping a standalone pack? The runtime is a multi-mod host — you can distribute your models as their own pack (a config file + assets) that merges on top of ENC without editing it. See Multi-Mod.md for the pack format and the haf_packs/ drop folder.


1. Prerequisites


2. Quick start (static model)

  1. Tools ▸ Model Factory.
  2. Pawn description → Pick — choose the unit your model replaces (e.g. Era6_Common_Hovercrafts_01). A Resource name is suggested; keep or edit it.
  3. Model file → Browse — pick your .glb/.obj/.fbx.
  4. Set Size (world length of the longest axis) and, if needed, Rotation / Position.
  5. Bake. Watch the Console for [Factory] <name> DONE. skeleton=… atlas=….
  6. Rebuild the mod (see §6) and relaunch.

That’s the whole loop. Everything below is detail and the animated workflow.


3. The window, field by field

Settings — game & Blender path (foldout at the top)

3D resource

Resource name / Pawn description / Model file

Animation (summary only — settings live in the Animation Lab, §15)

Since 2026-07-18 the Factory no longer edits animation settings itself. The two windows are mutually exclusive in their settings and work together:

Transform

Mesh / shading (ignored in Animated mode)

Texture / import

Bake / Reset

Preview panel

An interactive 3D preview is embedded near the bottom of the window (drag to orbit, scroll to zoom). It auto-updates after every Bake and when you pick a resource from the 3D resource dropdown — so you see the baked result in the window, no hunting in the Project view. It renders the baked prefab: <name>_Preview.prefab for animated models (a static, textured, upright copy of the injected mesh — not itself injected) or <name>_Model.prefab for static models. Use it to judge geometry, skin, and — with the Reduce to ~tris field + the Console vert/tri count — to dial in the lowest triangle count with no visible loss: drop the count → Bake → watch the model degrade live, then step back up.


4. Static model workflow

  1. Pawn description (Pick) + Resource name.
  2. Model file (Browse).
  3. Textured model? leave Convert grid = 0 (preserves UV seams). Untextured CAD model? consider Height-based UVs or a Convert grid > 0.
  4. Renders invisible / see-through in-game? it’s a single-sided/CAD mesh — enable Winding fix, or Double-sided for non-convex shells.
  5. Heavy model? set Reduce to ~tris (default 24000). Overflowing the shared buffer drops geometry silently.
  6. Set Size / Rotation / Position. Bake → rebuild mod → relaunch. Tweak and re-bake (Model file empty) as needed.

5. Animated model workflow

Your model must be rigged with a skeletal animation (an armature + at least one clip). glTF/GLB is easiest (the clip and bone pickers read it directly).

Raw / auto-rigged download (Sketchfab etc.)? Read §16 — Converting an animated model first: it explains the engine’s hard constraints (rotation-only clips, scale-1 skeletons, name-ordered bones), the automatic conversion the Factory performs, and the troubleshooting map for floating heads, smearing parts, and wrong orientations.

Only a STATIC vehicle model (no rig at all)? Tools ▸ HAF ▸ Vehicle Lab builds the rigged Spin GLB for you — probe the parts, mark wheels & turret (keyboard review, recipes, a Verify report), press Vehicleize, and the output feeds this exact workflow. Full guide: Animated-Models.md“Authoring the spin rig”. Verified in-game: the shipped ArmouredCar runs a Lab-generated rig. Tanks: mark the tread loop C (Caterpillar) and the barrel G (Gun) — the Lab instances the track as rigid links riding a measured belt path (see “Caterpillar tracks — treadize” there; bake with Keep bone translations ✓). UX (2026-08-01): recipes load from the top-row Edit existing dropdown (<new model> starts fresh); Wave rock (idle sway for floating hulls, e.g. the canoe) has an Enable checkbox, off by default — leave it off for wheeled/tracked units. After Vehicleize, the Animation Lab’s Auto-detect button fills the bake config from the generated Spin clip in one click. Part isolation: the height bracket (Hide below/above) is joined by a left/right slice (Hide left/right of (side) on the width axis) — bracket both to isolate one wheel, then mark it. All slider defaults are padded so nothing hides until you drag.

  1. In the Factory: Pawn description — choose a donor with no animated sub-parts and a full idle/move set (a land vehicle is ideal; an attack-helicopter donor forces its rotor onto your model). Resource name.
  2. Model file — the rigged .glb/.fbx. When detection finds animation you’ll see “Animation detected” with an Open Animation Lab button — press it (the Lab opens docked next to the Factory, pre-filled with this model).
  3. In the Animation Lab: Clip name → Pick → choose the loop you want (e.g. hover). Not the exploded/assembly clips.
  4. Animate only bones → Pick → choose the spinning group (e.g. prop). This keeps only those bones animated and freezes the rest, killing camera/body wobble. Leave empty for a fully-animated model (a walker, a turret).
  5. Size (back in the Factory — it owns the model’s transform/size) — the drone is small; try 4. The Console logs the computed Scale Factor.
  6. Make sure Blender is detected (Settings). Bake (either window). Watch for:
    [Factory] <name> FBX scale factor … (native longest … -> <Size> units)
    [Factory] <name> ANIMATED DONE. skeleton=… clip=… atlas=…
    

    A non-zero clip= means the animation baked correctly.

  7. Rebuild the mod (§6) and relaunch. The model should render and play its clip at real speed.

Iterating on an animated model (2026-07-18 — geometry caching is AUTOMATIC): the Blender re-slim runs exactly when one of its inputs changed — Rotation, Reduce-to-tris, Clip, Animate-only-bones, Material mode, or the Model file — and is skipped otherwise (fast). You never manage this. The checkbox (now labelled “Keep extracted texture (hand-edits)”) has ONE job on the animated path: protect a hand-edited extracted albedo from being regenerated when the re-slim runs. (Earlier today it also gated the geometry, which made Rotation silently unresponsive while ticked — that trap is gone.) For static models it additionally reuses the extracted OBJ, as before.


6. After baking: rebuild the mod (don’t skip this)

Baking writes assets into the Unity project + the registry, but the game loads your mod’s built AssetBundle. Your baked skeleton / clip / atlas won’t reach the game until you rebuild/export the mod (your normal Humankind mod build step). Then relaunch. This is the #1 “why didn’t my change show up” cause.


7. Presentation tips — make the unit read well

A few game-data touches (in Databases, not the Factory) that make an injected model land better on the battlefield. These are PresentationUnitDefinition / UnitDefinition edits — independent of the model bake, and they need a mod rebuild.

Formation = a swarm (visibility + presence). The unit’s PresentationUnitDefinitionPresentation Formation Definition controls how many dummy figures are drawn for the one unit. Switch a recon drone to Formation_Wedge_3 and it renders as three drones in a wedge — far more visible on the map, and it reads as less fragile (a coordinated swarm vs a lone scout). It composes for free with an injected animated model: every formation dummy is the same pawn descriptor, so the plugin’s multi-instance handling gives each one our skeleton + clip — all of them render and spin. Tune spacing with Dummy Offset Position / Angle.

Purely visual — not stronger. Formation dummies are cosmetic; they add zero HP or damage. It’s still one unit (one health bar, one combat roll; the figures move / fight / die together). If you want it to be as sturdy as it looks, edit the gameplay stats in its LandUnitDefinition (combat strength / health / defense) — a separate Databases edit.

Altitude (animated only). The registry position.z raises an animated model at runtime (§5) — set it high enough that the swarm clears tall city buildings and stays above the terrain. Combined, formation + altitude make the unit read clearly at every zoom: close-up spinning props, mid-range city-skyline flyover, and an identifiable banner on the strategic map.


8. Troubleshooting

Symptom Cause → fix
Model invisible / see-through Single-sided/CAD mesh (backface-culled) → Winding fix or Double-sided. Or it overflowed the shared buffer → lower Reduce to ~tris.
Model tiny (a speck) or huge Size is the world length — set it to what looks right; the Console logs the scale.
ANIMATED model bakes huge & floats high in the sky (fine in the Factory preview, wrong only in-game) The rig’s FBX embeds a metre→centimetre unit scale the SDK skeleton over-applies → ~100× oversize. Tick “Fix 100× oversize (FBX unit scale)” (Animation section) and re-bake at the real Size — the baker measures the FBX at true scale then bakes with the unit scale on, so Size = in-game units. It’s a per-model toggle (no universal rule: some exports need it, some break with it).
ANIMATED model vanishes / shrinks to a speck after ticking “Fix 100× oversize” That model’s FBX does not carry the metre→cm scale, so the fix over-shrinks it. Untick “Fix 100× oversize” and re-bake — most rigs (e.g. the drone) bake correctly with it off.
DEPLOY-CONVERT model (deployConvert ON): which way for Fix 100×? deploy_convert auto-selects a path by part count: small rigs (≤124 parts — the m114 howitzer class) take the LEGACY path (DeployArm, cm verts) → Fix 100× ON; huge rigs (T-62 class) take the CONTRACT path (DeployArmV2, meter verts) → Fix 100× OFF. The Console logs DEPLOY path: LEGACY/CONTRACT. If a deploy model is a speck with Fix 100× ON it’s on the contract path (turn it OFF) and vice-versa. The contract path also runs bone-slimming + a delta-form rebase, gated to big rigs — applying them to a small rig re-breaks it (invisible / crossed legs), which is what the deploy golden diff guard (§11) protects against.
Model looks dark / grey / washed-out in-game Expected for skins that relied on PBR shine or a dark texture — the injection path ships flat albedo (donor PBR neutralized). Raise Albedo brightness and/or Albedo saturation and re-bake. Judge the amount in-game, not in the dim preview.
A black part (glass canopy, cockpit) renders grey in-game (multi-material model) The near-black→grey neutralize step (which hides UV dead-zones) is flattening an intentionally black material. Tick Keep black (glass/cockpit) and re-bake.
Change didn’t show in-game You didn’t rebuild the mod (§6) — baked assets only reach the game through the bundle; a registry-only change (runtime flags) needs just a relaunch. (Since 2026-07-18 the re-slim runs automatically when a Blender-step setting changed — the “checkbox swallowed my change” failure mode no longer exists.)
Animated toggle greyed out The model has no animation the probe can see (OBJ, or a glTF with no animations). Use a rigged glb/fbx. FBX/.blend can’t be probed cheaply, so the toggle stays enabled — type the clip/bones by hand.
“No clips readable from this model” Clip/Bone Pick works for glTF/GLB only. For FBX/.blend, type the clip name and bone prefixes manually.
Animated model plays the wrong motion (parts assemble/explode) You baked the wrong clip → set Clip name to the loop (e.g. hover), not exploded_view.
“Fire on attack” clip loops constantly (won’t rest) The Fire on attack toggle isn’t set on this model → tick it and rebuild the mod. If it is set, confirm the firing unit matches (the plugin logs [Fire] *** OUR MODEL '<name>' FIRED on bombard) — only artillery/bombard units raise the event.
“Fire on attack” model never moves when it fires The clip must start and end at rest (the single pass returns to frame 0). Re-check the rig’s keyframes. Also confirm [Fire] *** OUR MODEL … FIRED appears in BepInEx\LogOutput.log when it bombards — no log = the unit didn’t raise the artillery event (it’s melee/air, not a bombard).
Animated model tears apart / arms fly out The clip source wasn’t isolated (an extra FBX in the folder → a multi-clip collection). Fixed in the current Factory (it bakes into a per-model anim/ subfolder). Re-bake; if it persists, remove stray .fbx from the resource folder.
~1s stall each loop A padded frozen tail in the clip. The Factory auto-clamps the frame range now — re-bake.
Body wobbles / “unbalanced flywheel” The clip animates the whole body → set Animate only bones to just the spinning group (e.g. prop).
A donor part shows through (rotor, extra mesh) Hide donor meshes → Pick it (after one launch so it’s logged). If it’s an animated donor sub-part, it can’t be hidden — pick a cleaner donor.
Model bobs / wiggles / wobbles (a rigid model on a hovering donor — e.g. an airship on the Recon-Drone donor) Your static mesh is inheriting the donor’s idle/move animation. Tick Freeze donor animation (Runtime section) — the plugin pins the donor’s pose so the mesh holds rigid, and it still glides tile-to-tile. Static models only; no re-bake. Different from the “unbalanced flywheel” row (that’s a model’s own clip animating its whole body — use Animate only bones).
First unit’s borrowed rotor sits ~1 low (after a load, or on a freshly built/spawned unit; other instances fine) An engine spawn race on the first pawn of the model, at creation. Tick Re-spawn after load on that model — the plugin rebuilds the unit’s pawns right after it renders and the rotor comes out right (tune Factory/RespawnDelayFrames in the plugin cfg if it’s briefly visible). Registry flag, no re-bake.
Bake fails: “needs Blender” Install Blender or set its path in Settings. For static decimation without Blender, use Convert grid instead of Reduce-to-tris.
Re-baked static model is 90° off / tipped up in-game (preview looks fine) An older Factory shipped a stale skeleton on re-bake (the static outputs were overwritten in place, so the skeleton baked from cached geometry). Fixed now — the static path deletes its outputs and force-reimports before baking the skeleton, so a re-bake matches a first bake. Just re-bake → rebuild → relaunch.
Texture looks stale in the editor A Unity texture-residency quirk after a multi-material bake — open the source textures in the Project view and back. The in-game result is correct.
Multi-material GLB comes out untextured / grey Fixed — glbconv now emits usemtl groups + a .mtl (and solid-colour swatches for flat parts), so a multi-material GLB atlases like FBX. Re-bake an older GLB model to pick this up (it was baked before the converter preserved materials). Keep Convert grid = 0 (faithful mode; material grouping only runs there).
Baked skin is flat / missing in-game but looks perfect in Blender (no error) The model UV-maps into a non-[0,1] tile (e.g. the whole hull sits in V 1→2) and leans on the shader’s texture wrap to repeat the skin. Blender wraps, so it looks right there; the atlas baker packs each texture into a fixed rect and can’t wrap, so out-of-range UVs sample outside the rect and the skin vanishes. Fixed — glbconv now integer-shifts each island’s UVs back into [0,1] before the V-flip (integer shift, so it never tears tile-crossing triangles). Re-bake with “Keep extracted texture” unticked (static path: that also re-extracts the OBJ) to pick it up. Diagnose by checking the extracted FactorySource/<name>/<name>.obj vt range — if U/V aren’t within [0,1], that was it. Note: genuine repeat-tiling (a small texture meant to span [0,N] and repeat N×) still isn’t atlas-supportable — none of the shipped models need it.
Multi-material model bakes all-grey (at 512) or near-black (Keep black on) — camo/markings gone (no error) Same non-[0,1] UV cause as the row above, but the materials each sit in a different tile, so glbconv’s single global shift can’t gather them. The atlas remapper now also folds per-vertex (u -= floor(u)) when placing each sub-mesh into its rect, which does cover the per-material case. Re-bake to pick it up. (If you saw grey it was the near-black→grey neutralize masking the miss; black is the raw miss with Keep black on.) Proven on the AH-1 Cobra (51 materials, U 0→23). Diagnose per-material with awk grouping the OBJ’s vt by usemtl. Once mapped correctly, remaining softness is just Atlas size — bump 512→1024/2048 for crisp markings.
Bake FAILED: IndexOutOfRangeException in MeshCollection.ImportMeshes (any animated model) The animated skeleton bake (Skeleton.Reimport) reads tangents off the skinned mesh; with none, Amplitude indexes an empty tangent array and throws. Fixed: the animated path always keeps tangents (importTangents = CalculateMikk), regardless of material count — the tangent-strip size optimization is static-path-only. (It bit twice: the multi-material howitzer, then the single-material drone.) The baker now also dumps a SKMESH … bones=/bindposes=/maxBoneIdxUsed=/tangents= line before the bake and flags a bone-index mismatch, so the opaque crash becomes a readable cause. Related: the weld pre-pass is single-material and single-bone only (welding across bone-part seams corrupts skinning). If you hit this after a baker change, check the SKMESH line — tangents=0 on an animated model is the tell.
A part is missing in-game but present in the Factory preview (a mast, antenna — typically small parts, no error anywhere) Shared vertex-buffer overflow. All injected models + the game’s own fx meshes share one ~100k-vertex GPU buffer; overflow is silently truncated from the tail of the last-registered model’s mesh — so late-order parts of your newest model vanish while its body renders fine. The preview is immune (it renders the mesh asset directly). Diagnosed live: a 48k-vert bake lost its rotor mast; visible again after Reduce to ~tris brought it down. Watch the bake log’s verts= line — stay well under ~25k verts per model (UV-seam splitting means verts ≈ 2× tris on textured models, so a 12000-tri target ≈ 24k verts).
Re-bake has no effect in-game (still the old shape/orientation; preview shows the new one) You re-baked but didn’t rebuild the mod. A re-bake keeps the same skeleton GUID, so the registry doesn’t change and the game silently keeps rendering the old geometry from the previously built bundle — no error, nothing to see in the log. Every geometry change needs bake → rebuild mod → relaunch. (Quick sanity test: bake with a wild rotation offset — if the game doesn’t tilt, your build isn’t reaching it.)
Registry gone after a game reinstall / “verify files” Open the Factory window — it auto-restores from the git-tracked backup and writes it back to BepInEx\config (console: restored N model(s) from the project backup). See §10 “Registry safety net”.
Source folder created with the wrong case (e.g. attackHelicopter/ not AttackHelicopter/) Cosmetic, bake-time only. Windows/Unity is case-insensitive + case-preserving, so the folder inherits the spelling of any pre-existing differently-cased asset of that name (e.g. a vanilla attackHelicopter512.png). Baked assets, registry, and in-game loading are all correctly cased. Use a non-colliding resourceName (e.g. AH1Cobra) if you want the folder capitalised.

Diagnostic tip — trust the atlas, not the preview. The Factory preview lights the mesh with a Standard (PBR) shader, so a smooth hull reads dark/glossy even when the baked skin is light and correct — don’t judge textures from it. To see the actual baked skin, select the <name>_Atlas.asset in the Project view and run Tools ▸ HAF ▸ Export selected atlas to PNG (writes to C:/tmp and logs the average RGB). For UV problems, awk the extracted OBJ’s vt lines for the U/V range. Both beat staring at the preview.


9. Where things land

Resources layout (reorganized 2026-07-28, in-game verified): the Assets/Resources ROOT now contains EXCLUSIVELY bake outputs of registered assets (<name>_Skeleton/_Atlas/_Clips*/_Mat/_ModelMesh/_Model + district _DistrictMesh/_FxMesh, prop _Collection/EQ_*_Fragment, projectile Projectile_*/_TrailDrawer). ALL UI images — unit-card portraits (every size), technology/constructible icons, narrative event JPGs — live in Assets/Resources/Images/. They are referenced from the databases by (nibble-swapped) GUID, so the path is free; keeping them out of the bake-output namespace exists because a prefix-wildcard cleanup once deleted a unit’s portrait (see the Remove flow’s curated cleanup + Textures.md). Retired experiment files go to Assets/FactorySource/Attic/ (on disk, not shipped) — nothing is ever just deleted.

10. Registry safety net

The registry is the one Factory artifact that lives in the game folder, so it’s the one a game reinstall or a Steam “verify files” can wipe. Three layers protect it — all automatic, nothing to configure:

11. Regression guards (run before committing baker changes)

Bakes are manual and the roster is growing, so a baker change can silently break a model you don’t happen to re-bake until much later. These guards catch that at the integration seam unit tests can’t reach — run them after any change to the baker, rig_anim.py, deploy_convert.py, glbconv, or the registry schema.

12. Texture-only reskins — the Unit Retexture window (no bake)

Sometimes the vanilla model is fine and only the skin is wrong — a Common copy that should look distinct from its emblematic original, a colour test, a themed variant. For that there’s a separate window — Tools ▸ HAF ▸ Unit Retexture — that reskins an existing unit without baking a model: the vanilla mesh is kept, and the runtime plugin paints your texture onto an isolated clone of the unit’s output layer, so the original unit (and every other unit sharing that layer) is untouched.

Everything is a plain registry entry (no assets, no mod rebuild). Section 3 of the window is Replace / adjust skin:

Proven on the grey corvette. (These sliders replace the earlier single grey/darken toggle.)

Custom models too (2026-07-20). A textureFile + adjustments on a baked model entry now recolour that model: the plugin hot-loads the PNG in place of the baked atlas (falling back to the atlas if the PNG is missing) and applies the Desaturate/RGB to it — so a custom model gets a new skin without a re-bake. Paint over the model’s own atlas dump exactly as with a vanilla unit. One caveat: the baked atlas itself isn’t CPU-readable at runtime, so an adjust-only recolour (sliders with no PNG) does nothing on a model entry — always route a custom-model recolour through a PNG (dump → repaint → assign). Previously a textureFile on a model entry silently diverted the entry down the vanilla texture-only path — painting the donor’s layer and never even swapping the mesh in.

Sound moved out of this window. Engine events and custom WAVs are configured exclusively in the dedicated Unit Sound window (§13–§14, with per-clip volume and ▶ preview); this window is skins only. Apply here never touches an entry’s sound fields — whatever audio the entry already carries is preserved.

Workflow:

  1. Pawn — pick the pawn descriptor (use the _Common_..._01 copy, not the emblematic original — the isolation clone is what leaves the original untouched; they share an output layer). The entry name defaults to Retex_<pawn>.
  2. Download the skin to paint — dump the unit atlases in-game first (F8 window ▸ Dump Atlases; files land in BepInEx\config\haf_atlas_dump\), then paint over the unit’s PNG in any editor. It’s the unit’s real UV layout, so what you paint is what wraps.
  3. Replace — point the window at your painted PNG and Apply: it copies the PNG into config\haf_skins\ and writes the registry entry (or tick Grey for the desaturate mode). Relaunch/reload the game to see it.

Because the mesh is untouched this can’t fix silhouettes — it’s for palettes, markings, camo. And since the plugin hot-loads from the game’s config folder, iterating is repaint → overwrite the PNG → reload; no editor round-trip at all.

Cost — essentially free on the vertex budget. A reskin keeps the unit’s existing mesh, so it adds no vertices, indices, or meshes to the GPU pawn-layer buffer — that buffer is budgeted per distinct mesh type (instances are free; see Vertex-Budget.md), and a reskin introduces no new type. It costs only an output-layer clone (a render slot) and one texture. So unlike a baked custom model — which does add its own distinct mesh’s verts — you can stack many reskinned variants without approaching the mesh ceiling. (Recolouring a single custom model without a re-bake is built — see above. The remaining extension is one baked mesh shared across different base units as textured variants: the plugin already dedups a shared skeleton’s mesh on upload, but that still needs work on the per-donor rename; not built yet.)

13. Unit sounds — engine audio & the sound catalog

Injected/retextured units are silent on move by default: the per-ship engine sound (Play_UNIT_Vehicles_<Type>_Start /_Stop) rides an audio-service path tied to the vanilla unit’s move state, which our re-loaded units never trigger. (Full diagnosis in the unit-movement-audio-investigation memory — the emitter, its Wwise registration and its 3D position are all fine; only the trigger is missing.) The plugin restores the sound by firing it itself.

Enable it — tick the Wwise engine event option in the Unit Sound window (§14; or set engineSound: true in the registry — sound config used to live in the Unit Retexture window, but is owned by the Sound window now). The plugin then watches each of that unit’s instances and, on a movement start/stop transition (render-position delta, like deploy-on-stop), posts the engine event onto the pawn’s AudioEmitter: a rev on departure, a settle on stop.

The one-click path (2026-07-25): “Use donor engine sound”. The Sound Studio’s Wwise section has a button that does the whole job: it reads the unit’s DONOR from the plugin’s load log (the donor Skeleton='Unit_…' line — so launch the game once with the unit loaded), extracts the family word (AntiAirGuns), fuzzy-matches the catalog for its Movement pair (plural-trimmed contains, prefers Move/Movement, Stop = the _Start→_Stop sibling) and fills both fields — e.g. the ArmouredCar → Play_UNIT_Vehicle_AntiAirGun_Movement_Start/Stop. Vanilla units (no donor line) fall back to their own family name. Every miss reports in the status bar; the fields below remain the manual path.

Manual path — searchable Pick (2026-07-25): both event fields have a Pick button opening a searchable dropdown over the full dumped catalog (no more hand-copying from the text file); picking a *_Start auto-fills the matching *_Stop when it exists. The button is disabled with a how-to tooltip until the catalog has been dumped once.

Name the sound (works for the FIRST unit, no capture): fill Start event / Stop event with Wwise event names. The plugin posts them by name (AkSoundEngine.PostEvent(name, emitterGuid)), so a named sound plays for the very first unit at load — no dependency on anything else having moved. Leave them blank and the plugin falls back to a handle auto-captured from any same-family vehicle that moved this session (fine mid-game, but the first unit stays quiet until then) — so name the events for a shipping mod.

Extract every sound — the catalog: F8 window ▸ Dump Sound Catalog writes every Wwise event name in the game (~800+) to BepInEx\config\haf_sound_catalog.txt — this file also FEEDS the Pick dropdowns and the donor auto-button above, so dump it once per machine and the UI takes over. Examples:

Unit family Start event Stop event
Modern warship (corvette, destroyer, stealth) Play_UNIT_Vehicles_StealthCorvette_Start Play_UNIT_Vehicles_StealthCorvette_Stop
Aircraft carrier Play_UNIT_Vehicles_AircraftCarrier_Start Play_UNIT_Vehicles_AircraftCarrier_Stop
Steam-era frigate Play_UNIT_Vehicles_SteamFrigate_Start Play_UNIT_Vehicles_SteamFrigate_Stop
Submarine Play_UNIT_Vehicles_Submarine_Start_Modern (or _Old) Play_UNIT_Vehicles_Submarine_Stop_Modern (or _Old)
Landing craft / hovercraft Play_UNIT_LandingCraft_Start Play_UNIT_LandingCraft_Stop
Helicopter Play_UNIT_Helicopter_Move Play_UNIT_Helicopter_Stop
Towed howitzer Play_UNIT_CanonObusier_Move_Start Play_UNIT_CanonObusier_Move_Stop
Wheeled gun / mortar Play_UNIT_Vehicles_Mortar_Move_Start Play_UNIT_Vehicles_Mortar_Move_Stop
AT gun Play_UNIT_AntiTankGun_Move_Start Play_UNIT_AntiTankGun_Move_Stop

Air units (planes, zeppelins, drones) have no engine-loop event in the game — only takeoff/shoot — so they can’t be given a continuous move sound this way; leave engineSound off for them.

It posts any named event, so this isn’t limited to engines — attach any sound in the catalog to a unit’s movement. (In the ENC mod, all the naval/ground/heli units are pre-configured with the events above; the three air units are left silent.)

Audio diagnostics (F8 window), kept for future sound work: Dump Audio logs each unit’s emitter (registration, position, idle/free-event state); Audio Trace toggles a live log of every sound the game posts (how the event names were discovered — it patches the service sink AudioManager.PostEvent); Play Audio (test) posts the captured engine handle onto the filtered units to confirm audibility.

Limits (honest): the Wwise-event path fires Start/Stop accents only (no sustained loop between) — for a continuous engine/hover loop, use the custom WAV path (§14). The auto-capture fallback uses the last-seen vehicle Start, so a named event is preferred; a shipped registry with names Just Works from the first unit, every launch.

14. Custom sound files & per-clip volume — the Sound Studio window

Tools ▸ HAF ▸ Sound Studio collects a unit’s whole audio profile into one dialog with collapsible sections — Silence inherited donor sound, Idle growl (file, volume, interval, one-voice radius), Attack sound, Death sound, Battle start war cry (each: file, volume, start offset), Movement (start/travel/stop), and Wwise engine event (with the searchable event Pick dropdowns and the one-click Use donor engine sound button — §13). The Pawn-description field has its own Pick (2026-07-25): a searchable dropdown over EVERY pawn descriptor in the loaded databases — ENC and mounted vanilla alike (first open scans the assets once per session) — and picking loads that pawn’s existing audio config. Pick a pawn (or Edit one from the “Units with audio” list, which fills the window’s remaining height) and every knob round-trips; each WAV row has a ▶ preview. Folded headers summarize their configuration (2026-07-25): while a section is collapsed, its configured WAV/event shows right-aligned in green on the header row — growl file + interval, one-shots with their start offsets, the movement trio, the shortened Wwise event, “silenced” — so a fully-collapsed window reads as the unit’s complete audio dossier and configured-vs-empty is visible at a glance. Everything below is one of those sections.

When the game has no suitable sound (drones, zeppelins) or you want a bespoke engine, drop in your own audio. Tools ▸ HAF ▸ Sound Studio — the Movement section — assigns up to three WAVs, each with its own volume —

These play through Unity’s own AudioSource (not Wwise), so any WAV works — no soundbank needed. Requirements: 16-bit PCM WAV (convert mp3/ogg first); mono for true 3-D positioning. Files are copied into BepInEx\config\haf_sounds\ and referenced by the registry (soundStartFile/soundFile/soundStopFile + soundStartVolume/soundVolume/soundStopVolume). It writes onto the unit’s existing registry entry, so a unit still has one entry. (A Wwise engine event option in the same window covers the §13 game-sound path.)

Volume is perceptual in the window — the slider tracks perceived loudness (√ curve) and the ×N label shows the real linear amplitude stored (hearing is logarithmic, so e.g. slider 0.4 ≈ amplitude 0.16). Seamless loops: if a raw Travel clip clicks at its wrap, crossfade a copy first (blend ~0.1–0.3 s of the tail into the head).

Audition in the editor: each clip row has a preview button that plays it at its set volume (with ■ Stop), and Edit on a unit in the “Units with audio” list loads its current Start/Travel/Stop files + volumes — so you can pick and tune a sound without launching the game.

Performance note: the runtime audio driver polls only our units’ sub-pawns and refreshes its scene lookup on a ~2 s cache — never a per-frame FindObjectsOfType (an early version did, and it visibly cut FPS). If you extend it, keep any full scene scan off the hot path.

14a. Silence an inherited donor sound (silenceDonorAudio)

A custom creature that reuses a donor inherits the donor’s sounds as well as its animation. The Abomination borrows a bear, so it played the bear’s idle growl and combat maul/scratch — and neither is a data field you can blank: the idle loop is PresentationPawnDescription.IdleAudioEvent posted at spawn, and the attack SFX is baked into the borrowed animator’s MecanimEventData (resolved by animator GUID). Both, however, funnel through the same chokepoint — AudioEmitter.PostEvent on the pawn’s Wwise emitter.

Tick “Silence the borrowed donor’s inherited sounds” in the Sound Studio window (or set silenceDonorAudio: true in the registry). At runtime the plugin registers each of the unit’s pawn emitters and drops every Wwise post on them (a Harmony prefix on AudioEmitter.PostEvent), plus a one-time StopAll to cut an idle loop already running since spawn. It silences only Wwise — your own custom WAVs (below) go through Unity’s AudioSource, so they still play. Confirm it engaged in LogOutput.log: [Audio] '<resource>' donor audio silenced (emitter <id>) (one line per pawn). Reusable on any unit stuck with an unwanted inherited sound (e.g. a borrowed-rotor helicopter that drones forever).

14b. Occasional idle growl (soundIdleFile)

Pair the silence above with your own idle vocalisation: the Idle growl row in the Sound Studio window assigns a WAV played as a one-shot occasionally while the unit stands still (not moving). The plugin fires it on a per-pawn timer at soundIdleInterval seconds (default 11), jittered 0.6–1.4× so a pack doesn’t growl in unison, and suppressed while moving (the cadence reschedules when it stops). This mirrors how the game’s own idle vocalisations play — periodic, not a continuous wall of sound. Registry keys: soundIdleFile / soundIdleVolume / soundIdleInterval (≤0 disables). Same WAV requirements as §14 (16-bit PCM; mono = 3-D).

One voice per unit (soundIdleGroupRadius, default 10). A unit is many pawns (a 5-monster stack is 5 pawns), so without de-dup all five snarl at once — a chaotic overlapping wall. When a pawn growls, any packmate within soundIdleGroupRadius world-units stays quiet until the interval passes, so a clustered formation speaks with one voice per interval and the “voice” rotates among its pawns as their timers come due. Units farther apart than the radius growl independently; a massed horde within it shares one occasional voice (usually what you want). ≤0 = per-pawn (every pawn growls — the old behaviour). Verified in-game on a 5-Abomination stack (2026-07-23): the wall-of-sound collapsed to one periodic snarl.

14c. Attack sound (soundAttackFile)

A distinct, violent one-shot for when the unit strikes — separate from the idle growl. soundAttackFile / soundAttackVolume / soundAttackOffset (a WAV in haf_sounds/). Three things make it land right:

14d. Death sound (soundDeathFile)

A one-shot rattle/scream when a pawn of the unit dies — closing the unit’s audio arc (alive → fighting → gone). soundDeathFile / soundDeathVolume / soundDeathOffset. The trigger is PresentationPawn.TriggerDeath — the exact presentation-side moment a pawn’s death animation starts, once per dying pawn. A wiped stack dies pawn-by-pawn in a burst, so the runtime enforces a short per-entry gap (~0.6 s): one rattle per beat instead of five at once. Plays at the dying pawn’s position through the same camera-audible one-shot path as the attack roar.

14e. Battle-start war cry (soundBattleFile)

A one-shot war cry the moment a battle begins with this unit on either side. soundBattleFile / soundBattleVolume / soundBattleOffset. The trigger is the sim event SimulationEvent_BattleStarted: the hook walks the battle’s attacker/defender groups for our unit definitions on the sim thread (managed reads only) and queues one cry per matching entry; the main thread plays it camera-anchored (like the attack cue) so it opens the battle audibly at any zoom. A per-entry gap keeps back-to-back battles from stacking cries.

So the full “replace a creature’s voice” recipe is: silenceDonorAudio: true + a soundIdleFile growl (interval + group radius to taste) + a soundAttackFile roar + a soundDeathFile rattle + a soundBattleFile war cry. All are set per-unit in the Sound Studio window (§14), every one with volume + start offset + ▶ preview, and persist in the registry. The Abomination ships the trio: bear silenced, an occasional bear/croc snarl at idle, a beam-roar on the strike.


15. The Animation Lab window — a model’s animation, in one place

Tools ▸ HAF ▸ Animation Lab (2026-07-18). Docks as a tab next to the Model Factory, so the pair presents as one tabbed dialog. The design rule: the Factory owns the MODEL (identity, pawn, model file, transform, size, geometry/shading, static runtime flags), the Lab owns the ANIMATION — every setting lives in exactly one of the two windows, and jump buttons hand context across (“Edit in Animation Lab” in the Factory loads the entry here). Both windows Bake through the identical pipeline (ConfigFor → UniversalBaker.BuildAnimated → ModelRegistry.Upsert), so it does not matter where you press Bake.

Auto-detect settings (2026-08-01) — a button at the top of the Clip section reads the model’s clips + rig and fills the whole animation config in one click, then explains its choice in the status bar (review-only, nothing bakes): a Vehicle Lab Spin rig → State-driven with Idle/reference = Spin[0..0] (still) and Movement = Spin (rolls), Convert-raw-rig + Auto-ground + Keep-translations ON, Fix 100× OFF — the exact recipe the Vehicle Lab prints; a character (an idle + a run/walk/move clip) → State-driven with idle/movement guessed from the names; a single clip → continuous loop; a deploy clip → a hint (deploy frame-ranges can’t be inferred from a baked clip).

A failed bake keeps your settings (2026-08-01) — both windows snapshot the form before baking and restore it if the bake errors, so a failure no longer reverts the config you just entered (RebaseOnRegistry + the field trims mutate the form in place otherwise). The status reads “Bake FAILED (settings kept): …”.

Edit existing lists the animated entries only. The model identity (Resource / Target pawn) shows read-only — change those in the Factory; the Model file row has a Browse… button (2026-07-19) for repointing an entry at a new source, and a ▶ Play clip button — THE recipe-authoring workflow: it plays the RAW model file’s entire source animation (every take, full length, no conversion, no slicing) with play/scrub, single-frame stepping (|◄ ►| buttons, ←/→ keys, Shift = ±10 frames), and set current frame capture. This is where every frame number in a recipe comes from — deploy start/end, barrel ready-frame, recoil range: step through the source, capture the frames with set current (each field also has a go ► jump back to its frame). A Speed /N field (2026-07-20) sits after the End field: the frame-skip step baked into the slice (/2 = every 2nd frame = 2× faster — this is where walk pace is authored, since pacing is bake-only), and ► Play previews at that stepped pace, so a gait is judged in the dialog before any bake. Confirm writes the full spec — clip[start..end/N] — straight into the clip field it was opened from; Cancel just closes. After every compile/domain reload the window RE-LOADS the selected entry from the registry (2026-07-19) — the file is the single source of truth; unsaved form edits from before a compile are deliberately dropped (Save (no bake) first if you want to keep them). The ↻ Reload button does the same explicitly — the escape hatch from any stale window copy (re-selecting the same entry in the dropdown does NOT reload it). This closed the recurring “stale Lab clobber” trap where a pre-reload form silently overwrote registry edits at the next Save/Bake. ↻ Reload only reloads the DATA — it does NOT rebuild the model preview (2026-08-01): the preview-rebuild path corrupts the texture on some models (a tiling-UV rig — UVs outside 0–1 that tile a seamless source texture — sampled against the packed atlas shows a scrambled checkerboard), whereas the correct preview from the initial open/bake is fine to keep for the same entry. Selecting a different entry, or a Bake, still rebuilds it. (The scramble is preview-only — the shipped unit uses atlas-remapped UVs and is unaffected; the real fix, a preview mesh with atlas UVs, is a backlog item.)

Deploy conversion (rigid-parts source → bone-per-part rig, 2026-07-19)

For Level-2 sources (Sketchfab vehicles/artillery animated by moving parts, not a skeleton): tick Deploy conversion, point the Model file at the raw original, and the bake runs Tools/deploy_convert.py automatically first — into FactorySource/<res>/deploy_converted.glb, cached on an args+source+tool fingerprint (a knob change reconverts and re-slims; nothing else does). Every knob is registry data — the full pipeline reproduces from the entry alone:

Field Meaning
Deploy frames … End the deploy motion’s source-frame range (scrub the raw file in ▶ to find it). Required.
Also remove (adds to defaults) extra parts to delete on top of the default crew/prop kill-list — build it with the Pick parts… dialog (a ticklist of the source model’s parts; no typing, no re-carrying the defaults). A ticked name also removes any sub-part whose name contains it. (deployStripExtra; the M114’s contrl_wheel,ctrl_wheel2 control hand-wheels live here.)
Strip parts (replace defaults) advanced — name substrings that REPLACE the default kill-list wholesale (usually leave empty; the Dugout Canoe’s camera is the rare case). To ADD parts, use Also remove above — it keeps the defaults
Barrel ready frame source frame of the fully-elevated barrel; re-keys barrel/cannon parts to rise over the deploy’s back half (empty = leave as authored)
Leg spread scale empty = source leg curves verbatim; a number re-keys *leg* parts as pure travel→spread rotation (1 = full width). Required for legs that slide — see the rotation-only law in Animated-Models.md
Barrel elevation scale >1 exaggerates past the source’s max (empty = 1)
Recoil frames (a..b) the fire cycle’s source range, remapped onto the deployed pose as the recoil clip. MULTI-SEGMENT (2026-07-26): comma-separate extra windows appended PRISTINE as epilogue, each with an optional /N speed step (every Nth frame = N× faster) — the M114’s shipped recipe is 442..530,305..441/2: the full fire+kick+reload window, then the source’s own aiming raise (which only exists before the fire) replayed at double speed to bring the barrel back up. Segment 1 alone drives the slide/slam analysis. Set the Attack clip to plain recoil. Empty = none
Return slow the SYNTHESIZED palindrome return: the window replayed backward at this multiple (empty = 4; 0 = none). With Keep bone translations the source’s own run-out plays — set 0, or the kick fires twice (the rotation-only era needed the synthesis; the translation era doesn’t)
Slam (deg) the ROTATION-FAKED kick pitch in degrees (the arc trick from the rotation-only era — see the revised Laws in Animation-Pitfalls.md). With Keep bone translations set 0: the real translation slide replaces the fake (they stack if you want both). POSITIVE = muzzle-down dip, NEGATIVE = muzzle-up
Recoil step arc sampling fineness. (Slide scale and raw Arc R are hidden from the UI: the former self-cancels under Slam-degrees, the latter is superseded — both registry fields survive for legacy recipes)

THE REAL KICKBACK (2026-07-26, verified on the M114): tick Keep bone translations (Animation Lab, under Auto-ground) and the source’s authored recoil SLIDE bakes and plays as true translation — the tube slams back and glides home exactly as animated. Recipe: Recoil frames 442..530,305..441/2, Return slow 0, Slam 0, Keep ✓. Translations are kept ONLY in the attack clip (deploy/stance stay rotation-only — keeping them there displaced the assembly) and are delta-rebased (pure motion, no pose offsets) + ×100 sandwich-compensated on the legacy path. The old Slam arc remains for models whose source has no real slide.

The conversion generates the state clipsdeployed, folded, unfold, fold, recoil — from those frame numbers; the Pick dropdowns and the ▶ picker inspect the converted file once it exists, so you just assign the five names to the five roles. (The clip names live in the converted GLB; the bake resolves them by name, isolates one clip per role folder, and the game loads the resulting ClipCollections by GUID.)

Clip (bake-time — changing these needs a re-Bake)

Behavior (runtime — Save (no bake) + game relaunch applies them, no re-bake, no mod rebuild)

Buttons

Safety nets

The animated flag is derived, not trusted: an entry carrying animation config (named clip, behaviors, bone filter, or a baked clip GUID) is re-marked ANIMATED automatically on load and before every bake. This ended the “howitzers on their side” failure mode — a stale unticked checkbox once re-baked the howitzer STATIC, which strips the clip + behaviors and bakes the (animated-path-ignored) Rotation offset into the mesh, shipping tipped-over guns. Baking a rigged model that truly has no animation config still asks for confirmation first.

Cross-window safety & the facing mechanism (2026-07-18)

Enforced field ownership. Bake/Save from EITHER window now rebases on the freshest registry entry and contributes only the fields that window owns (Factory: model/transform/size/…; Lab: clip/bones/Fix-100×/behaviors). Before this, whichever window held a stale copy silently clobbered the other’s values at bake time — it cost three bakes on the Combine soldier (a Factory bake dropped the Lab’s Fix-100× → 100× giant; the Lab’s stale copy then dropped rotation AND size). You can now edit in one window and bake from the other safely.

How the game turns pawns (found via the fixed-compass soldier). The engine orients a pawn’s visual through the procedural BoneRotation layer (PawnEntry.BoneRotation0-3), not only ObjectSpace.Rotation. The plugin used to zero that layer on every animated model (added so artillery aiming couldn’t twist the howitzer’s barrel) — which pinned the soldier to one compass facing forever. It now clears the layer only for models with the artillery behaviors (fire-on-attack / deploy-on-stop); everything else keeps the game’s layers and turns normally.

The raw-rig conversion (2026-07-18/19 — how the soldier’s “torn head” was actually solved). A Sketchfab/auto-rigged model can ship a scrambled rest pose that the clip’s location keys ASSEMBLE into the body every frame (the Combine soldier’s frame-0 posed bones sat up to 91 units from their rests on a 73-unit rig — 129 location curves were structural, not decorative). Amplitude plays rotation-only clips, so such a rig can never work as-is. rig_anim.py now performs, on the conversion path: (a) REST NORMALIZATION — snapshot every bone’s visual matrix on every frame, apply the armature modifier at frame 0 (the assembled body becomes the bind mesh), Apply-Pose-As-Rest (the assembled pose becomes the rest), re-bind, then re-derive the whole clip as pure rotations against the new rest (verified in-bake: frame-0 residual = 0.000167); (b) strip residual location curves; (c) collapse no-op root bones; (d) topological bone rename (Amplitude sorts alphabetically, parents must precede children); (e) fold rotation+scale into the data and export unit-clean (global_scale=0.01, no ×100 root Lcl Scaling, skeleton bakes all-Scale-1). Verified end-to-end with a litmus rig (Tools/make_litmus.py: a 12-deep bone chain of colored cubes — renders straight in-game, exonerating the runtime for clean rigs). Raw-FBX inspectors used for the diagnosis: Tools/fbx_binddump.py, Tools/fbx_lclscale.py. Gate refactor (2026-07-18): the conversion is now selected by the explicit “Convert raw rig” checkbox in the Animation Lab (registry field convertRig) — it originally triggered on a non-zero Rotation, which made the Rotation field a hidden pipeline switch (the soldier shipped with a 360,0,0 identity trick; a rotation edit on a legacy model silently rerouted its bake). Rotation is just a rotation again (applied only on the conversion path); flag OFF remains the byte-identical legacy pipeline, and old registries migrate automatically on load.

RESOLVED with the clean rig (2026-07-19): the “drone projectile invisible on attack” symptom disappeared once the soldier’s rig was properly converted — the corrupted skeleton state was evidently disrupting the attack presentation too (mechanism not separately traced; if it regresses, the designed fix was a Pose0 pass-through window during the attack). The temporary [Uni][facing] periodic log can be removed on the next plugin change.


16. Converting an animated model — from raw rig to Amplitude-ready

New here? Read Animated-Models.md first — the plain-language “what kinds of animated models can HAF import” page (three levels, what each needs, current limits). This section is the deep technical treatment behind it.

Everything learned getting the first humanoid (a raw Sketchfab ValveBiped) fully working in-game, distilled into the practical guide. The Factory does the conversion automatically — this section explains what it does, how to drive it, and how to read the safeguards. (For the other half — how the game consumes the baked result every frame — see Animated-Runtime.md.)

16.1 What Amplitude’s animation system can actually play

These are hard engine constraints (established by decompiling the bake — ClipEntry.Reimport, Skeleton.Reimport — and the runtime — AnimationManager.GetBoneTRS/ApplyPose/GetPoseTRS):

Constraint Consequence for your model
Clips are rotation-only (translations are dropped or mis-scaled) Any rig whose animation moves bones (location keys) must be converted
Uniform scale, and effectively scale 1 The skeleton must bake with every BindPose/Local scale = 1 — no unit-conversion “sandwiches” on nodes
Bones are sorted ALPHABETICALLY, parents must sort before children Rigs whose names don’t sort topologically (e.g. _014 < _02) get scrambled chains
Bounded chain depth (CPU cap 15) Pass-through root bones waste depth budget
One clip per ClipCollection folder The bake isolates your FBX in anim/ automatically

Amplitude’s own art pipeline satisfies all of these silently, by convention. Raw downloaded rigs usually violate several at once — and the failures MASK each other, which is what makes hand-debugging brutal (it took five stacked defects to get one soldier standing).

16.2 The three kinds of source rig

  1. Clean, authored rig (our drone; anything from deploy_convert.py) — rotation-driven animation, sane rest pose. Bake with “Convert raw rig” OFF = the untouched legacy path. If it works, never touch it.
  2. Clean rig, wrong orientation — rotation-driven but bakes lying down/facing wrong. Tick “Convert raw rig” and set a Rotation (probe one axis at a time in 90° steps, judge IN-GAME) — rotation is only applied on the conversion path.
  3. Auto-rigged / “assembled-by-animation” rig (the Combine soldier; typical of Sketchfab auto-rigs) — the REST POSE IS NOT THE BODY: the clip’s location keys assemble the model every frame. Diagnostic tell: lots of pose.bones[...].location curves, and body parts that float/detach in-game while Unity’s own preview plays the FBX perfectly (Unity supports location keys; Amplitude doesn’t). These NEED the conversion path — tick “Convert raw rig” in the Animation Lab (add a Rotation only if the converted bake comes out mis-oriented).

16.3 What the conversion path does (automatic, in this order)

Everything below runs inside the Bake’s Blender step (Tools/rig_anim.py) whenever “Convert raw rig” is ticked in the Animation Lab.

Three rig repairs run first (mech findings, 2026-07-20) — each a no-op for a rig that doesn’t need it:

Then the conversion proper:

  1. Rest normalization + visual rebake (the key step for type-3 rigs; conversion-path ONLY since 2026-07-19) — snapshots every bone’s visual matrix on every frame, applies the armature modifier at frame 0 (the assembled body becomes the bind mesh), applies the frame-0 pose as the new rest, re-binds, then re-derives the ENTIRE clip as pure rotations against the new rest. Console proof: RIGANIM rest-normalized + rebaked N frames x M bones ... frame-0 residual = 0.0001… — the residual must be ~0. (It used to trigger on location-key presence even with the flag off, quietly reaching into legacy re-bakes of deploy_convert.py outputs; now the flag alone decides.)
  2. Location-curve strip — whatever translations remain are removed (Amplitude can’t play them anyway). Runs on BOTH paths, deliberately: every verified legacy bake (drone, howitzer) went through it, and un-stripping could re-introduce the drone’s unscaled-translation wobble — so “legacy byte-identical” means no rig manipulation (no fold / rename / collapse / scale-fold), with this strip as the one shared step.
  3. No-op root collapse — unanimated, unweighted, single-child root bones (_rootJoint) are deleted for depth.
  4. Topological bone rename — every bone prefixed b###_ in hierarchy order, so alphabetical = topological.
  5. Rotation + scale folded into the data — your Rotation value and all node scales are baked into vertices + bone rests; nodes export as identity.
  6. Unit-clean exportglobal_scale=0.01 cancels the FBX exporter’s m→cm ×100 root scaling, so the file carries no scale annotations (UnitScaleFactor 1.0, all bind clusters 1.0).

The Unity side then imports with useFileScale off (non-Fix-100× path), so the baked skeleton reads Scale: 1 on every bone — verify with a text editor on Assets/Resources/<name>_Skeleton.asset if in doubt.

16.4 The workflow, start to finish

  1. Factory: pick the target pawn, Browse the model, set Size. Animation Lab (auto-opens via the jump button): pick the Clip. Bake once with “Convert raw rig” OFF — maybe you have a type-1 rig and you’re done.
  2. Rebuild the mod, look in-game (the game is the only honest judge — the previews’ orientation is meaningless for animated models).
  3. Wrong orientation or floating parts → tick “Convert raw rig” (+ a Rotation probe if it’s an orientation problem: start 90,0,0, one axis at a time) → Bake → check the console for the conversion lines and the residual ≈ 0 → rebuild → judge in-game. Two or three probes typically suffice.
  4. Watch the Console safeguards on every bake: the RIGANIM lines tell you exactly which steps ran, and the bake fails loudly rather than shipping a broken skeleton.

16.5 Troubleshooting map (symptoms → cause)

In-game symptom Cause Fix
Model lies down / faces wrong, plays fine otherwise Orientation (type 2) Tick “Convert raw rig” + Rotation probe, one axis at a time
A body part (head, hands) floats rigidly detached; Unity preview plays the FBX fine Type-3 rig: structural location keys, scrambled rest The conversion path (tick “Convert raw rig”) does it all
Parts smear/stretch, “movement looks exaggerated” Translation keys playing unscaled Same — conversion path
Whole model 100× too big FBX unit scale Fix-100× toggle (per-model; the conversion path usually makes it unnecessary)
Deep chains (fingers, head) scrambled, shallow parts fine Bone-name sort order Automatic (topological rename) on the conversion path
Model collapses flat onto one point, limbs flung; Unity console warns “verts with no weight → bone #0” Rig has NO skin weights — parts rigidly bone-parented (common downloaded mech/vehicle rigs) Automatic on the conversion path (bone-parenting → full-weight groups)
Skeleton ~100× off the mesh; rigid parts fling into a “wing” Wrapper empty with non-identity scale survives to export Automatic on the conversion path (wrapper-empty flatten)
Huge stretch spikes on a detailed rig (fine in Blender’s preview) Over Amplitude’s 256-bone GPU skinning cap Automatic (zero-weight leaf bones removed to ≤240)
Model won’t turn with movement The plugin was clearing the game’s facing layer Fixed globally — cleared only for artillery models now
“Is it my model or the pipeline?” Bake Tools/make_litmus.py’s chain-of-cubes rig; if it renders straight in-game, the pipeline is fine — it’s the model

16.6 Inspection tools (when you need to see for yourself)

Run any of them with blender -b --python <script> -- <args>.

17. Turrets & fire effects — target tracking, muzzle flash, smoke, tracers (verified 2026-07-24)

A vehicle model injected onto a weapon-carrying donor inherits the donor’s FIRE PRESENTATION — and every piece of it anchors to the DONOR’s rig: the aim rotation streams at donor bone indices, and the muzzle flash / launch smoke / projectile origin all resolve donor SOCKET NAMES that don’t exist on your renamed skeleton (the lookups fall back to the pawn root + the donor’s socket-local offset — the classic “flash in a corner”). This section is the complete, in-game-verified system that puts all of it back on YOUR model. Reference implementation: the ENC ArmouredCar (Ehrhardt) on the AntiAirGuns donor — tracking turret, flash, smoke and tracers all on the gun.

17.1 The knobs (all per-model registry fields)

Knob When What it does
turretBone + turretAxis runtime TARGET TRACKING: the game’s streamed aim rotation is remapped onto YOUR bone (substring), around the given local axis (0/1/2 = X/Y/Z; a turret wants its YAW axis, an artillery barrel its PITCH; -1 = the stream’s own axis). Save + relaunch.
socketBones bake DONOR SOCKETS: "DonorName=OurBoneSubstr[@x,y,z];..." — bakes zero-weight bones with the EXACT names the donor’s fire events look up, parented to your bone, so flash/smoke/tracer origin resolve NATIVELY and follow it (a tracking turret). Socketed models rename bones A###_ instead of b###_ (donor names are capitalized; Amplitude sorts alphabetically and needs parents first). Re-BAKE to apply.
muzzleOffset runtime WORLD-SPACE DIAL "x,y,z" added to the pinned fire origin — the empirical fix when a rig’s gun-bone HEAD sits somewhere unhelpful (the Ehrhardt’s is at the model base). Iterate value → relaunch; no bake, no rebuild.
muzzleBone runtime LEGACY REDIRECT (pre-socket): reroute a MISSING socket lookup to your bone. Superseded by socketBones for re-baked models, but keep it set — it gates the donor-offset compensation below.
silenceDonorVfx runtime SUPPRESS instead: drop the donor’s animation-driven VFX entirely (Factory checkbox). Sounds untouched. The fallback when relocation isn’t wanted.

17.2 The recipe (what actually shipped on the ArmouredCar)

Update 2026-07-25 — the shipped ArmouredCar now runs a Vehicle-Lab-GENERATED rig (bones Root/Wheel_00…/ Turret; the hand rig’s MW_* bones no longer exist in it). Its live calibration: socketBones: "Canon_Up_left=Turret;Move_bloc=Turret", muzzleBone: "Turret", muzzleOffset: "0.5,1,0" (dialed from the turret’s CENTER — the generated bone sits there, not at the gun), turret aim axis = 1 (Y) — generated rigs always yaw on Y because the bone is built tail-up. A socketBones naming a missing bone now fails the bake LOUDLY with the rig’s bone list (it used to hide behind “produced no FBX”). The MW_T recipe below remains the reference for hand/ripped rigs that carry weapon bones.

  1. Or skip the string entirely (2026-07-25): the Lab’s Edit… button on the Donor-sockets row opens the guided mapping dialog — it harvests the hardpoint names from the [Muzzle] log for this unit automatically, offers your model’s full bone list in searchable pickers (per-bone precision), takes optional offsets, and has a one-click “Map ALL hardpoints to one bone…” for the common everything-on-the-gun recipe. Steps 1–2 below describe what it automates.
  2. Discover the donor’s socket names. Fire the unit once and read the [Muzzle] GetBoneTRS('...') log lines — the AA-gun donor asks for Canon_Up_left and Move_bloc. The names LIE about their roles: decode them from the per-shot pin log — Move_bloc turned out to be the fire POSITION anchor (its events carry offsets reaching from a base block up to each gun) and Canon_Up_left the ROTATION/direction socket.
  3. Bake the sockets onto the gun: socketBones: "Canon_Up_left=MW_T;Move_bloc=MW_T" — BOTH on the weapon bone, so position and direction ride the tracking turret. Watch the bake console for the RIGANIM socket ... lines and the A###_ prefix note; the skeleton grows by the socket count (14 → 16 bones here).
  4. Dial the height: muzzleOffset: "0,2.6,0". Why needed at all: the Ehrhardt’s MW_T bone head sits at the model BASE (the gun mesh is skinned to it, but the bone origin is low) — and, an OPEN engine question, the socket’s provably-correct bind height did not reach the runtime pose either. The dial closes the gap empirically: value → relaunch → look; two iterations converged (1.3 = halfway, 2.6 = on the gun).
  5. The plugin does the rest at runtime: on a fire event it detects the native socket hit and pre-compensates the donor’s socket-local offset (the AA gun’s barrel-length displacement would otherwise fling flash AND tracer start off the gun — they share one startPosition), then adds your dial.

Reading the offsets — a multi-barrel donor shows itself here. The pin log’s donorOff= values varied per shot (0.80, 0.85, 1.20): that’s the AA gun’s MULTIPLE BARRELS, expressed not as separate sockets but as different per-event displacements from the one Move_bloc anchor. The compensation flattens them all onto your single muzzle point — correct and verified, but it means every flash shares one spot. (Also don’t confuse the two quads: the Muzzle-bone Pick dropdown’s MW (4 parts) is your model’s four MG-mount bones grouped by name prefix — MW_B/F/L/T on the Ehrhardt — not the donor’s barrels.) Two unbuilt refinements ride this observation — see the Review-Backlog: barrel variation (subtract the MEAN donor offset instead of each event’s own, so flashes scatter naturally around the muzzle) and multi-mount fire (rotate successive fire events across several of your gun bones).

17.3 Traps (each cost a test cycle — don’t repeat them)

Textures have their own field guide now — the atlas pipeline, every texture knob (Atlas size, Keep black, Material mode, reuseExtracted…), the symptom-indexed failure catalog (pale wash, silver panels, white parts, vanished skins), and the runtime re-skin system: Textures.md.

15. Clone, Bake lock & entry-state coherence (2026-07-27)

Three workflow tools born from the T-62 marathon’s aftermath — the first two user-designed:

Clone (Factory, next to Refresh). Duplicates the loaded entry’s ENTIRE recipe into a NEW, unsaved form: blank Resource name (an unnamed clone can’t Bake or Save, so the source can never be overwritten by accident), blank Pawn description (Pick the new target), baked GUIDs cleared (a clone owns no assets until its own bake), bake lock and disabled never travel. The fast path for re-pointing a proven recipe at another unit — the first production use built the Universal Tank in minutes: load AntiTankHalftrack → Clone → name UniversalTanks → Pick the new pawn → Bake. The Factory→Lab handoff carries the full unsaved form, so “Edit in Animation Lab” on a fresh clone arrives with the complete animation recipe intact (deploy conversion, clips, flags — the whole point of cloning). The databases side (unit stats, tech unlock, UI card/portrait, presentation definitions) is authored separately as usual — Clone covers the MODEL side only.

Bake lock (Lab checkbox above Bake; the Factory respects it read-only). While ticked, Bake is disabled in BOTH windows (“Bake (locked)”). Use it on entries whose baked assets are in-game VERIFIED and whose recipe the shared tooling has moved past — the m114 sits locked until its engine-contract migration, because a rebake would silently reconvert under rules its choreography machinery predates (proven divergent by headless comparison). Unlocking is a deliberate act: untick → bake → RE-VERIFY IN-GAME. Related defense: conversions self-identify their contract via the armature name (DeployArmV2), so rig_anim gives pre-rework cached conversions their exact legacy export even after an unlock.

Coherence fixes. The Lab preview now follows the entry selection (it used to keep showing the previous model). And the long-hunted keepTranslations self-reverting trap is dead: the field was simply missing from RebaseLabOwnedOnRegistry, so every Factory bake overwrote the Lab’s tick with its stale copy — if a checkbox ever “unticks itself” again, check that rebase list first.