HumankindAssetFramework

Animation Pitfalls — a field guide to the woes

Which animation page do I want?

Every trap in this document was hit for real, cost hours, and is now either fixed in the tooling or has a one-field recipe answer.

THE GHOST ROTOR (2026-08-03, four hours, ~19 relaunches). If a donor’s part survives replacing the entire mesh, surviving descriptor zeroing, pawn sweeps, and even degenerating every vertex of every Fx ContentLayer — stop looking for geometry. It may be a VFX billboard: a 2D animated sprite (the gunship’s rotor) played by the donor’s animator through a Mecanim event, drawn procedurally with shader-generated quads. It has no mesh, no renderer, no pawn, and ignores every geometry lever by definition. The tell: it looks flat/depthless from every angle. The fix: silenceDonorVfx (all donor Mecanim VFX) — per-name filtering is planned. Related traps from the same hunt: gunship-class donors spawn a pawn SQUADRON the formation override cannot cap (use hideSubPawns), and respawnAfterLoad leaks a live sub-pawn per attempt (turn it off for own-rotor models). Read this BEFORE debugging an animated model that “looks wrong”: the odds are your problem is on this page, and the odds are the first three explanations you’ll think of are not the cause. (Case study behind almost every entry: migrating the M114 howitzer from the legacy fire/deploy behaviors to the state-driven machine, 2026-07-19 — a day in which the model file was accused of corruption, three preview renderers were rewritten, and the actual causes turned out to be four engine constraints nobody had written down.)

The four laws

These are engine-level facts. They are not bugs, they cannot be patched away, and every recipe must respect them.

REVISION 2026-07-25 — Laws 1 and 5 are OUR PIPELINE’S defaults, not engine walls. The caterpillar investigation decompiled the runtime: the clip format has RotationTranslation(+Scale) encodings, vanilla tank clips use them liberally (tread shuttle bones, gun recoil), and GetPoseTRS zeroes translation ONLY for Rotation-encoded curves. Our bake historically (a) keyed rotations only in the conversion rebake and (b) stripped every location fcurve. Both now have an opt-out: the per-model keepTranslations flag (Animation Lab ▸ “Keep bone translations”, conversion path only) keeps genuinely translation-animated bones — verified end-to-end in-game (a translating test bone baked as RotationTranslation and played with CORRECT amplitude; the clean-unit conversion export sidesteps the native-scale trap that motivated the strip). Laws 1/5 below remain the DEFAULT behavior and stay true for every model that doesn’t opt in; the re-express-as-rotation recipes remain valid and battle-tested.

Worked example — the M114’s REAL kickback (2026-07-26, in-game verified): keepTranslations + Recoil frames 442..530,305..441/2 + Return slow 0 + Slam 0 plays the source’s authored fire cycle complete: translation slam, slide home, reload, aiming raise (multi-segment epilogue). Implementation notes that matter: translations are kept ONLY in the attack-role clip (kept elsewhere they double-render pose offsets rotations already cover — the hovering-gun symptom), delta-rebased to zero at the clip’s first frame (pure motion), and ×100-compensated on the legacy path (the m→cm sandwich folds 0.01 into bindposes, which Amplitude carries into translation curves while rotations pass scale-free — the exact mechanism behind the original Law-1 evidence). Also fixed en route: the slam-0 sentinel placed the RecoilArm pivot at 1e9, collapsing bone chains via float32 cancellation — the source of every NaN/garbage-import symptom this pipeline ever showed.

Law 1 — The clip bake is ROTATION-ONLY (default — see the 2026-07-25 revision above)

The engine’s baked clips keep per-bone rotation and discard translation (GetPoseTRS forces translation 0, scale 1). Any part whose source motion slides plays pivoting about the wrong point in-game.

Law 2 — A stance baked as the PRIMARY clip renders as REST

The primary clip defines the skeleton’s reference pose; clip data encodes against it. Bake a 1–2-frame stance as the primary and it encodes ~identity — in-game the unit shows the rest pose (usually the travel pose) no matter what the stance was.

