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> |
||
|---|---|---|
| .. | ||
| probe | ||
| scenes | ||
| godot-box3d.gdextension | ||
| godot-box3d.gdextension.uid | ||
| project.godot | ||
| README.md | ||
| run_probe.sh | ||
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
/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 (MIT). bin/ carries a
macOS arm64 dylib built from source (upstream has no releases; build recipe below).
A/B probe harness
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:
- Tunneling — ball fired at a 1 cm static wall at 5–160 m/s, CCD off and on.
- 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:
- 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_SPEEDsigns inscenes/table.gdto play under Jolt. - With the motor overdriving into them, Box3D enforces the angular limits hard while Jolt lets this setup punch far through them.
Rebuilding the extension
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).