Topic 9 · Animation
Animation (Mecanim)
Clip timing, Write Defaults, bindings that die silently, IK and rigs.
Non-DOTS animation: what a clip's length actually means on screen, Write Defaults, controller bindings that die silently, and particle effects that never age past frame 0.
-
**ANIMATION-BAKE-PRESERVES-EULER-ORDER ** - Parts separating during a baked animation can be a rotation-decoding fault even when every position key matches. Preserve the source Euler rotation order when sampling; XYZ curves are not equivalent to
Quaternion.Euler's ZXY composition. In Unity 6000.5, copying serializedm_EulerCurves[].curve.m_RotationOrderalone was not durable: a later unrelatedAnimationUtility.SetEditorCurvereset it. Bake evaluated orientations as quaternion tracks with continuous signs, including constant Idle/rest clips, and remove competing Euler tracks. Compare nativeAnimationClip.SampleAnimationposes and rigid mesh points in their parent part's frame, both at keys and between them; resampling still has interpolation error. Recheck dependent grips and sight calibration in Play. Observed: wrong order displaced a rigid part 23-27 cm and rotated Idle by 5 degrees; native comparisons exposed both while scalar-key checks passed. The reference evaluator must not reuse the bake's decoder. -
Write Defaults ON is often the ONLY writer of a prefab's authored default — flipping it OFF permanently breaks the pose, and disabling the Animator freezes it mid-blend. Two paired Mecanim footguns that both look like free perf wins on an idle Animator: (1) an "empty" default state (
m_Motion: {fileID: 0}) withm_WriteDefaultValues: 1is what writes the prefab's authored values back every frame — if a clip animatesm_LocalScaleto 0.477 but the prefab default is 0.7, WD:1 is the only thing restoring 0.7, so WD:0 leaves every instance permanently shrunk after the first play. Before flipping WD, diff the binding SETS of every clip in the layer: any binding present in one clip and absent from another is a value that will stick. (2)SetActive(false)/animator.enabled = falseon a controller whose prefab hasm_WriteDefaultValuesOnDisable: 0freezes the current pose instead of resetting it — gate it from anOnStateExitand you freeze mid-cross-fade (exit time is a fraction of the clip, not the end), leaving half-faded graphics rasterising forever. Also:m_CullingModeis inert on a Renderer-less hierarchy — Animator culling keys offRenderervisibility, so a Canvas/UI-only object always animates regardless. And triggers set on a disabled Animator are silently lost, so any gating needs the enable to precede theSetTrigger. (Verified on a mobile card gamePass-Animation.prefab:278/:288+SeatPass_CTRL.controller:204while auditing an iPad frame-rate drop — all three "obvious" optimisations on five idle per-seat Animators were net-negative.) -
ANIMFBX-TAKE-SWAP-UNBINDS-CONTROLLER — overwriting an animation FBX with a DIFFERENT source take silently NULLs every AnimatorController state that referenced it. A clip is a sub-asset whose identity follows the imported TAKE, not the file path or the clip's display name, so re-importing the same path with a new take (a different mocap action, a re-export from another source clip) leaves
state.motion == nullon every state — with no error, no warning, no missing-reference marker — and the unit just stands in bind pose at play. Re-buying/re-exporting the SAME take masks the trap entirely (bindings survive), which is why it surfaces only on the swap. After ANY animation-source swap: re-point the controller states in the same slice and assert eachstate.motion != nullprogrammatically — do not eyeball the Animator window, an empty motion slot reads as an ordinary state. Same class as the missing-script stub trap (the assets serialization page): the data is intact, the REFERENCE is what died. (a mobile strategy game, swapping a demon's clip set to different library actions.) -
ANIM-RETIME-CLIP — "make the clip longer" is three edits, and two of them are invisible if you skip them. Retiming an
.animby hand (scaling everytime:so the clip fills a target duration) is only correct if you also: (1) divideinSlope/outSlopeby the same factor — tangents are value-units per second, so leaving them alone silently distorts every ease and overshoot even though the keys land where you meant;inWeight/outWeightare normalised fractions of their segment and must not be touched; and (2) apply the identical transform tom_EditorCurves/m_EulerEditorCurves— those mirror the runtime curves for the Animation window, so skipping them makes the editor draw something different from what plays. Snap scaled times back onto the clip's ownm_SampleRategrid (round(t*fps)/fps) or the keys land off-frame and the clip becomes miserable to hand-edit later; stretching (factor > 1) can never collide two on-grid keys, shrinking can, so assert it. -
…and clip length is NOT on-screen duration. A state whose transition has
m_HasExitTime: 1withm_ExitTime < 1starts blending out at that fraction of the clip, so a longer clip is cut proportionally longer and the authored tail (a pop-out, a settle) never fully plays — lengthening the clip alone changes nothing visible. Fix the clip and them_ExitTimetogether. Corollary: raisingm_ExitTimeto 1 on a clip withm_LoopTime: 1makes it wrap and replay its intro underneath the exit blend — clearm_LoopTimeon one-shots so the last frame holds instead. Measure duration asclipLength * exitTime + transitionDuration, neverclip.length. (a mobile card game, stretching five emote clips to 3s: the exit transitions were firing at 77–89%, so the retime would have been entirely invisible on its own.) -
ONE-SHOTS-ARE-EDGE-TRIGGERS — a one-shot effect fires once, on the rising edge of a replicated flag or counter, with the baseline primed on first observation. Source: learned in shipped-project VFX work (networked one-shot effects); the rule applies to any one-shot effect or event, not only VFX. Status: locked (empirical). Fire one-shot effects (an impact burst, a per-tick damage puff, a pickup or cure flash) once, when a replicated flag goes from false to true or a counter increments, and record the current value as the baseline the first time the object is observed, so late joiners and respawned or pooled objects do not replay old bursts. Ongoing states (burning, stunned, a zone active) are plain replicated bools or 0..1 values that drive looping visuals, never one-shots. Why: a client that compares against a default baseline sees every existing flag or counter as "just changed" when it first spawns the object, and a one-shot driven by a level instead of an edge repeats while the level holds. Check: a late-joining client and a pool-returned object show no stray burst; holding the state for 10 s plays exactly one burst. Pairs with PARTICLE-SHOW-IS-EDGE-TRIGGERED below, the local half of the same rule.
-
PARTICLE-SHOW-IS-EDGE-TRIGGERED — a per-frame "show this effect here" path calls only the MOVE, never the RESTART;
Clear+Playevery frame pins every particle at age 0, and any size/color-over-lifetime curve starting at 0 then renders it permanently INVISIBLE. The shape that burns: a hover/aim/preview follower whose update doesfx.transform.position = p; fx.Play;(or a presenter method that internally doessystem.Clear(true); system.Play(true)), because "make sure it's playing" reads harmless. It is not — the effect is re-seeded every frame, so its particles never age past frame 0. With a Size-over-Lifetime curve that ramps 0 → 1 over the first ~10% of life (the norm on vendor ground rings, shockwaves, decals) the particle is drawn at scale 0 and the effect is simply never visible; with a fade-in alpha ramp it is fully transparent. Worse on a single-burst emitter (rateOverTime 0 + one burst at t=0, the standard authoring for a ground ring): there is exactly one particle to kill, so the whole effect vanishes rather than flickering — and it looks identical to "the prefab isn't wired", sending you to hunt a null ref that does not exist. Correct shape: split MOVE from START. Per frame set the transform/scale/radius only; call the restart on the hidden → shown edge, and again only when a property that is applied AT EMIT changes (main.startColor,startSize— an already-alive particle never picks those up, which is why a green/red validity tint DOES need a restart while a position change does not). Track the shown/variant state in the caller that owns visibility. Diagnose by reading the emitter:rateOverTime == 0+m_BurstscycleCount: 1means one particle, and aSizeModule/ColorModulecurve whosetime: 0key hasvalue: 0proves an age-0 particle cannot render. (Seen in a mobile strategy game: the Fire Rain aim ring never appeared —ShowAimAtcalledpresenter.Playevery Update while the ring was one burst particle with size 0 → 1 over the first 11%.) -
PARTICLE-SIMULATE-PAUSES-THE-CLOCK .
ParticleSystem.Simulateadvances particles and then pauses the system. A tail/IsAlivetest that waits afterward can diagnose its own frozen fixture as a pool leak. Prefer actual Play-frame aging, or explicitly resume playback after fast-forwarding; compare against an independent instance with the same authored settings. Recordtime,isPaused,isPlaying, emission and particle counts as well asIsAlive(true). Measured: both tested and reference systems held zero particles,IsAlive=true,isPaused=trueat time 2 for 37 seconds. A real-clock replacement completed ten variants over 202 frames with 12,636 checks; that result does not establish offscreen-culling behavior. -
PARTICLE-VELOCITY-AXES-SHARE-CURVE-MODE . Author compatible MinMaxCurve modes on all enabled velocity-over-lifetime axes, including zero axes. If the varying axis uses two constants, use explicit two-constant zero on the other axes. Fix both the producer and saved output, read back that exact enabled module, then verify ordinary Play has no velocity-mode errors. Measured: x/y/z modes 0/3/0 produced errors; 3/3/3 retained upward motion for 42 seconds with zero errors. This observation concerns velocity over lifetime, not every particle module.
-
EYE-FOLLOW-PASSES-THE-BREATH . A first-person camera that rides the animated eye height through a settle filter tuned against the walk bob lets the idle breath straight through: the breath is the SLOW signal and a low-pass keeps slow signals. Measured: a 0.35 s settle took a 1.9-3.2 cm walk/run bob at 2.5-3.3 Hz down to under 0.4 cm, but passed a 2.5 cm, 2.7 s idle breath at about 2 cm. Put a still band (hysteresis dead zone) BEFORE the settle, sized to half the idle's peak-to-peak: 1.5 cm for camera height; 3.5 cm for any body-under-the-lens slide, because the same breath swayed the head 6 cm fore and aft and slid the feet to follow it. Test both halves: a sine breath inside the band moves the eye under 1 mm over 10 s, and a 40 cm crouch still arrives within the band. Size the settle test to the settle: at 0.35 s the crouch is 94% there at 1 s. (Amended 0.5.71: build the band smooth and the settle as a spring - see EYE-FOLLOW-IS-A-SPRING-NOT-A-CLAMP.)
-
EYE-FOLLOW-IS-A-SPRING-NOT-A-CLAMP . A first-person lens that followed the animated eyes through a hard still band, a first-order ease and a hard lag clamp felt "rigid and jumpy" in Play: each one is a velocity step. The band releases a step when the gap leaves it, and the clamp snaps the lens onto the eyes' speed when it engages. Measured with the shipped function at 144 fps: a 30 cm landing drop gave 324 m/s2 and a 50 cm animated stance change 274 m/s2, while a steady run was already smooth (1.7 m/s2). The jerk is in the transitions, so a gait-only test misses it. Fix: a smooth band (
gap - band * tanh(gap / band)) into an exact critically damped step (stable at 10 fps), with the frequency raised by1 + (drop / dropLag)^2only while the eyes are below the lens, so a drop cannot show the neck. Same inputs gave 190 and 100 m/s2 (a rise 4.8) with the same run smoothing. Test the transitions against the old function called directly, not a hand copy of it. -
CAMERA-OFFSET-LEADS-THE-WEAPON-TARGET . A first-person weapon target is placed BEFORE the animation evaluates and reads last frame's lens offset, while the camera takes this frame's offset after. Any camera-only motion added there (walk bob, landing dip) therefore swims the gun on screen by one frame's change: 0.09 cm a frame at 144 fps and 0.21 cm at 60 fps for a 1.2 cm sprint bob. Step authored offsets before the weapon target is placed and add this frame's change to it, so gun and camera move together. Only the part that needs the evaluated pose (following the animated eyes) may lag a frame; measure that lag per frame. It reached 2.4 cm a frame on a hard landing at 144 fps and 5.6 cm at 60 fps.
-
REVERSED-CLIP-IS-THE-OPPOSITE-DIRECTION . A missing locomotion direction is filled by the opposite direction's clip played backwards, not by blending its neighbours. Blending is the trap it looks like the cheap fix: two perpendicular strides mixed at 45 degrees move the planted foot at about cos(45) = 71% of ground speed, so the feet skate, and the blend also inherits any crossing step in the neighbour clips - shipped once, the developer's verdict was that the corners were worse than the crossed originals they replaced. A run reversed keeps its own uncrossed legs and its foot-plant timing, and the human gait is near enough symmetric that it reads as the opposite corner: measured, the reversed forward corners travelled within 5 degrees of the exact opposite heading at the same speed, crossed in 0 of 120 frames, and their planted foot slid 0.35-0.39 of ground speed against 0.32 for the forward corner the developer had approved. It does not fix a SIDE direction: a side clip reversed is still the same side. Reverse by rebuilding the curves, not by a negative speed on a state (a blend tree child cannot hold one, and the asset stays honest): for each
AnimationUtility.GetCurveBindingscurve, walk the keys backwards intonew Keyframe(length - t, value, -outTangent, -inTangent, outWeight, inWeight)withweightedModeIn and Out swapped, then copy the clip settings over so loop time and cycle offset survive. Bake it to a project.anim(a gitignored vendor clip cannot be a binding) and give the authoring surface the button that does it, so the next direction costs a click. Verify a filled direction by driving the view without Play: crossed frames, foot gap and planted-foot slide as a share of ground speed, read against the direction the developer already approved - the slide metric carries a baseline, so only the comparison means anything. -
SQUARE-HIPS-CAN-STILL-CROSS-THE-FEET (second half superseded by). Hips square to the look do not make a strafe clip usable: a commercial pack's strafe-run back corners kept the hips within 10 degrees yet held the right foot left of the left for 26 of 49 samples, by up to 41 cm. Measure the mean of (rightFoot - leftFoot) along the hip axis over one cycle, in hip widths (divide the dot by the hip vector's squared length): 13 clean walk/jog clips read 1.2 to 2.7, the crossing ones below 0. Make the controller builder refuse a walk or jog under 0.5, next to its hips-turn guard. Measure in edit mode with
clip.SampleAnimation; a PlayableGraph evaluated outside Play leaves every bone still. Do NOT leave the direction unbound for the tree to blend - see REVERSED-CLIP-IS-THE- OPPOSITE-DIRECTION, which replaces that half of this rule. -
SUB-EMITTER-PLAYS-ON-ITS-OWN . A sub-emitter is still a ParticleSystem with its own Play On Awake and Looping. With both on it was found playing by itself while its parent read stopped, firing its own burst every
durationat the sub-emitter's origin with no parent particle to trigger it. Author sub-emitters with Play On Awake off, and never instantiate a system you do not play: a vendor weather module in GPU-graph mode still instantiated its legacy particle rain and stopped it. CheckisPlayingon every sub-emitter, not only the parent. Measured: parentisStopped,time0; its collision sub-emitterisPlaying,timeadvancing, one 0.1 m particle per 0.6 s at its origin 25 m over the camera - a pulsing dot straight overhead that follows the player. -
MECANIM-OVERRIDE-LAYER-WD — an EMPTY full-weight override layer with Write Defaults ON writes the default pose over the base and freezes the whole rig (sibling of ANIM-WRITE-DEFAULTS, the opposite direction). One locomotion driver per Animator — two stack silently.
CullUpdateTransforms(the correct default) does not pose off-screen bones, so verify that the composited pose CHANGES, not that parameters are live — a handoff "passed" on live params while nothing moved. Discarded root motion + the default humanoid root reference gives a rigid stride-frequency counter-sway: Bake Into Pose / Based Upon Original. (a survival game) -
ANIM-HUMANOID-FINGER-BINDINGS —
HumanTrait.MuscleNamedisplay names are not always clip binding names. Unity Humanoid finger channels such asRight Index 1 StretchedandRight Index Spreadserialize asRightHand.Index.1 StretchedandRightHand.Index.Spread(with the analogousLeftHandforms). When baking curves, map only the 40 finger channels to the serialized hand/finger paths and preserve the other muscle names. Verify the resulting bone pose or serialized bindings; a human-motion flag or valid Avatar does not prove the fingers are driving the intended bones. (Observed against a valid Unity Humanoid clip; final geometry acceptance remains a separate verification.) -
ANIM-FIRSTPERSON-HEAD-COVERAGE — first-person head visibility must cover every renderer type. A visibility or preview pass that handles only
SkinnedMeshRendererleaves rigid head attachments or descendants visible, while a skinned-only inventory preview omits rigid wearables. Traverse the authored head hierarchy and inventory attachments for bothSkinnedMeshRendererandMeshRenderer/rigid renderer descendants, then verify the actual camera image and geometry; source coverage alone does not establish a clean view. -
POOLED-PARTICLE-SCALE-INCLUDES-DESCENDANTS — when a pooled effect's root owns its overall scale, author every descendant ParticleSystem with
main.scalingMode = Hierarchy.Localignores ancestor scale;Shapescales the emitter shape without scaling particle size/motion. Include inactive children and sub-emitters in both desktop/mobile variants. Preserve authored child transforms; multiplyingstartSizeas well creates a second scale owner. Playback must forward the authored scale to the pooled root instead of resetting it to 1. Verify two materially different root scales using live particles or rendered extents, not a Transform read-back: a root at 0.1 can still emit full-size child shards. -
ANIM-MANUAL-GRAPH-OWNS-INPUT . When manually evaluating a vendor animation graph, advance its dependent input interpolation exactly once before evaluation. FPSAF UserInputController.SetValue can write an interpolation target without changing the consumed float until Update runs. EditMode/manual probes must reproduce that scheduling; otherwise an apparent pose-layer or mask defect may be a stale input value.
-
ANIM-OPTIC-FIT-INCLUDES-REACH . Align the physical sight's position and rotation, not an assumed forward axis, and test authored eye relief against real limb reach in every stance and perspective. Correct ADS math can still leave the support grip unreachable. Verify actual camera clearance too. If the graph overwrites an Animator-root offset, use an authored parent outside the Animator hierarchy and compensate the same translation in the aim target; never move authoritative collision or aim merely to fit a presentation mesh. Check the resulting feet/body placement, reload release/reattachment and graph reactivation.
-
ANIM-CONTROLLER-ARTIFACT-OUTLIVES-ITS-YAML . An AnimatorController is TWO things: the editor graph (states, blend trees, motion references) and a compiled blob the runtime actually plays. They can disagree. A branch switch or an interrupted import can leave a stale compiled artifact while the
.controllerfile on disk stays byte-correct, so every review that reads the YAML, walkslayer.stateMachine.states, or resolvesblendTree.children[i].motionreports a perfectly healthy controller — and the character still animates from nothing. The symptom is a pose that is CONSTANT across every state: with no clip playing, a humanoid falls to the avatar's zero pose, which puts the hips near the animator origin instead of at hip height. Measured on a 1.9 m body: hips 1.07 m low, feet 0.77 m below the character root, head down at chest height, while the model's rest pose and every clip'sRootT.y(0.92–0.96) were correct. It reads as a sunk or mangled model, so it is misdiagnosed as a rig, avatar, mesh or IK defect. Detect it with ONE number:controller.animationClips.Lengthagainst the clips reachable by walking the editor graph. Equal is healthy; fewer is a stale artifact (measured: 3 vs 22). Bisect before blaming the rig — swap controllers across two rigs. A controller that breaks every rig is the defect; a rig that breaks under every controller is the rig. Here the suspected rig animated correctly under a sibling controller, which cleared it in one step.ImportAssetwithForceUpdatedoes NOT repair it — the source hash is unchanged, so the bad artifact is reused. Force a real rebuild: re-assign each blend tree'schildrento itself,SetDirty,SaveAssets, then reimport. The fix writes NOTHING to the asset file, so there is no diff to commit and no way to spot it in review — say so in the report, and prefer regenerating the controller through its builder when one exists.
EYE-FRAME-RIDES-THE-PELVIS-NOT-THE-HEAD
A first-person eye is usually a socket at a fixed height above the feet, and the weapon and its hand IK are posed in that eye's frame. The animated skeleton does not hold still under it. A walk cycle drops the whole torso, so the gun stays at the authored height while the shoulders leave, and the arms stretch upward to keep hold of it. Measured on one humanoid: pelvis resting at 0.874 m fell to a 0.612-0.747 m band while walking, a 0.19 m mean drop plus a 0.135 m cyclic bob, against an aim target that held its authored height to 0.000 m. The eye also sat 0.118 m above the avatar's own eye bones, so the gun rode at skull-crown height in every pose.
It reads as an IK, grip or arm-length defect because the arms are what visibly break. The measurement that names it is the aim target's vertical RANGE over a walk cycle: exactly zero while the body's is not is the whole finding.
Follow the PELVIS, never the head or neck. A look layer distributes pitch across spine, neck and head, so those bones move with the camera and following them feeds the view back into itself. Measured across a 160 degree pitch sweep on a still body: pelvis 1.1 mm, chest 17 mm, shoulder 180 mm, neck 210 mm, head 290 mm. The pelvis carries the full locomotion bob and none of the look.
Take the offset against a rest height LEARNED WHERE THE BODY IS STILL, per stance. A stance's own authored eye height already carries the difference between standing and crouching; measuring against one global rest double-counts it and drives the eye through the floor (measured: crouch eye 0.950 authored, 0.372 delivered). Gate the whole thing on third person - a first-person eye IS the frame the weapon is authored in, and it must not chase the body it is looking out of.
POST-GRAPH-SKELETON-SURGERY-UNDOES-IN-GRAPH-IK
A pose that is finished by moving bones AFTER the PlayableGraph has evaluated silently breaks every IK the graph just solved. The classic pair is a foot-placement solver that gives the pelvis back some height so an unreachable leg can reach, and a weapon-grip hand IK that runs as the graph's last layer. The hand IK seats both hands on a weapon anchored to the CAMERA; the pelvis move then carries the whole upper body — shoulders, elbows, hands — off a gun that never moved.
The measurement that names it is a dot product, not an eyeball. Both hands are displaced by one
IDENTICAL vector, and that vector is the negative of the post-graph offset:
dot(normalize(handError), normalize(pelvisOffset)) == -1.000, magnitudes equal to four decimals.
Identical on both sides is the whole finding — it rules out grips, arm length, clip content and the
IK solver itself, all of which would differ per hand. Measured on one humanoid: 0.070 / 0.106 /
0.186 m of hand error at 0 / 2.5 / 5 m/s, each equal to that frame's pelvis drop.
It reads as a one-hand problem because the support hand on a fore-end shows a shift that the grip hand hides, so "the left hand is broken" is the usual report. Believe the numbers, not the side.
Two fixes, and the tempting one is wrong. Pre-compensating the in-graph IK target by the offset
needs a PER-BODY channel, and a layer settings object is a shared asset — a per-character value
written there leaks across every body using that weapon. Instead let whatever performs the surgery
own its own consequence: capture each end effector's world pose BEFORE the move, and put it back
after. Re-solve with the limb's OWN CURRENT BEND as the hint (middle.position - upper.position) so
the joint keeps the plane the animation chose; a hint copied from the leg solver inverts elbows,
because knees and elbows fold opposite ways. Two-bone IK rotates only upper and middle, so the end
bone inherits a changed rotation — restore the captured rotation explicitly or the hand twists on
the grip.
Gate the re-seat on a non-zero offset so a still body's arms are never touched, and put the drag distance on the authoring readout: a number that tracks the pelvis offset is the regression test.
LEG-HINT-NEVER-POINTS-BEHIND-THE-TOES
The own-bend hint above (middle.position - upper.position) is safe for a hand re-seated a few
centimetres. It is NOT safe for a leg whose target moves ahead of the knee line. A two-bone solver
projects the hint onto the plane perpendicular to hip->target; raise a heel-strike foot onto a step
with the hips dropped and that projection points BEHIND the leg, so the knee folds backwards for the
frame the foot lands. Measured on a 30 cm stair flight: one backward knee per step-up, worst 0.25 m
behind the hip-ankle line. Build the hint from the animated bend perpendicular to hip->ankle, remove
any part of it that points behind the sole's forward direction, then add a small lean toward the toes
(10% of the thigh length was enough). Same measurement after: 1 frame in 342, 0.03 m. The regression
test is one leg with a target ahead of the knee, asserting the knee ends ahead of the hip->foot line.
A second cause hid behind it: feet planted on the plane the character capsule rides. When stairs are ramp colliders for the capsule, that plane runs through the nosings, lifting the lead foot by up to one riser and tilting it with the slope. Probe each foot's own ground (heel and toe rays through a filter that excludes the ramp, keep the higher hit) and let the capsule keep the ramp.
SOLE-HEADING-COMES-FROM-THE-TOE-BONE
A foot solver that stores each sole's forward in ankle space, measured as the body's forward on the saved prefab pose, inherits that pose. Save a character with a foot splayed and the heading is off by the splay: measured on four humanoid rigs, right soles sat 52-92 degrees and left soles 6-44 degrees off the toe bone. Every reader of the heading then fails quietly. The knee hint leaning toward the toes pulled both knees 2.5-4.7 cm inward at idle and up to 8 cm in a walk, while the ankle spacing (owned by target placement) stayed the clip's, so it read as a narrow animation or an IK tolerance. Measure the heading toward the foot's own toe bone, projected on the body up. Ship a validator that reports each body's error in degrees and a repair that re-measures past a small tolerance (3 degrees), and test a rig whose saved pose splays one foot. Triage: measure the stance with the solver on and off; ankles equal and knees not points at the hint.
BONE-NAMES-MATCH-ON-THE-SUFFIX-NEVER-ON-A-SUBSTRING
Auto-wiring a rig by searching bone names for a keyword produces false positives that look like data. Two measured on one vendor pack of nine quadruped rigs:
Contains("ear")matched everyL ForearmandR Forearm, reporting ear bones on species that have none.- Rigs prefix every bone with the species name, so on the bear rig -
BEAR_ Pelvis,BEAR_ Spine- the same search matched all 38 bones. The count read as a richly rigged head and was noise. Contains("tail")matchedPonytail1, inflating tail chains by one on several species.
Match on the segment after the last separator, case-insensitively, against an exact expected name or
an expected-name-plus-index. Vendor packs built on one skeleton template share a suffix convention -
Pelvis, Spine/SpineN, Neck/NeckN, Head, Tail/TailN, L|R Clavicle -> UpperArm ->
Forearm -> Hand, L|R Thigh -> Calf -> Foot - behind a per-asset prefix, so suffix matching
auto-wires the whole pack while substring matching silently invents chains.
Verify a chain exists before building a feature on it. A feature authored against bones the models do not have ships as a tuning surface that changes nothing.
QUADRUPED-HIND-LEGS-ARE-THREE-SEGMENT
A digitigrade hind leg is Thigh -> Calf -> hock -> Foot, four joints and three segments, and
vendor rigs name the hock explicitly (HorseLink in 3ds Max Biped exports). A two-bone IK solver -
the one a biped character already has - bends such a leg backwards at the hock.
Plantigrade species in the same pack (bear, rabbit measured) carry no hock and DO solve with two bones, so a pack is mixed and the solver must branch per species rather than per pack. Budget foot planting on a quadruped separately from spine, neck and tail work: those are chain slerps driven by values the client already has, while feet need a different solver and correct per-species bindings, where a wrong binding reads as a broken leg rather than as nothing happening.
POST-ANIMATOR-BONE-OFFSET-NEEDS-A-VISIBILITY-GATE
A procedural layer that finishes a pose by multiplying an offset onto bone rotations AFTER the
Animator has evaluated is correct only while the Animator keeps rewriting those bones. The clip
writes absolute local rotations every frame, so bone.rotation = offset * bone.rotation is wiped
and re-applied cleanly - until the Animator stops writing, and then the SAME offset compounds onto
its own output every frame until the body folds in on itself.
AnimatorCullingMode.CullUpdateTransforms - the correct default for a crowd, because it keeps the
state machine running off-screen so a queued trigger still plays - is exactly the mode that stops the
transform writes. Gate the offset on the same question the culling asked: renderer visibility.
Any renderer visible means the Animator posed the skeleton this frame; none visible means it did not,
and the offset must not be applied at all. Animator exposes no "did I write this frame" flag, so
Renderer.isVisible is the test, and it is the same one culling keys off.
The failure is silent and slow: it needs the object to leave the camera, so it never reproduces in the inspector on a selected object, and the body is already deformed by the time anything looks.
A test harness reproduces it by accident. Calling the apply path N times inside one frame to "let the easing settle" is N multiplications with no Animator pass between them - measured 139 degrees of spine rotation from a solver whose authored cap was 14. One call per Animator evaluation is the contract; to measure a settled pose, call once with a large delta time, and save and restore the whole chain's rotations around each probe so successive probes are independent.
A clamp reads as a dead feature. A look-at with a yaw limit returns exactly zero for a target past that limit - deliberately, because a head that strains past its own shoulder is worse than one that lets go. A probe that happens to place the target just outside the limit (measured: 63.4 degrees against a 62 degree limit) gets 0.0 on every side and looks precisely like a layer that was never wired. Log the limit alongside the result, and probe inside it before concluding anything.
-
BLENDTREE-2D-DOES-NOT-NORMALISE-ITS-AXES — a Freeform Cartesian / Freeform Directional tree weighs children by plain euclidean distance in raw parameter units, so two axes with different magnitudes silently collapse into the larger one. The shape that burns: a locomotion tree whose X is a yaw rate in degrees per second (spanning ±45) and whose Y is a ground speed in metres per second (spanning 0–2). Distance is then ~95% X, the speed axis is very nearly ignored, and the fastest gait blends into the slowest turn — the animal plays a walking corner at a gallop. Nothing warns: the tree is valid, every child is reachable, the inspector's diamond preview looks right because the editor draws the two axes at equal screen width, which is precisely the lie. Any two quantities with different units are suspect — degrees vs metres, a normalised −1..1 against a raw speed, a health fraction against a timer in seconds. Put both axes in the same units before placing a child: scale one so its full-scale value sits as far from the origin as the other's does, and feed the tree a SECOND parameter carrying that scaled coordinate rather than rescaling the first — the original stays readable in the Animator window and usable by state transitions, which need a real threshold, not a blend coordinate. Also:
useAutomaticThresholdsis 1D-only and is inert here, so it never rescues the placement. Sanity-check by asking what a child at (fullAxisX, 0) and a child at (0, fullAxisY) are worth at the sample halfway between them — if it is not close to an even split, the axes are not commensurate. (Found adding a turn axis to a quadruped locomotion tree, before it ever ran.) -
MECANIM-ANDS-CONDITIONS-SO-AN-OR-IS-N-TRANSITIONS. Every condition inside one
AnimatorStateTransitionis ANDed; there is no OR operator anywhere in the UI or the API. "Leave Idle when the body is moving or turning" is therefore two transitions out of Idle with one condition each, while the return edge — "settle only when it is neither" — is a single transition carrying all of them, since that direction genuinely is an AND. Getting this backwards yields a state you can enter and never leave, or one that leaves on the first condition alone. Build the truth table before writing the transitions; the asymmetry between the two directions is the part that is easy to miss when the forward edge is written first and the return edge copied from it. -
PRESENTATION-SPRING-STEPS-EXACTLY - a per-frame Euler spring's swing depends on the frame rate. A hit flinch built as
velocity += (-k*angle - c*velocity)*dt; angle += velocity*dt, kicked with a velocity sized to peak at an authored angle, peaked at about half that angle at 40 fps (damping 22 x 0.025 s removed 55% of the kick on the first step), about 70% at 60 fps and near full only at high rates, so a body flinched harder on a faster PC. Step a presentation spring with its exact transition over the frame: for unit mass, stiffness k, damping c, with d = c/2 and f = sqrt(k - d^2),x' = e^(-d t)[x (C + d S) + v S],v' = e^(-d t)[v (C - d S) - k x S]where C = cos(f t), S = sin(f t)/f (cosh/sinh over f when overdamped, 1 and t at critical). Solve the four coefficients once per frame per (k, c) and apply them to every spring sharing them: no allocation, no substep loop. Measured after: the same hit peaked at 10.69 / 10.92 / 10.92 degrees at 30 / 60 / 120 fps (the 30 fps frame lands beside the peak). Prove it by pinningTime.captureFramerate(the frame time the code steps) alongsideApplication.targetFrameRate; a slow Editor renders its own rate regardless. A screenshot taken at the peak stalls that frame, so read spring peaks without captures.
MEASURE-THE-HIPS-TURN-BEFORE-MASKING-THE-HIPS-OFF
An upper-body layer that leaves the hips on locomotion (so the walk and the feet survive) plays only the clip's spine and arm motion RELATIVE to the hips. An attack whose power comes from turning the whole body keeps none of that turn. Measured on a commercial pack's heavy sword combo: the hips heading swept 360 deg from windup to hit. With the hips held, the weapon dipped 0.95 m under the eye and then snapped 0.67 m back up in one 33 ms key. An overhead attack from the same pack (hips within 56 deg) never moved more than 0.29 m per key. A strike window picked as "the fastest stretch of the baked path" chose the snap, not the cut, and the server's hit landed 0.3 s early.
- Before choosing a clip for a hips-masked layer, sample the hips heading (from the thigh line) across it. A span past ~90 deg in the windup means the motion lives in the turn: pick another clip or keep the hips.
- A path baked from a clip with authored phase ranges (windup, hit) takes its strike window from those ranges, never from speed.
EDIT-MODE-GRAPH-POSES-IN-A-PREVIEW-SCENE
Sampling a humanoid clip outside Play through a manual PlayableGraph (DirectorUpdateMode.Manual,
SetTime, Evaluate(0f)) posed nothing on an instance hidden in the open scene: every frame read the
bind pose, with no error. The same graph posed every bone once the instance lived in a preview scene
(EditorSceneManager.NewPreviewScene) with animator.cullingMode = AnimatorCullingMode.AlwaysAnimate.
Measured on 45 commercial-pack clips x 53 bones at 30 Hz.
- Sample in a preview scene with culling off, and close the scene in a
finally. - Before trusting a sample set, check that two frames differ. Identical frames mean the rig was never evaluated, not that the clip is still.
- If a graph evaluated outside Play leaves every bone still (SQUARE-HIPS-CAN-STILL-CROSS-THE-FEET, this
file), check the instance's scene and culling mode before falling back to
clip.SampleAnimation.
MECANIM-STOPPED-HOLD-OFFSET
A "hold the last frame" state (the death clip again at speed 0) showed the clip's FIRST frame: dead
animals stood back up once the death clip ended. AnimatorState.cycleOffset does not apply to a
non-looping clip, so the stopped copy sat at normalized 0. Measured on three quadruped death clips:
bone mean height 0.594 m (standing) in the hold before the fix, 0.057-0.143 m (lying) after.
- Place the stopped copy with
AnimatorStateTransition.offset(0.99) on the transition into it, and enter it from code withAnimator.Play(hash, layer, 0.99f). One constant feeds both. - Hold at 0.99, never 1.0: normalized 1.0 wraps a LOOPING clip back to frame 0, and one of the three death clips was imported looping.
- Assert the entry offset in the controller bake or validator (reject below 0.9), so a rebuild cannot drop it silently.
SetActive(false -> true)restarts the Animator in its default state with default parameters. A view that sends parameters only on change never re-sends them: reset its cache inOnEnableand jump back to the hold. The hold must survive a hide/show in the live check, not only the first entry.- Measure the symptom: log state, normalized time and bone mean height through clip end and a reshow. A state name reading "Hold" proves nothing about the pose.
Latios MecanimV2 has its own hold rule (MECANIM-HOLD, the latios anim page); this one is Unity's built-in Animator.
POSE-SPIKE-IS-LOCATED-AT-RENDER-TIME
A one-frame hand pop on a stance change read 27 cm in bones sampled from EditorApplication.update, which
could be sampling order rather than the drawn pose. Two steps turn a spike into a located link:
- Sample the same row at
RenderPipelineManager.beginCameraRenderingfor the gameplay camera, once per frame, and unsubscribe at the end. When the update-time and render-time rows agree, the pop is on screen. - A hand position in camera space cannot tell a root or capsule snap from an IK miss. Log the root, the eye
socket, the camera and the chest world positions, plus
Animator.IsInTransition(0), on the same drawn frame. Measured: the hand stepped 44 cm while root, camera and chest moved under 5 cm, on the base layer's first crossfade frame, so the step was in the hand target chain (weapon bone or aim target), not in the feet. An observed copy showed the same step one frame later.
Then log each link of the hand chain (IK target, the bone it rides, that bone's own target) in camera space before changing code.
ACTION-FADE-SWINGS-THE-ELBOW-POLE
A support arm snapped back at the end of a reload although both hand tracks ended exactly on the hold grip. The hand stayed on the gun (0 cm in gun space) through the fade; the elbow did not. An aim layer blended its elbow pole targets by the action weight, so the 0.08 s action fade swung the elbow 20-29 cm (rifle) and 4-11 cm (pistol) onto the hold's IK pole in about five frames.
- Measure the elbow (every joint a pole or hint drives), not only the hand, across an action's blend-out.
- List every layer that reads the action weight; the hand track is only one of them.
- Size the blend-out per weapon to that swing (here 0.40 s rifle, 0.25 s pistol) and keep the old value as the default for weapons nobody asked about.
PROP-IS-A-BONE-WITHOUT-A-RENDERER
A loose round floated 35 cm under a rifle in every phase and no renderer or bounds audit found it: the round is a bone of the gun's skinned mesh with no Renderer of its own, and the gun renderer's bounds were unusable (158 x 18 x 259 m). First-person packs also park hand-carried props at rest away from the gun, and an Idle or Fire clip built from a reload's first frame inherits that pose.
- Audit props per clip by bone: scale and position against the gun body through the clip, then capture at the bone.
- A prop the clip never carries into the gun is hidden in every frame (scale 0 for the whole clip); a carried prop shows only inside its in-hand window.
- Lock it with a test that samples the scale curve through every clip copied from the reload's first frame.
LAYER-FLAG-SWITCHES-ITS-INPUT
While aiming, the hands turned and the chest followed about 0.8 s later (slow 30 deg turn, seconds to 90 %: gun
1.88, chest 2.70). It looked like network lag. Cause: the look layer's useTurnOffset flag switches its yaw source
from the mouse input to a turn-offset input, and the driver wrote 0 there, so the spine never yawed and only the
body's turn-in-place deadband moved the chest. With the flag off in memory the chest matched the gun (1.88 s).
- Before blaming the network or a solver, read which input property each layer flag selects and log what the driver writes into it.
- Fix at the writer: feed the owed twist into the selected input, scaled by the state that wants it (aim weight), and keep the flag for the states that want the delay.
- Measure seconds to 90 % per link (camera, gun, chest, root, remote copy) on a scripted turn below and above the deadband.
FULL-BODY-FP-HAND-SITS-AT-ARM-REACH
Every first-person tool carry that showed the fist straightened the arm: elbow 173 deg with the grip 35 cm ahead of the eye, 150 deg at 33 cm. Relaxing the elbow dropped the hand out of the frame. A full-body rig seen from its own eye keeps the shoulder about 19 cm behind and 28 cm under the eye, so at a gameplay FOV of 50 a hand in view sits at the arm's full reach. At that FOV a visible fist also pushed a 0.45 m tool's head off the top (head viewport y 1.02).
- Pick one: move the grip up the haft so the fist and the head both fit (IK-TARGET-IN-VIEW-IS-NOT-THE-HAND-IN-VIEW); keep the fist just under the frame corner with the head in view (grip viewport y -0.34, head 0.65-0.78), which a developer holding a reference with the fist in view rejected; or draw the arms with their own view-model FOV.
- Log the elbow bend and the hand's viewport position with each carry trial; a hand on screen with the elbow over 165 deg is this, not a bad pose.
POSE-FROM-A-REFERENCE-FRAME
Two rounds of first-person carry trials were rejected Each rest was designed from direction vectors and elbow angles with nothing to match, so every trial optimised a proxy. Laying the haft into the screen or turning the edge forward read as "sideways". The first pass built against a reference frame matched it on two tools and was written.
- Before posing, pull an idle frame of the game the developer names. With no browser, a video's storyboard works: the
watch page's
playerStoryboardSpecRendererspec gives sprite sheets (level L3 = 320x180 frames, 3x3 per sheet) thatcurlfetches. Image search and wiki fetches returned paywalls or unrelated hits. - Read three numbers off the frame: the head's and the grip's viewport positions and the blade's direction. Build
the rest from directions, never from hand-typed Euler angles:
rotation = LookRotation(edge projected off the haft, haft),position = grip point - rotation * grip offset. - Judge by captures side by side with the frame, not by a logged point. A tool's configured reach point can sit past its visible head (0.65 m against about 0.4 m), which moved a correct placement.
- Before trialling, confirm the field moves the pose: a clip-driven carry ignores rest fields, and four identical captures are that, not a bad value.
IK-TARGET-IN-VIEW-IS-NOT-THE-HAND-IN-VIEW
A developer rejected a first-person tool rest that kept the fist just under the frame corner, with a reference frame showing the fist bottom right. The trial logs had judged the fist by the grip target, and that is a poor proxy: the wrist is solved toward a target, and a target past arm reach, or an arm another layer still owns, leaves the hand short while the tool stays at the target. Measured: target viewport y 0.04, hand bone -0.35.
- Log the hand bone's viewport (
Animator.GetBoneTransform(HumanBodyBones.RightHand)) beside the target's with every trial, and judge the fist at the knuckle midpoint, not the bone (HAND-BONE-IS-THE-WRIST). - To fit the fist and the tool's head in one frame at a narrow FOV, move the grip offset up the haft (0.12-0.20 m on a 0.45-0.6 m tool) and keep the target inside reach: hand viewport (0.93-0.98, -0.06 to 0.01) with the head top right.
- If the hand bone does not move while the target does, first compute the IK wrist target and its distance from the shoulder: a target past reach looks the same. Only a bone short of an in-reach target means another layer owns the arm; read that layer's weight before switching it off ( corrected a layer blamed at weight 0).
HAND-BONE-IS-THE-WRIST
A handoff blamed an animation layer for a first-person hand that "never reached the grip" and blocked a pose trial on it. Replaying the logged poses offline showed the layer at weight 0: the computed wrist target met the logged hand bone within 0.7-2.6 cm wherever it was inside reach, and missed by 8-10 cm only where it sat past the arm. The humanoid hand bone is the wrist; with a knuckles-up grip it sits 5-7 cm under the visible fist.
- Judge the fist at the midpoint of the index and little proximal bones, never at
HumanBodyBones.RightHand. - Before naming a cause for a hand short of its grip, compute the IK wrist target from the logged pose (grip point plus the palm-frame wrist offset) and log shoulder-to-target against the arm's length (upper + lower bone).
- A layer is the cause only when the bone misses an in-reach target; read its weight, do not infer it.
FIT-REACH-AND-VIEW-TOGETHER
A first-person melee rest and swing were authored 0.08-0.19 m past arm reach, so the elbow locked straight and the developer called the arms stiff. Pulling the pivot into reach with the tilt kept put the fist below the frame and the head above it (viewport y -0.05 / 1.01): at 0.3 m from the eye a 50 deg vertical view spans 0.28 m, the same as the fist-to-head length of a hand tool.
- Keep every key's wrist target within about 0.50 m of the shoulder (arm about 0.52 m), including the interpolated path between keys, and check the whole swing, not only the rest.
- Fit the pivot together with a lean of the haft into depth (20-37 deg measured) so the head recedes into the frame; sliding the pivot alone cannot hold both ends in view.
- Fit with a constrained optimizer (SLSQP with the reach and in-front limits as inequalities, multi-started over the lean). Nelder-Mead on a quadratic reach penalty stalled on the penalty wall and returned the start pose.
- If the melee reach is derived from the posed strike, an in-reach strike shortens it (0.2-0.35 m measured): decide the reach explicitly before writing the keys, since it changes a server outcome.
ACTION-FADE-RUNS-ON-PRESENTATION-TIME
A server-timed reload's arm pose jumped in at its start: action weight 0 to .36-1.0 in one frame, support elbow 7-29 cm, on all 24 captured reload starts. The vendor mixer computes its fade-in as (clip time - start time) / blend-in on the clip's own clock, and the driver slaved that clock to the server's reload progress, which held for 2-3 frames and then jumped 0.13-0.2 s. The fade was over before it began.
- A clip whose time follows a network or simulation clock gets its weight envelope on presentation time: count elapsed frame seconds from the play call and write the weight onto the newest mixer input playing that clip.
- Give the entry the same seconds as the exit, and log the action weight per frame at the action's first 5 frames.
WEIGHT-GATED-DELTA-IS-ZERO-AT-THE-GATE
Every gun stepped 0.6-2.5 cm on the frame its reload ended, after a smooth blend-out. The aim layer applied the action's weapon delta whole while the action weight was relevant and dropped it at the relevance threshold, and that delta also held the hold pose's drift since it was captured.
- Anything switched by a weight-relevance gate must already be near zero at the gate: scale it by the weight
(
Lerp(identity, delta, weight)), never add it whole behind the gate. - Check by logging the driven bone's per-frame step across the frame the weight crosses the threshold.
FP-CLIP-HAND-CUTS-OFF-SCREEN
On a full-body first-person rig the support hand teleported 51 cm in one frame during a reload, for the developer and for observers. The vendor's first-person clip takes that hand out of the camera frame for a speedloader and cuts it back in two samples; an arms-only view model never shows the cut, a full body does. The loose prop teleported 120 cm too.
- When baking a vendor first-person clip onto a full body, scan every hand track for steps over 8 cm per 60 Hz sample and bridge each with an eased reach of about 0.25 s ending at the cut's far side.
- Keep the loose prop hidden until the hand arrives, and lock the bake with a test on the maximum per-sample step.
SIMULATE-SEEKS-PARTICLES-NOT-SCRIPTS
ParticleSystem.Simulate(t)advances particles only. A light, material or transform animated by a script on the effect (an effect pack's light-animation component) stays at its t = 0 value, so a seeked still shows the burst without its light. Seek the script's own clock too (itsAnimate(t), by reflection if it is not public), and read PARTICLE-SIMULATE-PAUSES-THE-CLOCK above before timing anything after aSimulate.- The same scripts explain lights saved at intensity 0 that look dead: the script ramps them after every
OnEnable, and a pooled view that togglesSetActivere-runs it on each play. Judge a light by what drives it (a retained script field that references it, or an Animator), never by its serialized value; see DRIVEN-VALUE-IS-NOT-THE-SOURCE-OF-TRUTH in the scene flow page. A converter that strips "dead" lights tests for a driver first. - Stills without a live event: instantiate the view as a
DontSavescratch object in Edit mode,Play,Simulate(t)the root systems, seek the scripts, and render a scratch camera. Author in Edit mode: an editor authoring tool that refuses Play mode can still save a source edit before its check throws.
SHAKE-IS-VERIFIED-BY-POSE-LOG
- Stills cannot show camera motion. Verify a shake or kick numerically: add a listener after the camera owner's (for
Cinemachine,
CinemachineCore.CameraUpdatedEvent), log camera pose andTime.timeScaleevery frame, and remove it before stopping Play. Pass: the authored amplitude and decay, then a return to the exact base pose, which proves nothing accumulates.
CONTACT-LANDING-RESPECTS-ARM-REACH
A first-person chop landed onto the aimed surface by turning the posed strike about the eye and sliding it along the look ray up to a landing shift. That moved the IK pivot with no regard for the shoulder: on a surface 0.8-1.2 m away the wrist target sat 0.78-1.19 m from the shoulder (arm about 0.52 m), and live the elbow stayed locked at 173 deg through the whole swing, for old and newly fitted keys alike.
- Bound any contact landing by the arm's reach. A surface past the landed head takes the hit at the natural impact with no slide out; only a nearer surface pulls the strike back.
- Never raise the landing shift to buy reach; a longer slide stretches the arm further.
- Gate: log shoulder-to-wrist at the contact frame beside the arm length, not only the head's reach.
FIT-THE-EDGE-NOT-ONLY-THE-VIEW
The developer reported every held melee tool "rotated the wrong way completely": at a wall the haft stood upright and the top of the head met it, not the edge. The key fit had scored only fist/head viewport and wrist reach, so the facing was free, and the optimizer pointed the strike's haft straight ahead with the edge down (a chop into a log on the floor). The landing then moved only the head's position, never its edge.
- Constrain the strike's edge direction (forward-down) and the haft's diagonal in the fit, beside reach and view.
- Build the windup as the strike turned back about the haft x edge axis (55-120 deg measured) so the edge leads the head's path at impact (edge . path 0.90-0.99).
- At landing, roll the strike about its own haft (reach-neutral) to put the edge into the aimed surface.
- Gate: capture the edge at contact on a wall and on a floor target; reach and viewport numbers cannot see facing.
PICK-THE-RIG-NEAREST-THE-CAMERA
A first-person probe logged a rest wrist of 0.51 m and a fist at viewport (4.7, -5.5) for two different pose variants. It had picked the Animator by name while two character rigs were live, so every number came from the other body.
- In a live multi-rig session select the rig whose hand bone is nearest the camera and log how many rigs matched.
- A viewport outside 0..1 for a fist the capture shows on screen means the probe read the wrong rig.
BIPED-QUADRUPED-HIPS-ARE-THE-THIGHS-PARENT
- A quadruped on a Biped-named rig hangs its hind legs off the first spine bone (
Spine), not offPelvis/Hips. Bending that bone as a trunk joint yaws the whole skeleton: measured 13 degrees of body heading ahead of travel in every corner, read by the developer as "spins in place". - Resolve the hips as the common parent of the hind thighs at bake time and store it as its own reference. Give it the rear counter-rotation (the hind body trails the path tangent) and bend only the joints ahead of it. Never pick the hips by bone name.
SPINE-BENDS-BY-PATH-CURVATURE
- A travelling body's spine bend authored as a share of a reference turn rate over-bends fast wide corners and under-bends slow tight ones, because the same yaw rate is a different path at each speed.
- A body lying on its path bends by curvature (yaw rate / speed, 1/m) times its length: give each joint curvature x its span measured once from the bind pose, capped per species. Keep the effort-share bend only for turning on the spot, where speed is zero and curvature is undefined. Let the neck lead into the corner.
APPROVED-POSE-BOUNDS-A-REFIT
A first-person gun hold the developer had called good was replaced by a grid fit scored only against a reference game's screen targets (muzzle and hands in viewport boxes). The pick passed every measured box and sat 5.6 cm lower; the developer's verdict was that the gun was now too low, and the old hold was right.
- A pose the developer approved is a constraint, not a starting point. Record its values (and a capture) before a fit, and add its distance to the fit's score so a pick far from it has to earn the move.
- Show the developer a before/after capture before baking over an approved pose; a passing measurement is not approval.
- When the developer rejects the new pose, restore the approved values from version control (
git show <commit>^:<asset>) rather than authoring a third pose.
MEASUREMENT-NEVER-GATES-ITS-OWN-REFRESH
A first-person weapon stayed in its wall-pulled pose after the wall was gone. Its muzzle-to-wall reach was re-measured only while the weapon was not pulled, and the pull came from that reach, so the first blocked reading froze the pull.
- Never skip a measurement on the state it drives: an effect that switches off its own input latches the first bad reading for as long as it stays on.
- Measure every frame from an input the effect does not move (the pre-effect pose, the unpulled frame), and cover the release with a test that steps blocked -> clear -> blocked.
ANIMATOR-REWRITES-A-BOUND-TRANSFORM
A weapon's wall-clearance probe never fired for one gun: its aim target, written by a script every frame, read back a constant pose (yaw 217.7 degrees) after the Animator evaluated, so the probe cast 0.9 m behind the eye. Another gun passed only because its stale pose happened to face forward.
- A transform bound to the Animator stream (
BindStreamTransform, which rig frameworks call for every rig element) is written back from the stream on each evaluation. A script value written before evaluation is gone afterwards. - Jobs that read the transform before evaluation are fine. Anything that reads it later keeps the written value in a field and reads that. To find it, log the transform before and after evaluation: a post-evaluation value that holds constant while its input moves is the write-back.
RIG-LOOKUP-MISS-RETURNS-BONE-ZERO
Three generated character bodies passed every load and play step while 448 of their weapon-layer bone references were wrong: blank pelvis, layers still built against the source rig, indices from another skeleton.
- A by-name bone lookup that returns a default element on a miss (a rig asset's
GetElementByName) hands back index 0, the root, and anOnValidaterebind writes that index into the asset. Nothing throws. - A rig validator compares the returned element's NAME with the name asked for, then the index, and fails on a
blank or
Nonebone. Run it in tests and in the authoring tool.