Skip to content

feat: 0.6.0 balance + behaviors — step 1: feet load-bearing - #101

Open
blugart-dev wants to merge 11 commits into
developfrom
feat/balance-behaviors
Open

blugart-dev wants to merge 11 commits into
developfrom
feat/balance-behaviors

Conversation

@blugart-dev

Copy link
Copy Markdown
Owner

Summary

Milestone 0.6.0 — Balance + behaviors (docs/PLAN.md), into develop (main is frozen until the maintainer's feel gate). This PR carries the milestone; step 1, feet load-bearing, is complete and measured. Later steps (BalanceState, behaviors, the deletions) land as further commits here.

Step 1 — the legs carry the body

Before: the root anchor's position motor held the pelvis with up to 2500 N, the feet were masked out of collision while foot IK solved, and when allowed to collide they carried ~2 % of the 804 N body (tools/bench/foot_probe.gd).

  • Sole-aligned foot collider (BoneDefinition.sole_aligned): a level box with its bottom face on the foot IK sole and a heel behind the ankle. The old bone-aligned box followed the Mixamo foot bone 27° down; its corner sat 7 cm under the floor, a colliding foot was pushed 15° off its pose — the reason the feet had to be masked.
  • Feet collide in every state (foot_ik_disable_foot_collision default false) and report contacts.
  • muscle_root_support (default 0): the anchor has no vertical authority; a second, world-aligned anchor carries the position motor so the up-axis limit is truly vertical.
  • Get-up on load-bearing feet: full anchor support + frictionless feet during the canned blend, faded back over 0.75 s. Shooting range: ≤ 8° at recovery_finished, 1–4° after 3 s, pelvis at target height on its own legs.

Numbers (ybot, tools/bench/ybot_bench.gd)

Mode, Hz IDLE mean/max HIT peak/recover SETTLE after react SAG FEET resolver ms
legacy 60 0.81 / 1.56 1.2° / 4 0.96 (balance 0.09) +0.5 mm 100 % 0.16
JOINT_MOTOR 60 1.10 / 3.51 1.9° / 4 9.9 (balance 0.54) +6.2 mm 100 % 0.22
JOINT_MOTOR 120 0.73 / 1.87 2.0° / 4 1.63 +0.8 mm 100 % 0.21
JOINT_MOTOR 30 5.45 / 23.0 6.6° / 4 20.9 +9.3 mm 100 % 0.22

Exposed, and the next step's job: after the violent react clip the loaded feet stay where friction planted them while the animation returns to idle (SETTLE 9.9° vs legacy 0.96°). A load-bearing foot cannot be dragged into place — it must be unloaded and stepped. That is BalanceState + StepBehavior. 30 Hz regressed 4.85 → 5.45 and stays open.

Suite: 253 tests green (9 new in test_feet_load_bearing.gd); all 8 demos load headless clean.

Human gate (visual, when the milestone is complete)

  • idle stands on its feet, no float, no sag visible
  • knock a character down (Explosion) — gets up upright and stands on its legs
  • steps read as steps; no moon-walking feet (after the step behavior lands)

🤖 Generated with Claude Code

https://claude.ai/code/session_018RbYJ9LscLhHypHqWfhtGF

blugart-dev and others added 5 commits September 13, 2026 17:07
…te; 0.6.0 removal list from the feature inventory

- main is release-only; milestones PR into develop and fast-forward it.
- 0.5.0 marked code complete on develop, feel gate deferred by the maintainer.
- 0.6.0: removal list (micro reactions, motor-mode velocity clamps, legacy
  root spring, dice knockdowns out of the physics path), tip-over owned by
  BalanceState (nothing physical decides a fall in motor mode today).
- 0.7.0: retire VELOCITY_OVERWRITE after 0.6.0's A/B.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_018RbYJ9LscLhHypHqWfhtGF
…g feet, anchor without vertical authority

The legs carry the body. Before: the root anchor held the pelvis with up to
2500 N, the feet were masked out of collision while foot IK solved, and when
allowed to collide they carried ~2 % of the 804 N body (tools/bench/foot_probe.gd).

- BoneDefinition.sole_aligned (feet): the foot box is built level with the
  character, bottom face at foot_ik_ankle_height below the ankle (the IK sole),
  heel added (SkeletonDetector.FOOT_HEEL_RATIO 0.3, FOOT_SOLE_OFFSET). The old
  bone-aligned box followed the Mixamo foot bone 27 deg down: its corner sat
  7 cm under the floor, a colliding foot was pushed 15 deg off its pose and the
  character up — why the feet had to be masked. build_body takes the up axis;
  sole_shape_transform is the geometry; both profile factories set it.
- foot_ik_disable_foot_collision defaults to false (feet collide in every
  state); foot bodies report contacts (balance layer input).
- RagdollTuning.muscle_root_support (default 0): share of muscle_root_force
  the position motor may spend along world UP. Implemented with a second,
  world-aligned anchor (<Root>_anchor_lin) — per-axis limits in the tilted
  pelvis frame still lifted ~500 N through the "sideways" axes.
- Get-up on load-bearing feet: the controller gives the anchor full support
  and makes the feet frictionless for the canned blend (a sole box pinned by
  friction ended the blend 0.5 m from target, legs 40-50 deg off), then fades
  the support back over 0.75 s. Shooting range: <= 8 deg at recovery_finished,
  1-4 deg after 3 s, pelvis at target height on its own legs.
- SpringResolver.get_bone_target_global(); set_root_support_override().
- Bench: SAG / FEET columns, HIT measured from the clean idle, SETTLE after
  the react clip; variants nofootcol / support025 / support05 / support1.
  60 Hz motor: idle 1.10/3.51, hit 1.9 deg / 4 ticks, sag 6 mm, CPU 0.22 ms vs
  legacy 0.16; 120 Hz idle 0.73/1.87 beats legacy; 30 Hz 5.45 (open).
  Exposed: SETTLE after react_front 9.9 deg at 60 Hz (legacy 0.96) — loaded
  feet stay where friction planted them; the step behavior must move them.
- Harness skeleton: hips 0.935 m so the soles rest on y = 0.
- Tests: test_feet_load_bearing.gd (9); muscle tests pin the anchor at full
  support (they characterise the muscles on a groundless rig); 253 green, all
  8 demos load headless clean.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_018RbYJ9LscLhHypHqWfhtGF
…CI saw the rig before its first solve)

