Shoulder range of motion: drive-mode angular limits in the Box3D fork
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
1 parent
423b709cc9
commit
3e300c7f04
7 files changed
+826
-5
No files matched your search
@@ -17,7 +17,7 @@ the joint primitives for the Project-TX-style muscle ragdoll (`nimso-project-tx.
|
||||
|---|---|---|
|
||||
| all 3 linear axes locked (limit on, lower ≥ upper, no linear spring/motor), angular not fully locked or angular motor/spring on | `b3SphericalJoint` = **BALL** | joint **X = twist axis** (angular X limits = twist range); swing cone symmetric, half-angle = largest \|limit\| of angular Y/Z (warned if Y/Z differ); angular spring → rotation spring to the angular equilibrium (Euler, Godot default YXZ); angular **motor = torque-capped velocity motor** |
|
||||
| everything locked, no angular motor/spring (the Godot default) | `b3WeldJoint` = **WELD** | rigid at the creation pose |
|
||||
| any linear axis free/springy/motored | `b3MotorJoint` = **DRIVE** | springs pull frame B onto frame A shifted by the equilibrium points (linear + Euler); force/torque caps = Linear/Angular **MotorForceLimit** (0 = uncapped); motor target velocities add a velocity drive; linear limits NOT enforced (warned) |
|
||||
| any linear axis free/springy/motored | `b3MotorJoint` = **DRIVE** | springs pull frame B onto frame A shifted by the equilibrium points (linear + Euler); force/torque caps = Linear/Angular **MotorForceLimit** (0 = uncapped); motor target velocities add a velocity drive; linear limits NOT enforced (warned); **angular limits enforced** (patch 3, below) once any angular axis has a real range |
|
||||
|
||||
- **Units differ from Godot/Jolt**: spring STIFFNESS = frequency in **Hz**, DAMPING = **damping ratio** (Box3D
|
||||
soft constraints are mass-independent). Choose ~5–15 Hz, ratio ~1.
|
||||
@@ -46,3 +46,46 @@ horizontal with a 10 N·m cap (0.0°) and FAILS with 1.5 N·m (drops 107°); tra
|
||||
moving target to 0.000 m; a hand driven into a wall never penetrates it and pushes the 10 kg anchor body AWAY
|
||||
(−0.82 m — push-off reaction; pulling toward a fixed point is the climbing case); all-locked = rigid (0.13°); 90
|
||||
live flag toggles stay finite. All existing spikes (weld, physics, grab, body, collapse, arm) PASS 3× on the new binary.
|
||||
|
||||
## Patch 3 (2026-10-02): angular limits on the DRIVE — the shoulder range of motion
|
||||
|
||||
Problem: the shoulder is a DRIVE (linear motor = moving girdle socket, angular motor = muscle), and Box3D's
|
||||
`b3MotorJoint` had no angular limits — an external force could bend the arm into the chest or behind the back.
|
||||
A second joint can't add them (`b3SphericalJoint` always pins its anchor and would fight the moving socket), so the
|
||||
limits went into Box3D's motor joint itself.
|
||||
|
||||
- Fork branch `shoulder-cone-limit` (on `generic-6dof-joint`), commit "Angular limits for drive-mode
|
||||
Generic6DOFJoint3D". The Box3D change is `patches/box3d/0001-motor-joint-angular-limits.patch` in the fork, applied
|
||||
to the `box3d/` checkout by CMake at configure time (marker `b3MotorJointSwingLimit`, idempotent) — the submodule
|
||||
stays pinned to upstream. Rebuild recipe unchanged (`cmake -B build …`, `cmake --build build`). Linux binary only.
|
||||
- Box3D side (`b3MotorJointDef`): `limitRotationA/B` (own limit frames, so moving the drive target never moves the
|
||||
range), `enableSwingLimit` + `lower/upperSwingX/Y` (per-quadrant ELLIPSE on the swing rotation vector — asymmetric
|
||||
bounds), `enableTwistLimit` + `lower/upperTwistAngle`. Solved after the angular motor/spring, Jacobians rebuilt
|
||||
every sub-step: swing `J = −n + r′(φ)(z − cot θ · m)` (the radius change along an eccentric boundary — without
|
||||
it a fast arm sank 36° into the corners), exact twist Jacobian (Box3D's spherical `tan(θ/2)` form lags ~5° at 100°
|
||||
swing). Warm-started, included in joint torque reports and recordings (def size tripwire 168 → 232).
|
||||
- Wrapper mapping: a DRIVE gets limits when any angular axis has the limit flag on with lower < upper. Joint
|
||||
**X = twist** (X limits), **Y/Z = swing bounds**: the swing rotation-vector components about joint Y and Z must stay
|
||||
inside the ellipse whose semi-axes are the per-sign bounds (lower ≤ 0 ≤ upper, up to ±0.99π). Flag off = that axis
|
||||
free; lower ≥ upper = locked. Measured between the joint frames AS CREATED (relative rotation identity = centre),
|
||||
not the equilibrium-shifted target. Limit softness 120 Hz (joint default 60 Hz let the twist sink ~5° while the arm
|
||||
slid along the swing boundary under 25 N·m). Limits rebuild the joint — configure before `AddChild`.
|
||||
- Plain drives (angular limit flags off, or Godot's locked default with no real range) are byte-for-byte unchanged:
|
||||
`ragdoll_spike` prints identical numbers on the old and new binary.
|
||||
|
||||
### Shoulder values (for `PlayerRagdoll`)
|
||||
|
||||
Joint frame at build: X = the humerus as built (`dir`), Y = world up made square to it, Z = X × Y. Fitted against
|
||||
target arm directions (chest frame): bounds Y ∈ [−90°, +100°] (right arm; left mirrors to [−100°, +90°]),
|
||||
Z ∈ [−85°, +155°], twist ±80°. Inside with ≥ 2.6° margin: hanging, 45° extension, forward, overhead, overhead 10°
|
||||
behind, lateral, lateral 30° behind, 45°-across hug, hand on the opposite shoulder, hand behind the back, up-and-across.
|
||||
Out by ≥ 8°: medial horizontal, 80° extension, straight back, behind-up, behind the head across, medial behind-down.
|
||||
(Arm hanging medially in front of the torso stays allowed — real, and own-body collision is off anyway.)
|
||||
|
||||
### Proof (added to `sixdof_spike`, 3× identical; weld_spike 3× PASS on the same binary)
|
||||
|
||||
Shoulder-style drive (2 kg × 0.3 m arm, socket = linear velocity motor 2000 N, bounds Y [−20°, 60°], Z [−40°, 100°],
|
||||
twist ±45°): a 25 N·m torque sweeping every swing direction + 6 N·m alternating twist → swing overshoot 1.1° max,
|
||||
every bound reached (61/20/101/41°), twist max 46.3°, socket 1.8 mm. A 30 N·m muscle PD reaches targets inside the
|
||||
range (0.0 / 0.0 / 1.3° error) and stops at 60.2° when commanded to 85° (beyond the 60° bound). Moving the socket
|
||||
5.8 cm (shrug) tracks to 0.0 mm with the arm still (0.0° swing).
|
||||
@@ -219,3 +219,5 @@ fall); held poses drift 1 cm with the eye within 7 mm; crawl, mantle, climb, pul
|
||||
| **Stick crouch (right stick Y)** = `RagdollDriver.StickCrouch`, a virtual floor this far above the real one, fed to the solver, ground control and the view re-centering | The pose crouches, the play space settles that far below the ground, the camera rides down. Spike: 0.6 m → eye 1.045, origin −0.601, eye vs camera 5 mm; back up to 1.650 |
|
||||
| **XR origin drawn interpolated** (`PlayerRig`: physics owns `_physOrigin`; `_Process` (priority −100) shows `prev.InterpolateWith(cur, Engine.GetPhysicsInterpolationFraction())`, `_PhysicsProcess` (priority −100) restores it; anyone else moving the origin is adopted; jumps/snap turns don't interpolate) | The bodies are drawn interpolated a tick behind while the origin stepped raw at 144 Hz under a 90 Hz display — the body looked like it was in two places when moving (owner). Not verifiable headless; needs the headset |
|
||||
| Menu summon on left X only | Right A is jump |
|
||||
|
||||
**Shoulder range (2026-10-02):** the fork's DRIVE now honours angular limits (Box3D motor joint + swing ellipse/twist, `research/godot-box3d-6dof.md`); `PlayerRagdoll.ShoulderRange` (Y −90..+100° mirrored, Z −85..+155°, twist ±80°, frame X = humerus at build). Ragdoll spike unchanged with it on — the limits only engage when something forces the arm.
|
||||
Reference in new issue
Block a user