Law 3 — Pacing is BAKED, never a runtime knob

The runtime plays every clip at its authored length (24 fps). A 7.5 s authored fold outlasts a one-tile map move — the unit spends the whole trip in the fold’s first frames (“it forgets to fold”).

Law 5 — Bone POSITIONS are pinned at the bind pose; only ROTATIONS animate (default — see the revision above)

Stricter than Law 1, discovered by experiment (Arc-R scaling test): the engine keeps every bone at its bind-pose position and plays only orientations through the hierarchy. Consequences:

LAW 5, RESTATED PRECISELY (2026-08-22, the Vehicle Lab recoil). keepTranslations gets a translation curve into the clip, and the engine’s own GetPoseTRS decodes it correctly — both were measured end-to-end on a barrel that never moved a pixel in game ([AnimDiag] … SLID 0,3 (0,0,-0.001)->(-0.001,-0.013,-0.301), matching the authored frame to three decimals). Decoding is not rendering. A bone’s OWN local translation still does not move it; what moves a bone is its ancestors’ ROTATION, rebuilt by forward kinematics. deploy_convert recorded the same finding years of debugging earlier — “sliding cannon2’s local left its baked bbox unchanged”. So the far-pivot arm is not a workaround for a pipeline that can’t keep translations: it is how you move a bone. Diagnostic: [AnimDiag] reports SLID d (t0->tm) per bone per role, so “the data is there” and “the model moves” can be told apart in one line instead of a day.

Law 4 — What a preview shows is NOT what the game plays

Three different things can lie to you independently:

The method: sandbox + gate

What finally broke the debugging loop was not a cleverer theory — it was a measurement protocol:

  1. Sandbox on a donor unit. Never iterate on a shipping entry. Clone the recipe onto a throwaway unit (the SiegeHowitzersCar donor) so every failed bake costs nothing and the working entry stays as reference.
  2. Gate against the proven bake. When a “should-be-identical” bake misbehaves, don’t stare at the screen — diff the artifacts:
    • Assets/Resources/<name>_ClipsPoseData.bytes — byte-compare against the proven model’s file. Identical prefix + divergence at frame N is a location, not a mystery (a scene-state leak was found at frame ~103 this way).
    • slim FBXs — sample both in headless Blender and diff evaluated bone poses per frame (the gate scripts: load both, frame_set sweep, compare world bone heads + local quats; PASS = 0.0000). The whole Blender stage can be gated without ever opening Unity or the game.
  3. Change ONE delta per bake. The sandbox failures compounded exactly when several knobs moved at once (settings drift + stale caches + new file). The gate tells you which delta mattered.
  4. Bytes over eyes. Every “the file is corrupt!” accusation this day was wrong; every byte/pose diff was right. Measure chirality, don’t eyeball crossed legs; measure pose rows, don’t squint at stances.

The bake fails LOUDLY now (2026-08-16)

Three long-standing traps used to ship a broken rig with exit 0 — a silent bad bake you’d only catch in-game. The bake scripts (Tools/rig_anim.py, Tools/deploy_convert.py) now abort with a clear error instead:

A green bake (ANIMATED DONE with residual = 0.000000 and, on deploy models, DEPLOY baked N bones for N > 0) means all three guards passed. They never trip a valid bake — a converged fold reads ~1e-4, ~1000× under the floor.

Symptom index