On the CI runner setup_complete fires before the resolver's first physics tick,
so test_rig_builds_all_joints counted no anchor joints. The anchors and motor
flags are now applied at the end of _init_bones when the tuning is JOINT_MOTOR;
the lazy path in _tick remains for runtime mode switches. The test also waits
two physics frames.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_018RbYJ9LscLhHypHqWfhtGF
…bs; limits left alone on evidence

- RagdollTuning.self_collision defaults to true. Jointed pairs are excluded by
  their joint; any non-adjacent pair already overlapping in the build pose is
  excluded for good by a safety net one physics tick after the build
  (PhysicsRigBuilder.get_self_collision_exclusions); everything else collides.
  find_overlapping_pairs() is the space query behind the probe and the tests.
  Off (the pre-0.6.0 behaviour) still excludes every pair.
- Why it was off: the velocity-overwrite resolver's commands were rewritten by
  every contact impulse. Under bounded joint motors the ybot has no
  non-adjacent overlaps in idle / react / a 4 s ragdoll and the bench is
  bit-identical on and off (tools/bench/overlap_probe.gd); the shooting-range
  get-up still lands within 2-5 deg.
- Joint limits NOT tightened: tools/bench/limit_envelope.gd samples all 21
  ybot clips against the authored limits and finds 17 axes already exceeded
  (elbow lateral +-58 vs +-20, spine X -75 vs +-35, knee 149 vs 140, shoulder
  twist 158 vs +-90). The folding limbs were a self-collision problem.
- shape_local_transform() extracted from build_body (sole box or bone-aligned).
- Tests: test_self_collision.gd (6): defaults, jointed-pair exclusion, the
  safety net on an oversized Hips box, a limp hand shoved at the chest blocked
  with self-collision on / passing inside with it off. Fidelity tests pin the
  0.4.1 numbers with self_collision = false explicitly. 259 green; all 8 demos
  load headless clean.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_018RbYJ9LscLhHypHqWfhtGF
@blugart-dev

Copy link
Copy Markdown
Owner Author

Step 2 — self-collision on (maintainer asked for the ragdoll look before the balance state). self_collision defaults to true: jointed pairs excluded by the joint, build-pose overlaps excluded by a safety net, everything else collides — a limp arm is blocked by the chest (test_self_collision.gd). Measured: no non-adjacent overlaps on the ybot idle / react / ragdoll, bench bit-identical on/off, get-up 2–5°. Limits not tightened: tools/bench/limit_envelope.gd shows the 21 clips already exceed 17 authored axes (elbow lateral ±58 vs ±20, spine X −75 vs ±35, knee 149 vs 140); tightening would fight the get-up / injured clips. 259 tests green, demos load clean.

