Playable table (hinge-motor flippers, plunger, bumper, drain) plus a headless probe harness that measured, per engine: tunneling of a fast ball through a 1 cm wall with CCD off/on, and hinge-motor drive. Findings in godot/README.md: Box3D never tunnels even with CCD off, but its hinge motor sign is inverted vs Jolt/GodotPhysics, and Jolt blows through angular limits that Box3D holds. Ships a from-source macOS arm64 libgodot-box3d.dylib (upstream has no releases). Also gitignore the remaining Microsoft media at repo root. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
93 lines
3.9 KiB
Markdown
93 lines
3.9 KiB
Markdown
# Godot physics testbed (Box3D / Jolt / GodotPhysics A/B)
|
||
|
||
A minimal playable pinball table for Godot 4.7 whose real job is comparing 3D physics
|
||
backends on the things pinball stresses: fast-small-ball CCD, motorised hinge flippers,
|
||
and joint limits. The backend is one project setting, so the same table runs under all
|
||
three engines.
|
||
|
||
## Run it
|
||
|
||
```bash
|
||
/Applications/Godot.app/Contents/MacOS/Godot --path godot
|
||
```
|
||
|
||
Keys: `Z` left flipper, `/` right flipper, `Space` plunger, `R` reset ball.
|
||
|
||
The project defaults to `Box3D Physics (Extension)` via
|
||
[godot-box3d](https://github.com/bearlikelion/godot-box3d) (MIT). `bin/` carries a
|
||
macOS arm64 dylib built from source (upstream has no releases; build recipe below).
|
||
|
||
## A/B probe harness
|
||
|
||
```bash
|
||
godot/run_probe.sh # GODOT_BIN=... to point at a different binary
|
||
```
|
||
|
||
Writes `override.cfg` to switch `physics/3d/physics_engine`, runs `probe/probe.tscn`
|
||
headless under each backend, and prints per-engine results (also saved to
|
||
`probe/results/`, untracked). Two tests:
|
||
|
||
1. **Tunneling** — ball fired at a 1 cm static wall at 5–160 m/s, CCD off and on.
|
||
2. **Hinge motor** — a motorised HingeJoint3D arm; a flipper is a motorised hinge.
|
||
|
||
There is also a table-level flipper check:
|
||
`PINBALL_AUTOTEST=1 godot --headless --path godot` flips both real flippers for 0.5 s
|
||
and prints the swing angles.
|
||
|
||
## Results (2026-08-06, Godot 4.7.stable, M3 Ultra)
|
||
|
||
Tunneling — ball contained by the 1 cm wall?
|
||
|
||
| speed m/s | Godot ccd off | Jolt ccd off | Box3D ccd off | all three, ccd on |
|
||
|---|---|---|---|---|
|
||
| 5 | tunneled* | tunneled* | **contained** | contained |
|
||
| 10 | tunneled | contained* | **contained** | contained |
|
||
| 20 | contained* | contained* | **contained** | contained |
|
||
| 40 | contained* | contained* | **contained** | contained |
|
||
| 80 | tunneled | tunneled | **contained** | contained |
|
||
| 160 | tunneled | tunneled | **contained** | contained |
|
||
|
||
\* With a 60 Hz tick, every speed here can step over a 1 cm wall; the discrete-engine
|
||
"contained" rows are phase luck (2 m at 20 and 40 m/s lands a tick exactly on the wall
|
||
— and both engines produce identical trajectories, so they get lucky identically).
|
||
The signal is the pattern, not individual rows: **Box3D never tunnels even with the
|
||
CCD flag off** (speculative contacts are always on), the other two need
|
||
`continuous_cd = true`, and with it all three hold at 160 m/s.
|
||
|
||
Hinge motor (target 5 rad/s, 1 s):
|
||
|
||
| engine | reached rad/s | moved | verdict |
|
||
|---|---|---|---|
|
||
| GodotPhysics3D | −5.0 | 73.5° (wrapped) | works |
|
||
| Jolt | −5.0 | 75.1° (wrapped) | works |
|
||
| Box3D | **+5.0** | 73.5° (wrapped) | works |
|
||
|
||
Flipper autotest on the real table (motors at ±25 rad/s, limits −5°…+50°):
|
||
|
||
| engine | left swing | right swing |
|
||
|---|---|---|
|
||
| Box3D | +51.8° — **stops at the 50° limit** | −51.8° — stops at limit |
|
||
| Jolt | −106.6° — wrong way, **blows ~100° past the limit** | +104.3° — same |
|
||
|
||
Two real findings, half a second of headless runtime each:
|
||
|
||
1. **Box3D's hinge motor sign is inverted** relative to GodotPhysics/Jolt (upstream
|
||
drop-in-compatibility bug candidate for godot-box3d). The table's flipper signs are
|
||
tuned for Box3D, the project default — flip `FLIP_SPEED`/`REST_SPEED` signs in
|
||
`scenes/table.gd` to play under Jolt.
|
||
2. With the motor overdriving into them, **Box3D enforces the angular limits hard**
|
||
while Jolt lets this setup punch far through them.
|
||
|
||
## Rebuilding the extension
|
||
|
||
```bash
|
||
git clone --recurse-submodules https://github.com/bearlikelion/godot-box3d
|
||
cd godot-box3d && cmake -B build -DCMAKE_BUILD_TYPE=Release && cmake --build build -j
|
||
cp bin/libgodot-box3d.dylib <here>/bin/
|
||
```
|
||
|
||
Known Box3D gaps that don't matter for pinball: no Generic6DOF/ConeTwist joints, no
|
||
SoftBody3D, Area3D can't detect trimesh/heightmap bodies (the ball is a sphere; fine).
|
||
Box3D also ignores Godot's hinge BIAS/LIMIT_SOFTNESS/LIMIT_RELAXATION params (warns at
|
||
load; harmless here).
|