Symptom (in-game unless said otherwise) Cause Fix
Part sweeps through the model; previews fine Law 1 (translation stripped) re-key as rotation (legScale / RecoilArm)
An authored wheel/bone roll sweeps through the air or pivots at the ground; the FBX previews perfectly a converted rig’s clips sit in a different frame than its rest (90° + scale) — see “Authoring INTO a converted rig” don’t author into a converted rig: carry the source’s own motion, rebuild on a Vehicle Lab rig, or fix the converter’s frame
Clip preview stands the model on end while the rest pose stands upright same — the clip and rest frames disagree not a preview artifact; measure the bbox both ways
Left/right pair (wheels) rotate about points at different heights non-identity rest rotation on those bones; the skeleton bake mangles the offset leave bone rests alone; compose the _Skeleton.asset chain and check the pair is symmetric
Idle shows travel pose; stance previews fine Law 2 (stance-as-primary) full clip as Idle/reference; stance in Idle override
“Forgets to fold” — travels deployed Law 3 (fold outlasts the move) speed step /6…, or empty Pre-move = instant snap
Fold/unfold plays but glacially Law 3 speed step
Unit invisible wrong animation id (invisible ⇒ id, frozen ⇒ constant data — the diagnostic dichotomy) check [Uni] clip log lines resolve
Unit frozen mid-pose constant clip data (hollow bake) byte-check PoseData; re-slim (cache busters below)
Whole unit tiny/huge/floating FBX unit scale Fix 100× oversize per model
A rotating part (wheel) flings off / orbits in the air while the body sits still; idle fine, only movement flings the m→cm ×100 export sandwich mangles rotating bones’ TRS (and Fix 100× ON re-creates it) Convert raw rig ON (cancels the ×100) + Fix 100× OFF — see below
Baked skin scrambled on one part (wheel) multi-material albedos missing — the animated path now generates them (glbconv) but a failed extraction falls back to a single atlas, loudly check [glbconv] Console errors, re-bake
Preview (custom window) shows mirrored/giant parts Law 4 (renderer bug) render real instances (AddSingleGO), never hand-rolled BakeMesh draws
Settings revert / edits ignored after compile stale window form (survives domain reload) the Lab re-syncs on reload + ↻ Reload button; registry file is the truth
A knob change bakes identical output stale slim cache — edits made through the Lab re-slim automatically; edits made directly to the registry file behind an open window do NOT (the cache compares form vs file) ↻ Reload first, or delete anim*/…_anim.fbx
Crossed/wrong limbs in a stance ROLE clip (historical) role slicing leaked pose values into channels the primary doesn’t key fixed: slicing saves/restores all pose bones (rig_anim.py)
Same bake differs run to run (historical) export-time pose was whatever frame the tool last touched — it becomes the engine’s reference fixed: every export pins the scene to the clip’s first frame
Whole gun pitches/dives when firing (close zoom) Law 5: the arc kick IS a pitch; it reads as a dive only nose-to-the-glass judge at map zoom; tune via recoil range/Return slow
Attack plays stale/old animation after a recipe edit (historical) Blender exits 0 even when the conversion script CRASHES — the baker reused the old converted GLB and recorded the bad args as success fixed: success = the script’s own final marker; reversed recoil ranges rejected with a clear error
Aim-layer suspicion during fire the donor streams runaway angles (5000°+) — but at the INVALID bone index sentinel (0xFFFFFFFF): applied to nothing exonerated; a throttled [Aim] log in ClearAimLayer shows what streams
A BoneRotation feature does nothing, and its own log says it applied ClearAimLayer zeroes ALL FOUR slots, and it runs LATER in the same frame than the feature writing one. Gun elevation owns slot 3 and was erased every frame — measured as a live 12.8° write on the right bone with no movement at all ClearAimLayer skips the slot the elevation owns; any future slot-writing feature must be excluded the same way. clearAimLayer is ticked on most artillery, so this bites exactly the models that want a gun
Model collapses flat onto the root, limbs flung (mech) rig has NO skin weights — parts rigidly bone-parented; the join drops the binding, all verts fall to bone #0 (Unity warns) fixed: conversion path converts bone-parenting to full-weight vertex groups (rig_anim.py)
Skeleton ~100× off the mesh, rigid parts become a “wing” wrapper empty with non-identity scale (mech: 0.010) survives export; Amplitude reads bind poses without it fixed: conversion path flattens wrapper empties before transform_apply
Huge stretch spikes in-game, Blender preview fine (detailed rig) over the 128-bone-INDEX GPU wall — NOT 256 (mech: 222 bones; verts on bone index >127 collapse — see The 128-bone-index GPU wall below) keep bone indices ≤127: zero-weight leaf bones removed, and the deploy path pair-merges instanced link chains to ≤126