…n, ratio, loaded foot

One balance measurement per physics tick, refreshed in every state and read by
every decision (ActiveRagdollController.get_balance(); the 0.4.x dictionary API
keeps working with the new fields added).

- addons/kickback/balance_state.gd: mass-weighted CoM and velocity; support
  polygon = convex hull (monotone chain) of the sole footprints of the feet in
  contact (foot contact reports; no contact -> no support); XCoM = CoM + v/w0,
  w0 = sqrt(g / CoM height above the support plane); signed margin to the
  polygon edge; ratio = XCoM offset from the centroid over the polygon radius in
  that direction (0 centre, 1 edge, > 1 outside); per-foot load share and the
  loaded foot. reset_velocity() after the recovery re-base.
- The static CoM-vs-ankle-midpoint ratio is gone: it read 0.5-0.6 for a
  perfectly standing character on load-bearing feet and staggered it on a hand
  shot. Ybot idle now: margin +12.5 cm, ratio 0.40-0.42 over 2 s of sway ->
  thresholds recalibrated: balance_stagger_threshold 0.8, recovery 0.6,
  ragdoll 1.0 (capture point outside the feet), ranges to 1.5.
- F3 HUD draws the contact hull on the support plane, the CoM diamond and the
  XCoM ring with its margin in cm; foot_probe prints the balance components.
- Tests: test_balance_state.gd (7): hull, signed margin, edge distance, XCoM =
  CoM + v/w0 on a synthetic rig, the standing harness, airborne = no support,
  a 1.5 m/s shove puts the XCoM outside. 266 green; all 8 demos load clean.
- Not yet: the tip-over decision on the XCoM and the anchor release (needs the
  step behavior first: a 150 N.s shove must step, not fall).

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_018RbYJ9LscLhHypHqWfhtGF
@blugart-dev

Copy link
Copy Markdown
Owner Author

Step 3 — BalanceState. One balance measurement per tick: CoM, CoM velocity, XCoM (CoM + v/√(g/h)), support polygon = convex hull of the sole footprints of the feet in contact, signed margin to the edge, ratio (0 centre / 1 edge / >1 outside), loaded foot. Replaces the static CoM-vs-ankle ratio that read 0.5–0.6 while standing; ybot idle now reads 0.40–0.42 with a 12.5 cm margin, thresholds recalibrated (stagger 0.8, recovery 0.6, ragdoll 1.0). HUD draws the hull + XCoM. 266 tests green. Tip-over on the XCoM waits for the step behavior.

blugart-dev and others added 3 commits September 13, 2026 20:00
…ead of asserting penetration (CI landed the arm where the limits held it out)

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_018RbYJ9LscLhHypHqWfhtGF
…g, limp arm shoved across the torso)

A ragdoll landing is not a fair A/B: self-collision changes the fall itself, so
the two runs landed the arm in different places (CI: the limits held it out).
The rig now stands (anchor support 1) with the left arm limp; the hand is
shoved across the body and is blocked by the torso boxes with self-collision
on, swings through them with it off.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_018RbYJ9LscLhHypHqWfhtGF
…t teleport) and the arm windmill are gone

- KickbackBehavior / BehaviorContext (addons/kickback/behaviors/): a behavior
  reads the shared BalanceState each tick and answers with stiffness floors
  and pose targets; the controller runs a fixed ordered list in NORMAL /
  STAGGER (get_behaviors()); the 0.7.0 arbiter replaces the order.
- StepBehavior: balance steps (XCoM at the edge of the feet for 4 ticks -> the
  fall-side foot swings to the capture point) and re-plants (calm + a loaded
  foot > 10 cm from its animation spot -> the less loaded foot is lifted and
  stepped there), through FootIKSolver.begin_step (lifted arc, then a hover at
  the goal until the physical foot arrives) on top of foot LOCKS (a contacting
  foot that drifted > 5 cm from its spot is held where it stands so the leg
  does not fight friction). Motor mode only; legacy numbers unchanged.
  Signal step_started(foot_rig, target).
- Removed: _update_directed_stumble (the root teleport), _do_directed_step,
  _apply_stumble_brace, the windmill, the stumble_* / arm_windmill_* /
  arm_brace_weight knobs, stumble_step_started, test_stumble_step.gd.
  grep "global_position +=" addons/ is empty. Tuning Lab: STEPS sliders.
- RagdollTuning "Balance: Steps" (8 knobs, replacing the 8 stumble knobs) and
  muscle_root_hold (sideways share of the anchor force, default 1).
