Files
VR-Mess/research/godot-box3d-6dof.md
T

92 lines
8.0 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
# Addon patch 2: Generic6DOFJoint3D on Box3D (2026-10-02)
Second local patch to the godot-box3d fork (after the ConeTwist→weld patch, `godot-box3d-weld.md`). Purpose:
the joint primitives for the Project-TX-style muscle ragdoll (`nimso-project-tx.md`, `vr-physics-rig-hexabody.md` §4).
- Fork branch `generic-6dof-joint` (on top of `rigid-weld-joint`) in `.vendor/godot-box3d`, commit "Expose Box3D
joints through Godot's Generic6DOFJoint3D"; patch file `.vendor/box3d-generic-6dof.patch`. NOT pushed to
`github.com/Foxyz1302/godot-box3d` yet (owner decision). Linux binary only (same rebuild recipe as the weld
patch: `cmake --build build --parallel 12` in `.vendor/godot-box3d`, copy `bin/libgodot-box3d.so` to
`addons/godot-box3d/bin/`; stock binaries stay in `addons/godot-box3d/bin-stock/`).
- Files: `src/joints/box3d_generic_6dof_joint_impl_3d.{hpp,cpp}` (new), `src/servers/box3d_physics_server_3d.cpp`
(`_joint_make_generic_6dof` + param/flag get/set dispatch; was a hard error).
## Mapping (configuration-based — Box3D has no general 6-DOF constraint)
| Godot configuration | Box3D joint | Notes |
|---|---|---|
| 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); **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.
- Motor target velocities are per joint axis in **joint frame A**; the impl converts to Box3D's world-space relative
angular/linear velocity at set time using body A's current rotation → set them EVERY TICK (they are a PD output).
- Flags and limits **rebuild** the Box3D joint (warm start lost — set them once); motor velocities, force limits,
springs and equilibrium points apply **live**. Configure before `AddChild` (as with every joint here).
- Bullet-era params (limit softness/restitution/damping, ERP, angular force limit) are ignored.
## Muscle recipe (C#, used by `scripts/SixDofSpike.cs`)
```csharp
// BALL joint: angular limits as the anatomical range, EnableMotor on X/Y/Z, AngularMotorForceLimit = strength (N·m)
Quaternion rel = frameAWorld.Inverse() * frameBWorld; // current joint rotation, frame-A coordinates
Vector3 err = BodyMath.RotationError(targetRel, rel); // axis*angle in frame-A coordinates
Vector3 w = (err * Kp).LimitLength(maxSpin); // PD velocity
joint.SetParamX(Generic6DofJoint3D.Param.AngularMotorTargetVelocity, w.X); // …Y, …Z
```
The solver applies at most `AngularMotorForceLimit` torque per step — strength is a real cap, leverage emerges.
## Proof — `scenes/sixdof_spike.tscn` ("6DOF: overall PASS", 3× identical)
server type `Type6Dof`; BALL pivot holds (≤ 0.7 mm) while swinging 100°; cone 30° / twist 10° hold under hard torque
(31.3° / 11.6° peak — Box3D soft-limit overshoot); muscle PD holds a 1 kg × 0.6 m lever (2.94 N·m gravity load)
horizontal with a 10 N·m cap (0.0°) and FAILS with 1.5 N·m (drops 107°); tracks a 60° raise to 0.0°; DRIVE follows a
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).