The rotating-bone fling — the metre→centimetre sandwich

Case study: the Ehrhardt armored car (first custom spinning-wheels vehicle, 2026-07-24). Wheels attached and still at idle, but the moment the movement clip rotated them they flew off and orbited through the air while the hull stayed put. The same class of bug as the Combine soldier whose “head rode off his shoulders.”

Why: Blender’s FBX exporter writes metres→centimetres by scaling the ROOT objects ×100. Unity compensates with 0.01 in every skinned-mesh bindpose + a ×100 root — a sandwich Amplitude’s uniform-scale TRS composition mangles on any bone that rotates (a static bone composes fine; a rotating one orbits about a mis-scaled pivot). That is why idle looked perfect (0° rotation) and only movement flung. The ▶ picker and Unity preview also look perfect — they use a clean import, not the sandwiched bake (Law 4).

The cruel part: the size fix and the fling fix pull in opposite directions on the legacy path.

The answer is neither toggle — it’s convertRig. The conversion path exports with global_scale=0.01, which cancels the exporter’s ×100 at the source (rig_anim.py ~L691-699): net node scale 1, UnitScaleFactor 1, bind clusters 1 — the clean profile. So a rotating-bone rig bakes correct and grounded with:

Convert raw rig ON + Fix 100× oversize OFF.

This overturns the old “convertRig OFF for clean purpose-made rigs” guidance: a purpose-made rig with rotating bones (wheels, turret, propeller-on-bone) still needs convertRig ON, unless its source file happens to carry a 0.01 object scale that already cancels the ×100 (the ReconDrone’s luck — which is why the drone bakes fine OFF). When in doubt for a rig with any spinning part: convertRig ON.

Placement is automatic on both paths (2026-09-20 — “switching between static and animated should give the same result in both facing and offset”). Every bake, static or animated, centres the model’s box on the origin horizontally and drops its lowest point to it. Until this change the animated path did neither — only the opt-in Auto-ground toggle did the vertical half — so an animated bake sat wherever the artist left the model inside its file, and the same model switched between the two paths jumped (the steam frigate: 0.97 × 1.84 game units at size 5). The toggle is gone; there is nothing left to tick.

Re-dialing after the placement change (the one-time migration)

Nothing moves until an entry is re-baked — placement is baked in — so this is per-entry and can wait. When you do re-bake, the model centres itself, and whatever your Position offset was cancelling is suddenly an over-correction.

Run the migration tool against your pack:

blender --background --python placement_shift.py -- "…/BepInEx/config/haf_packs/<mod>/pack.json"

It measures every animated entry the way rig_anim.py does — junk cull, the entry’s reference-clip pose, the registry rotation folded into the geometry, then the box and size/longest into game units — and prints the Position offset each entry needs to look exactly as it does today:

new_x = old_x + move_x     new_y = old_y + move_y     new_z = old_z − lift   (lift only if it was not auto-grounded)

How you know the sign is right: an entry whose dial was pure compensation collapses to ~0. On the shipped ENC pack four independently hand-dialed entries collapse at once — GatlingGuns −3.70 + 3.34 = −0.36, AntiTankIFV +0.50 − 0.497 = +0.003, StealthHelicopter (−0.50, +0.50) + (+0.391, −0.525) = (−0.11, −0.03), TOW-Infantry −0.30 + 0.247 = −0.05. With the sign the other way each would double (the Gatling guns to −7.04 on a size-2.5 model), which nobody would have shipped.

