macrosoft3dpinball/godot/probe/probe.gd
m3ultra 2ca67bb28b Add Godot 4.7 physics testbed: Box3D/Jolt/GodotPhysics A/B pinball table
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>
2026-08-06 05:33:07 +10:00

113 lines
3.8 KiB
GDScript

extends Node3D
## Headless physics probe. Run via run_probe.sh, which switches the physics
## engine through override.cfg and launches this scene under each backend.
##
## Test 1 — tunneling: fire a ball at a 1 cm static wall at increasing speeds,
## with CCD off and on, and report whether the wall contains it.
## Test 2 — hinge motor: drive a hinged arm with a joint motor and report
## whether it actually rotates (a flipper is a motorised hinge).
const WALL_THICKNESS := 0.01
const BALL_RADIUS := 0.0135
const START_Z := 2.0
const SPEEDS := [5.0, 10.0, 20.0, 40.0, 80.0, 160.0]
func _ready() -> void:
var engine := str(ProjectSettings.get_setting("physics/3d/physics_engine"))
print("PROBE_START engine=%s tick=%d" % [engine, Engine.physics_ticks_per_second])
_run(engine)
func _run(engine: String) -> void:
var wall := StaticBody3D.new()
var cs := CollisionShape3D.new()
var box := BoxShape3D.new()
box.size = Vector3(1.0, 1.0, WALL_THICKNESS)
cs.shape = box
wall.add_child(cs)
add_child(wall)
for ccd in [false, true]:
for speed in SPEEDS:
var r := await _fire(float(speed), ccd)
print("PROBE engine=\"%s\" speed=%d ccd=%s result=%s final_z=%.3f drift_y=%.4f" % [
engine, int(speed), "on" if ccd else "off",
"tunneled" if r.tunneled else "contained", r.final_z, r.drift_y])
var m := await _motor_test()
print("PROBE_MOTOR engine=\"%s\" moved_deg=%.1f works=%s" % [
engine, m.moved_deg, "yes" if m.moved_deg > 20.0 else "NO"])
get_tree().quit(0)
func _fire(speed: float, ccd: bool) -> Dictionary:
var ball := RigidBody3D.new()
var cs := CollisionShape3D.new()
var sph := SphereShape3D.new()
sph.radius = BALL_RADIUS
cs.shape = sph
ball.add_child(cs)
ball.mass = 0.08
ball.continuous_cd = ccd
ball.gravity_scale = 0.0
ball.can_sleep = false
ball.position = Vector3(0, 0, START_Z)
add_child(ball)
ball.linear_velocity = Vector3(0, 0, -speed)
# Enough ticks to cover the distance plus settle time.
var ticks := maxi(90, int(ceil((START_Z + 1.0) / speed * 60.0)) + 30)
for i in ticks:
await get_tree().physics_frame
var result := {
"tunneled": ball.position.z < -(WALL_THICKNESS / 2.0 + BALL_RADIUS + 0.05),
"final_z": ball.position.z,
"drift_y": absf(ball.position.y), # nonzero = gravity_scale unsupported
}
ball.queue_free()
await get_tree().physics_frame
return result
func _motor_test() -> Dictionary:
var anchor := StaticBody3D.new()
var acs := CollisionShape3D.new()
var abox := BoxShape3D.new()
abox.size = Vector3(0.05, 0.05, 0.05)
acs.shape = abox
anchor.add_child(acs)
anchor.position = Vector3(3, 0, 0) # far from the wall test
add_child(anchor)
var arm := RigidBody3D.new()
var ccs := CollisionShape3D.new()
var bbox := BoxShape3D.new()
bbox.size = Vector3(0.1, 0.03, 0.022)
ccs.shape = bbox
ccs.position = Vector3(0.05, 0, 0)
arm.add_child(ccs)
arm.mass = 0.15
arm.gravity_scale = 0.0
arm.can_sleep = false
arm.position = Vector3(3.1, 0, 0)
add_child(arm)
var joint := HingeJoint3D.new()
# Local Z (the hinge axis) pointed along world +Y.
joint.transform = Transform3D(Basis(Vector3(1, 0, 0), -PI / 2.0), Vector3(3.1, 0, 0))
joint.set_flag(HingeJoint3D.FLAG_ENABLE_MOTOR, true)
joint.set_param(HingeJoint3D.PARAM_MOTOR_TARGET_VELOCITY, 5.0)
joint.set_param(HingeJoint3D.PARAM_MOTOR_MAX_IMPULSE, 5.0)
add_child(joint)
# Paths only resolve once the joint is inside the tree.
joint.node_a = joint.get_path_to(anchor)
joint.node_b = joint.get_path_to(arm)
var start_q := arm.global_transform.basis.get_rotation_quaternion()
for i in 60:
await get_tree().physics_frame
var end_q := arm.global_transform.basis.get_rotation_quaternion()
var moved := start_q.angle_to(end_q)
print("PROBE_MOTOR_DEBUG rotation=%s ang_vel=%s pos=%s" % [
arm.rotation, arm.angular_velocity, arm.position])
return {"moved_deg": rad_to_deg(moved)}