Repository navigation
feat: 0.5.0 muscle layer — JOINT_MOTOR mode (torque-bounded Jolt velocity motors) - #100
Open
blugart-dev wants to merge 5 commits into
Open
blugart-dev wants to merge 5 commits into
blugart-dev wants to merge 5 commits into
Conversation
…city motors) RagdollTuning.muscle_mode = JOINT_MOTOR executes the resolver's command (joint-space rotation error x muscle_gain per tick + feed-forward) through each Generic6DOFJoint3D angular motor with force limit = BoneDefinition. muscle_torque x strength ratio x muscle_strength_scale. Strength is a torque, gravity stays on, a limp bone has a zero force limit, hits react physically. The pelvis is attached to the world by a limit-free joint whose motor drives its orientation (muscle_root_torque); its position pin stays, scaled. Legacy VELOCITY_OVERWRITE is untouched and remains the default until the visual gate passes. Engine facts measured (GODOT_CONSTRAINTS.md): motor target axes mirrored; the 6DOF angular motor is solved on swing-twist axes — twist in the parent frame's X, swing in the child frame's Y/Z (single-frame commands leave every joint of a real idle 2-4 deg short). Ybot bench (tools/bench/ybot_bench.gd, 60 Hz): idle 0.80/3.42 deg vs legacy 0.78/1.65 under real gravity; react_front 16.3 vs 10.1 (torque-limited); bullet on the hand 6.3 deg peak, muscle recovery in 4 ticks (legacy 1.3). 120 Hz beats legacy. 30 Hz documented as open (non-colliding feet ring). Also: BoneDefinition.muscle_torque + SkeletonDetector.MUSCLE_TORQUE_TABLE; KickbackCharacter resolves null profile/tuning once (a null passed through configure() used to strip the controller's roles and kill foot/arm IK); test_muscle_layer.gd (16 tests); docs (REFERENCE muscle section, INTEGRATION, CHANGELOG, PLAN checklist + measured acceptance, MUSCLE_SPIKE addendum). Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_013gMUV3TEWBfATLUiYpmX71
…e; root holds until limp - RagdollTuning.muscle_mode defaults to JOINT_MOTOR; VELOCITY_OVERWRITE kept for comparison (the legacy-characterising suites pin it explicitly). - muscle_strength_curve (0.5): torque = muscle_torque x ratio^curve. The hit / stagger / fatigue reductions were tuned as blend fractions; as raw torque the 10 % stagger floor collapsed a staggered character into a ragdoll in 0.3 s. - The root's world motor and pin hold at full authority until the bone is limp (_root_hold_factor) — they stand in for balance until 0.6.0; scaled by hit strength they let a stagger topple. - Tests: stagger stays standing and recovers in motor mode; a motor pushing into a limit yields a bounded amount; joint count tolerates the root world joint; 241 tests green, all demos load. - Docs: REFERENCE / INTEGRATION / CHANGELOG / PLAN / README / CLAUDE.md for the flipped default. plugin.cfg bump + tag wait for the visual gate. Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_013gMUV3TEWBfATLUiYpmX71
…e), tip-over off in motor mode; trace recorder + scene probe tooling Found by the user in shooting_range.tscn: the whole body wobbled with no input and the feet clipped the floor. Measured: the pelvis stood 3.5 cm below its target and bounced at ~3 Hz (13.5 mm p-p). Cause: the root position pin was a velocity written to the pelvis alone, diluted by the joint solve across the ~55 kg hanging from it. Broadcasting the correction to every body was wrong too — it cancels gravity for all of them and discards impulses applied between ticks. - SpringResolver: the root world joint now carries a LINEAR motor (muscle_root_force, 2500 N) holding the pelvis with a bounded force inside the solver: within 1 mm of target, no bounce, feet at legacy height. Root torque/force are not scaled by muscle_strength_scale (balance, not muscle). - ActiveRagdollController: the balance-driven tip-over does not fire in JOINT_MOTOR mode — a held pelvis cannot topple; CoM-vs-feet spikes of 1.0-1.5 were ragdolling a standing staggered character. - KickbackTraceRecorder (F5 in the demos) + tools/bench/trace_report.py: per-tick trace of every character to user://kickback_traces/*.jsonl and an offline summary (sag, bounce frequency, error spikes, ground clipping, root-motor saturation, frame pacing). tools/bench/scene_probe.gd loads any demo scene headless and logs the characters with no input. - Tests: pelvis stands within 6 mm of target without bouncing; trace recorder end to end. 243 tests green, all demos load. - Docs: REFERENCE / INTEGRATION / CHANGELOG / PLAN / README / CLAUDE.md. Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_013gMUV3TEWBfATLUiYpmX71
… angle space (get-up no longer upside down) User report: characters got up UPSIDE DOWN after a ragdoll on the ground, and wobbled until they settled there. Measured: with a fixed world joint the pelvis lies 90 deg+ from the joint frame, Jolt's swing-twist motor axes degenerate and the orientation motor drives to the wrong stable point. Re-anchoring by rebuilding that constraint (reassigning node_b) left every other joint's motor inert (correct commands, full torque, zero motion). - SpringResolver: the root is joined to a static ANCHOR body teleported every tick to the (foot-IK-shifted) target, clamped to lead the pelvis by <= 1 rad / 0.5 m and placed on the pelvis while limp, so the joint's relative rotation stays small and nothing is ever rebuilt. Angular (muscle_root_torque) and linear (muscle_root_force) motors drive it as bounded forces; the linear command is expressed in the anchor frame. - Limb (and root) motors are commanded per axis in Jolt's swing-twist angle space — the space the limits are defined and verified in — instead of a rotation vector, which only agreed near rest and stalled joints thrown to their limits. - Balance tip-over off in JOINT_MOTOR mode (held pelvis cannot topple). - Tuning Lab: MUSCLES slider section (gain, strength scale, curve, root torque/force). - Verified: shooting-range ragdoll -> get-up upright on all 5 targets, 2 deg worst error; idle 0.94 deg; 245 tests green, all demos load. - Recorded as MISSES in docs/PLAN.md: react_front 24.5 deg (2.4x legacy) and hand-hit deflection 2.4 deg (1.8x) — robustness won over vividness for now. Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_013gMUV3TEWBfATLUiYpmX71
Probe for the zero-force-limit latch that was committed with the anchor-root fix; the finding it chased is covered by test_muscle_layer.gd. Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_018RbYJ9LscLhHypHqWfhtGF
Owner
Author
|
Maintainer decision 2026-09-13: |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Summary
Milestone 0.5.0 — Muscle layer (
docs/PLAN.md):RagdollTuning.muscle_mode = JOINT_MOTORexecutes the resolver's command through each joint's Jolt angular velocity motor withforce limit = BoneDefinition.muscle_torque × strength ratio × muscle_strength_scale. Strength is a torque, gravity stays on, a limp bone has a zero force limit, and a hit produces a real, bounded reaction.JOINT_MOTORis now the default (per the maintainer's request); the legacyVELOCITY_OVERWRITEpath is untouched and available on the tuning.Suite: 245 tests, all passing (21 new in
test_muscle_layer.gd, plus the trace recorder); all 8 demos load headless clean; legacy regression suites (test_rig_fidelity,test_runtime_rig,test_state_machine) unchanged and green.What was built
SpringResolvermotor path: per joint, the relative-rotation error in the joint frame ×muscle_gainper tick + feed-forward, clamped, written as the motor target; force limit from the profile torque × strength ratio. Parent-first so a child reads its parent's target from this tick. Foot/arm IK overrides work unchanged.Generic6DOFJoint3Dto a static body (Hips_anchor) the resolver teleports to its target every tick; the joint's angular motor (muscle_root_torque, 400 N·m) and linear motor (muscle_root_force, 2500 N) drive it as bounded forces. The anchor leads the pelvis by ≤ 1 rad / 0.5 m and sits on it while limp, so Jolt's swing-twist motor axes never degenerate. Three earlier root designs were measured and rejected on the maintainer's scene: a velocity pin on the pelvis (diluted by the joint solve: 3.5 cm low, 3 Hz bounce, the whole-body wobble), a fixed world joint (got up upside down after a ragdoll on the ground), and re-anchoring that joint by rebuilding it (left every limb motor inert). Verified: after a ragdoll every shooting-range character gets up upright with ~2° error.ragdoll_probability/ pain / explicit triggers until 0.6.0's balance layer owns the decision.KickbackTraceRecorder(F5 in the demos) writes a per-tick trace of every character touser://kickback_traces/*.jsonl;tools/bench/trace_report.pysummarises sag, bounce frequency, error spikes, ground clipping, root-motor saturation and frame pacing;tools/bench/scene_probe.gdloads any demo scene headless and logs the characters with no input.BoneDefinition.muscle_torque+SkeletonDetector.MUSCLE_TORQUE_TABLE(hip 200, knee 150, spine 150, shoulder 60, elbow 40, neck 30, wrist 10, ankle 60), set by both profile factories.muscle_mode,muscle_strength_scale,muscle_gain(0.10),muscle_max_angular_velocity,muscle_root_pin,muscle_root_torque,muscle_angular_damp,muscle_linear_damp.tools/bench/ybot_bench.gd: the acceptance bench on the real character (idle / react_front / deterministic bullet on the hand;BENCH_HZ,BENCH_DIAG,BENCH_VARIANT).muscle_strength_curve(0.5): torque =muscle_torque × ratio^curve. The hit/stagger/fatigue reductions were tuned as blend fractions; as raw torque the 10 % stagger floor collapsed a staggered character into a ragdoll within 0.3 s. √ keeps limp = 0 and full = 1._root_hold_factor): they stand in for balance until 0.6.0; scaled by hit strength they let a stagger topple.KickbackCharacterresolves a null profile/tuning once and hands concrete resources to every controller (a null passed throughconfigure()used to strip the controller's roles and disable foot/arm IK).Engine facts measured (now in GODOT_CONSTRAINTS.md)
Ybot bench (60 Hz, foot IK on)
120 Hz: motor beats legacy on idle and react (0.73 / 2.43 idle, 10.6 react). 30 Hz idle is 4.9° with the defaults (was a 15° ring). Acceptance per
docs/PLAN.mdmet at 60 and 120 Hz; the plan records two criteria revised after the first bench run (the original TRACK and HIT targets were written before the bench existed and were unmet even by legacy).Open, documented as MISSES in docs/PLAN.md, carried to 0.6.0: react_front tracks at 2.4× legacy against a 1.75× bar and the hand-hit deflection is 1.8× legacy against a 3× bar — the per-axis command is robust but less responsive on violent motion, and hits read less vividly than legacy (the muscle knobs are now Tuning Lab sliders). Also: 30 Hz hand hits can wrap a wrist limit; feet are not yet load-bearing. Resolver CPU ≈ 1.4× legacy on a quiet run (µs timing is noisy on Windows).
Human gate (visual, in the editor)
Every demo now runs on
JOINT_MOTORby default:trigger_ragdoll/set_persistent) falls and settles at real gravityOn pass: merge, bump
plugin.cfgto 0.5.0, tag.🤖 Generated with Claude Code
https://claude.ai/code/session_013gMUV3TEWBfATLUiYpmX71