Two things it has to get right, and both are easy to get quietly wrong (PR #72 review found both):

An entry you have ALREADY re-baked is done. Its dial is in the new frame, and the table would move it a second time. The tool cannot tell which is which — it prints the warning and leaves that to you. It also reports deploy-converted entries as not measured: that recipe synthesizes its own rig and clips, so the honest answer is to re-bake and read the bake’s own RIGANIM placement: world centre … line, which is the ground truth for any row here.

Work the largest change first: if it lands on its hex, the rest follow. If it lands twice as wrong, stop and re-derive the sign rather than hand-fixing a dozen entries.

Turretize — aim a turret (or artillery barrel) at the target

The game already computes the aim and streams it as a HEADING angle into a PawnEntry.BoneRotation0-3 slot ({SkeletonBoneIndex, AxisIndex, Angle}) — but on an injected model that slot’s SkeletonBoneIndex is the invalid 0xFFFFFFFF sentinel, so it drives nothing. Turretize retargets that slot to your turret bone, so the engine’s own aim math rotates it — no per-frame trig.

Caterpillar treads — the loose-track saga (2026-07-26)

Making the Jagdpanzer’s tread move took seventeen rig revisions; these are the lessons that survived, so nobody walks the dead ends again (the working system is documented in Animated-Models.md → treadize):

The spike plague — engine-seam debugging, a field method (2026-07-26)

The first in-game launch of the 242-bone translating tread skeleton exploded into map-spanning spike ribbons, missing tread geometry, and twitching that touched VANILLA units. One afternoon of one-change-per-launch debugging found FIVE independent real defects stacked on top of each other — worth recording because any high-bone custom unit can hit each of them again:

  1. Decimation shreds link-cell skinning (the biggest). The animated path’s “Reduce to ~tris (0 = off)” silently substituted 12,000 — and a decimator eats flat, dense geometry (the subdivided tread band) first, merging verts ACROSS rigid-cell boundaries. Blended weights between distant link bones = vertices torn across the map as links move. For link-cell treads decimation must be OFF (0 now honestly means off).
  2. Zero-weight bones get silently dropped between Blender and the baked assets — the side-skirt-hidden tread stretch produced empty cells whose bones vanished, shifting every bone index above them. The rig now creates bones ONLY for vert-owning cells; verify with the mesh line: bones == bindposes, no name gaps.
  3. A two-fragment donor draws its own skinned tread submesh over yours, skinned by donor bone indices against YOUR skeleton (garbage). hideMeshes handles it — but only since the hide also patches…
  4. …the GPU pawn descriptor SNAPSHOTS (fragments AND BonesCount) taken at RegisterPawnDefinition, BEFORE the plugin’s swap. Both are now patched in place (the same surgical mechanism the hand-prop append uses).
  5. The shared per-frame animated-bone pool (65,535 entries for ALL pawns on screen) overflows once high-bone customs multiply — overflowing pawns read other pawns’ matrices (vanilla units spiking!). SkeletonBoneBudget (plugin config) now sizes it (default 262,144).

The method that actually worked — in order of leverage:

What the legacy howitzer really was (calibrate your expectations)

The “old functionality” everyone remembers was one clip + two runtime tricks: hold the full deploy clip at normalized time 0.999 when idle (0.999, not 1.0 — Repeat(1.0) wraps to frame 0, the folded pose: the original edge-overflow bug), and snap to frame 0 while moving. No stance clips, no fold animation, no state machine. Recreating it state-driven therefore wasn’t porting — it was building five clips through machinery the legacy path never exercised, which is why “it worked before” was true and useless at the same time. The state-driven equivalent that ends up matching it, entirely in data:

Role Clip Why
Idle / reference deploy (full) Law 2 — defines the reference pose
Idle stance (override) deploy[179..180] the deployed hold, as a role
Movement deploy[0..0] travel stance
Pre-movement deploy[179..0/12] (or empty) fast fold (empty = legacy instant snap)
After-movement deploy[0..179/3] the unfold
Attack deploy[180..250] the source’s own recoil kick

The engine contract — decoded from the live engine (2026-07-26, the T-62 marathon)

One evening, one Sketchfab T-62 with object-baked animation, and seven consecutive in-game failure modes — each one a real engine constraint nobody had written down. The instruments that ended the guessing are now permanent plugin residents: [AnimDiag] (one-shot per entry: the engine’s live per-bone GPUAnimationEntry records — FrameCount/Format/StartPoseData/BBox — plus the engine’s OWN GetPoseTRS decode at frame 0 and mid-clip, plus the skeleton rest TRS) and [PawnLive] (throttled: the pawn entry AS THE GAME LEAVES IT — pose slot ids/weights/times, BoneRotation records). Read both from BepInEx LogOutput.log; read the editor side from Unity’s Editor.log instead of squinting at the console.

The contract itself. Amplitude’s clip encoder normalizes every clip against the skeleton’s BIND rest and discards any constant frame-0 offset. Every working unit shows the same shape in [AnimDiag]: skeleton rest carries the full pose, clips decode to ~identity deltas at frame 0. Therefore BIND must equal animation frame 0 — a model whose bind differs from f0 renders its bind, forever, no matter what plays. The m114 satisfied this by accident (raw local verts + node transforms carried each part’s rotation into the bindposes); the clean-unit rework broke it, and the fix is structural, in deploy_convert: verts folded to their full frame-0 world state, translation-only axis-aligned bones (safe through Blender→FBX bone-axis conversion), pose-scale fcurves stripped (a cm-source’s constant 0.01 lands in pose scale keys the engine mishandles — the AW101 missing-fuselage class), and every bone’s keys delta-form rebased (basis_f' = basis_f @ basis_0⁻¹, hemisphere-continuous — identity at f0 by construction).

The 128-bone-index GPU wall. Per-vertex bone indices break past 127 — not 256. Bones 128+ render collapsed/invisible (the T-62’s turret and wheels, bones 128–140, vanished while links 1–120 animated). This retroactively closes two cold cases: the Jagdpanzer’s 241-bone spike ceiling and the mech’s broken wings at 222 bones. deploy_convert clamps to 126 total by pair-merging instanced link chains (a dropped link binds to its numeric neighbor’s bone and rides it rigidly). Merges MUST be spread evenly across all chains — clustered merges put every rider on one half of one track and that half fails together in-game; distributed, each rider only mis-swings during its own brief wrap transit (~2 links visible at cinematic zoom, invisible at gameplay zoom).

Three smaller laws from the same night.

The from-source tracked-vehicle recipe (what all of the above buys): any model whose animation is baked as rigid-part object motion — no armature needed — becomes a fully animated vehicle with NO rigging work: Deploy conversion ✓ + frame range, Idle/reference = full clip, Idle stance = clip[0..0], Movement = full clip (slice later for pacing), Keep bone translations ✓, Clear aim layer ✓ (artillery-family donors stream aim junk onto arbitrary bones), Fix 100× OFF, Convert raw rig OFF. deploy_convert handles unit normalization, recentering, root-motion anchoring (a source that drives across its scene bakes hull-relative, in-place), bone slimming (bones only for binding targets — 1033-node wrapper rigs collapse to ~139) and the 128-wall budget automatically. “Has baked animation” is now a BONUS when sourcing models, not a complication.

Meta-lesson (the trap that burned three bakes): the Factory and Animation Lab windows hold separate in-memory copies of shared entry state; baking from one silently reverts fields edited via the other (or via the registry file directly) — keepTranslations was lost three times this way. Until the root cause is fixed: after ANY field change, Reload in the window you’ll bake from and eyeball the checkbox before pressing Bake.

Authoring INTO a converted rig — you can carry motion, you can’t add it (2026-08-22, the howitzer’s wheels)

deploy_convert gives a rigid-part Sketchfab rip a working unit for free. What it does not give is a rig you can author new bone motion into. Spending a day proving that produced the sharpest statement of the limit:

Motion the source already animates rides through correctly. Motion you key onto a converted bone does not.

The T-62 is the proof of the first half — deploy-converted, and its wheels spin and its tracks crawl in-game, because its Sketchfab source animated them, so that motion sits in the same frame as everything else in the clip. The M114 howitzer is the proof of the second half: its source never rotates its wheels, so we keyed a roll onto the wheel bones — and no version of that roll ever looked right in-game.

Why. A converted rig does not share a frame with itself. Measured on the shipped M114:

  measurement
model at rest pose bbox (52.1, 135.7, 37.6) — gun along Y, 37.6 tall
the same model inside any clip folded (41.7, 27.6, 119.3), deployed (41.7, 55.8, 143.9) — gun along Z: rotated ~90°
the legacy deploy clip (83.4, 55.3, 238.6) — same rotation, and 2× scale
baked skeleton scales howitzer:main Local 2, wheel BindPose 0.005 (a clean rig — the ArmouredCar — reads 1 / 1)

Everything that acts at pawn level is unaffected: the engine’s own orientation hides the mismatch, which is why the model looks perfect in-game and every pawn-level feature (fold-before-moving, deploy-on-stop, the bombard hold, turn-in-place) works. Everything that acts at bone level inherits a frame that disagrees with the geometry — so an authored wheel roll pivots about a point that is not the hub, however correct the input.

The tell you can see for free: open the clip in the Lab’s ▶ Play in preview. If the model stands on end there while its rest pose stands upright, the clip and the rest are in different frames. (The user spotted this immediately from the preview; it was explained away twice as “a harmless preview artifact” before the bbox numbers above confirmed it. It is not harmless — it is the whole defect.)

Verify a bake OFFLINE — no game launch, no guessing

Four in-game drills can only report “it still looks wrong”. These answer why, in seconds, on the bake output:

  1. PivotsAssets/Resources/<name>_Skeleton.asset is plain YAML. Compose each bone’s Local TRS up its ParentIndex chain. A left/right pair (wheels, legs) must come out symmetric, differing only sideways. Asymmetric heights = the pivot is wrong. This is what finally localised the wheel bug: l_wheel y −0.4038 vs r_wheel y +1.4612, while the legs read 0.5851 / 0.5751.
  2. Frame — import the converted GLB in Blender, measure the mesh bbox at rest and inside the clip. Different orientation = the table above.
  3. Does the part actually spin in place? — evaluate the armature per frame and track the centroid of the verts weighted to that bone. Constant centroid = spinning; drifting = orbiting.
  4. In-game, what does the clip really animate?[AnimDiag] <model>:<role> bones that MOVE (f0 vs mid) names every bone whose rotation changes between frame 0 and the middle frame, with its angle. A flattened clip prints NONE — the baked clip is a held pose; a working wheel roll prints the wheel bones. Run it on a model you believe works as the control.

Dead ends — don’t re-walk them

If you need authored bone motion on a converted model — the Vehicle Lab route (VERIFIED in-game 2026-08-22)

Rebuild the model on a Vehicle Lab rig. Done on the M114 the same afternoon the limit was found, and every measurement that failed on the converted rig passes on the generated one:

check converted rig Vehicle Lab rig
clip frame vs rest frame (41.7, 27.6, 119.3) vs (52.1, 135.7, 37.6) — 90 deg out identical (52.1, 135.7, 37.6)
wheel pivots (baked skeleton) l −0.40 / r +1.46 — one below its hub, one above symmetric ±0.9325, 0.5424, −0.001
chain scales main Local 2, wheel Bind 0.005 all 1
bone count 28 (bone-per-part) 4 (VehicleRig, Root, two wheels)

Recipe: Vehicle Lab -> Browse the STATIC source (m114_gun_only.glb — no armature, no crew), mark the two road wheels W (watch the auto-guess: it also grabs the crews hand-cranks by name), Generate rig; then Factory/Lab with Deploy conversion **OFF**, Convert raw rig **ON**, Fix 100x **OFF**, Idle stance Spin[0..0], Movement Spin, and every deploy…/recoil` clip field cleared. Cost: the fold, deploy and recoil the converter gave you are gone until re-authored on the clean rig.

The alternatives remain: find a source that animates the part itself (the T-62 route), or fix the converter so clips are authored in the rest poses frame — the real repair, with the acceptance test **folded at frame 1 must have the rest poses bbox orientation**.