Unity Workflow LibraryUWLAbout

Topic 5 · DOTS

Latios, Kinemation & skinned animation

Skeletons, deform shaders and bone masks under Latios, and why units freeze in bind pose.

Skinned animation under Latios: skeletons, deform shaders, bone masks, and the several ways a unit ends up a bind-pose statue with a completely clean console.

  • Kinemation skin instances: bake a REAL entity prefab via a baker GetEntity(prefabGameObject) reference — never Prefab-tag a live skeleton and clone it; and under NetCode don't Parent a skeleton root to a ghost. Two paired landmines from wiring per-unit Latios skins: (1) manually adding Prefab to a baked live skeleton (or promoting one at runtime) clones WITHOUT its LinkedEntityGroup/bind state — the instance holds DependentSkinnedMesh etc. but renders nothing and Kinemation jobs throw (Index -1 out of range); the only reliable source is a SubScene authoring MB whose Baker stores GetEntity(authoring.skinPrefab, Dynamic) (LEG + binding baked prefab-side, Instantiate just works). (2) A Unity.Transforms.Parent added to a skeleton-root skin gets stripped the same frame by the NetCode/Latios stack (no PreviousParent left behind) — drive the skin with a per-tick follow system copying the root's position/rotation (and destroy the skin when its root is gone, or they leak per respawn). (Seen in a survival RTS: unit skins.)

  • Kinemation skin prefab ASSETS stay FBX prefab VARIANTS, and runtime skeleton spawners pin ordering against the buffer-init workaround. Two more skin landmines (same day, same stack): (1) building a skin prefab by UNPACKING the FBX instance and deleting unwanted kit-part nodes breaks Kinemation's optimized-rig mapping — socket bone indices bake against one hierarchy while the runtime OptimizedBoneTransform buffer is another length (UpdateSocketsSystem IndexOutOfRange flood). Keep the prefab a VARIANT of the FBX and trim kit parts with SetActive(false) overrides (inactive nodes bake Disabled, don't render, rig identical). (2) On Latios 0.15.x with a PreBounds/buffer-expansion workaround system, ANY system that Instantiates optimized skeletons at runtime must declare [UpdateBefore(<init system>)] — unpinned, the implicit sort reshuffles the day another system is added, fresh skeletons reach TransformSystemGroup with bind-pose-length buffers, and every spawn frame throws (classic unpinned read-after-write). Also: a follow job that writes LocalTransform by iteration while reading it via lookup needs [NativeDisableContainerSafetyRestriction] on the read lookup when the sets are provably disjoint.

  • Latios deform clothing lives on _Albedo_Map, not _BaseMap. Generated M_Deform_* variants keep the generic deform base on _BaseMap and the pack/clothing atlas on _Albedo_Map. Comparing or swapping only _BaseMap (URP Lit habit) silently no-ops in-match rematerial — units keep the pack look. Read _Albedo_Map first; never assign a URP Lit kit material onto a Kinemation skin (T-pose). Mounts (horse/wolf) are a second deform mat — skip them or every cavalry rematerial paints the animal.

  • Kinemation's deform path requires TANGENTS — vendor FBX packs that ship without them bake clean-looking skins whose subscene bake floods InvalidOperationException: Mesh data does not have Tangent vertex component from import workers. The skin-prefab build itself succeeds (the exceptions land seconds LATER during entity-scene bake/warmup, easily blamed on the wrong step), and Unity's importer default importTangents: Import silently yields NONE when the FBX carries no tangents (ImportFBX Warnings: Can't import tangents and binormals is the only breadcrumb). Before wiring a new model pack into a Latios/Entities-Graphics pipeline, audit every mesh with mesh.HasVertexAttribute(VertexAttribute.Tangent) — read the imported MESH back, never trust importer settings (same class as texture textureCompression lying) — and set ModelImporterTangents.CalculateMikk + SaveAndReimport on the misses. Packs are inconsistent even within one vendor line: in one four-pack drop exactly one race pack (all 22 FBXs) lacked tangents while 143 sibling models were fine. (Seen in a mobile strategy game: wiring the Dwarf unit pack; SW3 water has the same tangent requirement on meshes — see unity.md.)

  • In-place AnimatorController regeneration does NOT rebake dependent Mecanim data — force reimport controllers + skin prefabs + subscene, and let the bake FINISH before entering play. Editing a controller asset in place (GUID kept, per ASSET-REGEN-GUID) leaves already-baked skin-prefab MecanimController blobs and MecanimParameter buffers stale/mismatched against the new parameter list — name-resolved parameter writes land in wrong slots and NO transition ever fires (every skin frozen in its default state while Speed still drives; reads as "walk cycle during attacks / no death anim"). After any generator run: ImportAsset(...ForceUpdate) the controllers, the prefabs referencing them, AND the baked subscene — then WAIT for the import/bake to go idle; entering play mid-bake loads the stale artifacts again. (Seen in practice: twice same day on SC unit controllers.)

  • MecanimV2: freezing a final pose = a hold state pinned by a TIME parameter — state.speed ≈ 0 + cycleOffset does NOT freeze. MecanimV2 samples non-looping clips through LoopToClipTime (unconditional fmod), so a finished one-shot state visibly REPLAYS its clip forever, and the tempting speed≈0/cycleOffset≈1 "park" state doesn't hold the pose either. The working mechanism (PT AC_Pawn DeathHold, re-proven on SC generated controllers): a float parameter defaulting to ~0.99, and a hold state with timeParameterActive = true; timeParameter = "<param>" playing the SAME clip — normalized time is pinned by the parameter, nothing ever advances. Chain: one-shot state → (exit time 0.9-0.95, NEVER exactly 1.0 — that transition never fires in MecanimV2) → pinned hold with zero outgoing transitions. Works from AnimatorController generation code (UnityEditor.Animations.AnimatorState .timeParameterActive/.timeParameter).

  • Never InstallKinemation / Mecanim on a world that ticks before match start. Menu CreateLocalWorld and the Editor dummy are not presentation worlds — installing skins there Burst-JITs BatchSkinning in the Editor while the player AOT path stays ~1 GB. Do not GetAll(Default) on those worlds either (Default = Client+Server). Client world at match launch only. Rule owner: BOOT-LOCAL-IS-NOT-THE-MATCH in the Netcode overview page.

  • SIM-WINDOW-ANIM-FIT — every one-shot anim state tied to a sim window is speed-scaled to that window (speedParameter, driver sets clipSeconds / windowSeconds, clamp ≥1) — for EVERY window kind, not just the one that burned first. The server paces by AUTHORED seconds (attack cooldown, cast wind-up root, channel time); the client plays a fixed-length pack clip. Any window shorter than the clip leaves the tail animating over resumed behavior — reads as foot-slide the moment movement returns (the unit "casts while walking"). The trap is partial application: the attack swing gets the fit rule ("one swing fits one cooldown", PT bow lesson) and the NEXT one-shot state added (Cast, channel, emote) ships without it because nothing fails loudly — the slide is cosmetic and only developer eyes catch it. When adding any one-shot state driven by a sim window: add the speed param in the controller generator, set it on the trigger edge from the ghosted window seconds, and scale the FALLBACK path too (a caster without a Cast state plays its attack clip — same fit, same window). (a mobile strategy game: Blood Rage 0.7s cast root vs the 1.33s _11_cast pack clip — orcs slid the last ~0.6s of every cast; Attack had the fit for months, Cast never did.)

  • MECANIMV2-OPTIMIZED-SKELETON-ONLY (HARD) - Latios MecanimV2 drives OPTIMIZED skeletons exclusively; a skin rig imported with optimizeGameObjects=false bakes an EXPOSED skeleton whose Mecanim pose is silently never applied - a bind-pose statue with zero errors. The addon's own feature table says "Exposed Skeletons: Not Supported" (V1 supported them - the V1 habit is the trap). Parameters tick, blobs bake, transitions fire; nothing ever writes the bones, so every asset-side audit (clips pose the rig when sampled, controllers correct, registry wired, materials fine) comes back clean because nothing IS broken except the import flag. The diagnostic tell is elimination symmetry: Humanoid pack skins (optimized) animate while Generic authored-AC skins (exposed) freeze. Fix: optimizeGameObjects=true on every skin source FBX. Companion trap: vendor COLOR PREFABS (pack _Rd/_Pe colorway variants) are often plain UNPACKED copies, not prefab variants - they freeze the pre-optimization hierarchy, so a skin built FROM the color prefab stays exposed even after its FBX is optimized. Build the skin from the FBX and transfer the colorway's materials by renderer-node name. (a mobile strategy game: six air flyers + nine vendor god/hero replacements all bind-pose statues in match; an earlier lane had even play-claimed "wing-animating".)

Kinemation deform and masks (a project, both invisible-with-no-error)

  • A skinned mesh needs a DEFORM-AWARE shader — Kinemation ships a Shader Graph NODE, not a shader. The bake classifies deform by scanning the material's shader for _latiosCurrentDeformBase / _latiosCurrentVertexSkinningMatrixBase / _latiosCurrentVertexSkinningDqsBase; with none present it REFUSES the deform blob and the mesh renders bind-pose. Duplicate the vendor shader and add the Latios Deform node (SG browser: Input → Mesh Deformation, object space, Vertex block only) — never edit the vendor shader in place. Vendor environment/prop shaders carry no skinning nodes and will silently T-pose anything skinned that uses them. (Extends LATIOS-DEFORM-ALBEDO-MAP with the why and the fix.)

  • Layer bone masks need a TRANSFORM AvatarMask. Build them with SetTransformPath / SetTransformActive from the rig MODEL's ModelImporter.transformPaths. A mask authored via SetHumanoidBodyPartActive has transformCount == 0 and bakes a mask with ZERO bits — the layer looks completely healthy (weight 1, transitions fire, states advance) while writing its pose to no bones. The rig being humanoid is a red herring; Latios works at bone-transform level. Never enumerate the instantiated prefab hierarchy — an optimized skeleton has no bone Transforms there.

  • Drain skinned-skeleton spawns at a few per frame (~4), not EntityManager.Instantiate(prefab, N, …). At N ≳ 10 the simultaneous skeleton allocations plus first compute-skin dispatches stall the GPU past Windows' ~2 s TDR, which presents as a graphics-driver crash and gets blamed on hardware. Player-spawn paths (1–2 per connection) are exempt.

  • MECANIMV2-TRIGGERS-NEVER-RESET — MecanimV2 0.1.6 (source drop) never auto-resets triggers, although its docs promise it: transitions loop. It is a vendored-framework bug, patched in MecanimUpdater.Update; the patch must survive every addon re-drop, and "clear triggers every frame in game code" was the misdiagnosis that preceded it. (a project docs/pitfalls/pawn-animation-latios-mecanimv2.md:48.)