- Measured (ybot 60 Hz): root moved 0.25 m with planted feet -> both feet
  re-planted by 0.83 s, legs 1.5 deg; after react_front 5 steps, legs 3 deg
  by 2.8 s (bench SETTLE 10.4 -> 6.2 in the 2nd second, was 9.9 flat);
  150 N.s shove with the hold: 2-4 steps, pelvis moves 1-2 cm. With the hold
  released and no upright behavior the idle is unstable (7.2 deg, constant
  steps) - the UprightBehavior is next and takes muscle_root_hold to 0.
- Harness: ankles 5 cm behind the leg line (CoM mid-foot, was on the heels),
  straight_legs option for the knee-fallback tests. 269 tests green; all 8
  demos load headless clean. SELF_PRESERVATION.md rewritten as the spec.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_018RbYJ9LscLhHypHqWfhtGF
@blugart-dev

Copy link
Copy Markdown
Owner Author

Step 4 — behavior layer + StepBehavior; the directed stumble is gone. KickbackBehavior reads BalanceState and answers with stiffness floors + pose targets; the controller runs a fixed list in NORMAL/STAGGER. StepBehavior: balance steps to the capture point and re-plants of a loaded foot the animation cannot drag, via foot locks + FootIKSolver.begin_step (hover-until-arrived landing). The root teleport, stumble brace and windmill are deleted (grep 'global_position +=' addons/ is empty). Measured: root moved 0.25 m → both feet re-planted by 0.83 s; after react_front legs within 3° by 2.8 s (SETTLE 10.4 → 6.2 in the 2nd second). muscle_root_hold stays 1: with it released and no upright behavior the idle is unstable (7.2°, constant steps) — UprightBehavior is next. 269 tests green.

blugart-dev and others added 2 commits September 13, 2026 20:49
…h under addons/ is GUT's input factory)

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_018RbYJ9LscLhHypHqWfhtGF
…hysics decides the fall

- muscle_root_hold defaults to 0.25 (~625 N sideways): the anchor assists
  balance instead of holding the character up. Ybot 60 Hz: idle 0.98/4.4 deg
  (same as the full hold), settle after react_front 10.4 -> 6.1 deg (2nd
  second 5.0), a 150 N.s shove is caught by the legs + steps with the pelvis
  moving ~6 cm. Hold 0 not reached: the passive stance reads 3.4-4.4 deg and
  is chaotic under the shove in every lever combination measured.
- Ankle torque 60 -> 150 N.m (MUSCLE_TORQUE_TABLE Foot): the ankles sat at
  ~60 % of their limit just standing with the CoM 9 cm off centre.
- muscle_leg_gain (new, 0.2): the leg chains + pelvis run a faster motor loop
  than the arms (SpringResolver._leg_set from the profile roles).
- step_trigger_ratio 0.9 -> 1.0; BalanceState.com_velocity exponentially
  smoothed (3-tick constant) so the XCoM does not jitter.
- Physical fall detector (ActiveRagdollController._check_fall, motor mode,
  NORMAL / STAGGER): pelvis below 60 % of its target height or tilted past
  0.55 for 0.15 s -> RAGDOLL, no dice. A 400 N.s shove with the assist
  released is detected within 2 s; no false fall while standing or getting up.
- UprightBehavior (experimental, off by default): whole-body target shift
  against the XCoM error via FootIKSolver.set_body_shift. Measured worse than
  the passive stance at every gain; kept as the starting point for a
  torque-level ankle strategy.
- Bench variants hold0/015/025/05/1, nosteps, noupright, gainup/down,
  leggain2/3, ankle120/150/200. Tests: test_balance_control.gd (6); balance /
  step tests adjusted for the smoothed velocity and the assist. 275 green;
  all 8 demos load headless clean; shooting-range get-up 2-4 deg (one 24).

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_018RbYJ9LscLhHypHqWfhtGF
@blugart-dev

Copy link
Copy Markdown
Owner Author

Step 5 — balance control. muscle_root_hold 0.25: the root anchor is a bounded assist (≈625 N) instead of the thing holding the character up. Ankle torque 150 N·m, muscle_leg_gain 0.2, smoothed CoM velocity, step trigger 1.0, and a physical fall detector (pelvis low / tilted for 0.15 s → RAGDOLL, no dice — a 400 N·s shove with the assist released is detected within 2 s). Measured: idle 0.98°, settle after react 10.4 → 6.1°, 150 N·s shove caught by legs + steps. Hold 0 not reached: the passive stance is 3.4–4.4° and chaotic under the shove; the IK-shift UprightBehavior measured worse and ships off. 275 tests green.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant