extends SceneTree
## A CAPSULE PUSHED ALONG RIPPLED GROUND, SWEEPING THE RIPPLE AMPLITUDE.
##
## PHASE 4 minimal reproduction for the boundary the play data draws:
##
## perfectly flat trimesh -> 100% (this gate, amplitude 0)
## diffusion map, 0.01-0.03 m rms, 0.05 m steps per 0.25 m -> 6-32%
## erosion map, 0.18 m rms, 0.24 m steps -> 97%
##
## Both real surfaces are meshed at the same 1 m voxel, so both carry ripples of
## about that wavelength; they differ only in amplitude, by roughly 10x. The
## failing case is neither smooth nor rough but IN BETWEEN, at a few centimetres
## -- the scale of the capsule margin and of the contact depths measured in play
## (0.00284 m, reported three times identically).
##
## PREDICTS: efficiency dips at small non-zero amplitudes and recovers as the
## amplitude grows past what the capsule can bridge. If instead it falls
## monotonically, the story "small ripples are the worst case" is wrong.
##
## godot --headless --path . --script res://scenes/test/far_coord_flat_gate.gd
##
## The amplitude-0 row is the control and must read 100%.
##
## This exists because I tested the wrong quantity earlier and drew the wrong
## conclusion from it. far_coord_walk_gate measured a STATIC contact normal,
## which is derived from a triangle's plane and stays exact to 100 km -- so it
## reported "coordinate magnitude is fine" while the play data said otherwise.
## The quantity that matters is what happens over MANY frames of contact
## resolution, where penetration depth is compared against a margin.
##
## Measured in play: at x=53756, z=38402 one float32 ULP is 3.9 mm and the
## contact depth sat at 2.84 mm -- 0.7 of an ULP -- reported three times
## identically, with the character delivering -0.0001 m of a 0.1325 m request.
## At x=-145, z=59 the ULP is 0.06 mm, depth was 1.0 mm, contacts were distinct,
## and travel efficiency was 98.2%.
const RADIUS := 0.3
const HEIGHT := 1.58
const STEPS := 300
const DT := 1.0 / 60.0
const SPEED := 8.0
func _initialize() -> void:
print("capsule r=%.2f h=%.2f pushed at %.1f m/s over ground rippled at 1 m wavelength"
% [RADIUS, HEIGHT, SPEED])
print("amplitude 0 is the control and must read 100%")
print()
print(" amplitude (m) | rms (m) | delivered | requested | efficiency | mean depth")
var base := -1.0
for amp in [0.0, 0.005, 0.01, 0.02, 0.03, 0.05, 0.08, 0.12, 0.18, 0.25]:
var r := _push(0.0, amp)
if base < 0.0:
base = r["eff"]
print(" %13.3f | %7.4f | %9.3f | %9.3f | %9.1f%% | %.6f"
% [amp, amp * 0.707, r["got"], r["want"], 100.0 * r["eff"], r["depth"]])
print()
var ok := base > 0.95
print("CONTROL at amplitude 0: %.1f%% -> %s"
% [100.0 * base, "PASS" if ok else "FAIL — harness broken, ignore every row"])
quit(0 if ok else 2)
func _flat_faces(off: float, amp: float) -> PackedVector3Array:
# 1 m triangles, matching LOD-0 voxel scale, over 80 x 80 m.
var f := PackedVector3Array()
var n := 80
for i in range(n):
for j in range(n):
var x0 := off - 40.0 + float(i)
var z0 := off - 40.0 + float(j)
# A 1 m ripple, the wavelength both real surfaces are meshed at.
var p00 := Vector3(x0, _h(x0, z0, amp), z0)
var p10 := Vector3(x0 + 1.0, _h(x0 + 1.0, z0, amp), z0)
var p01 := Vector3(x0, _h(x0, z0 + 1.0, amp), z0 + 1.0)
var p11 := Vector3(x0 + 1.0, _h(x0 + 1.0, z0 + 1.0, amp), z0 + 1.0)
# Standable winding, per trimesh_winding_gate: (b-a)x(c-a) points DOWN.
f.append_array([p00, p10, p11])
f.append_array([p00, p11, p01])
return f
func _h(x: float, z: float, amp: float) -> float:
if amp <= 0.0:
return 0.0
return amp * (sin(x * TAU / 1.0) * 0.5 + sin(z * TAU / 1.0) * 0.5)
func _push(off: float, amp: float) -> Dictionary:
var world := World3D.new()
var shape: RID = PhysicsServer3D.concave_polygon_shape_create()
PhysicsServer3D.shape_set_data(shape, {"faces": _flat_faces(off, amp)})
var ground: RID = PhysicsServer3D.body_create()
PhysicsServer3D.body_set_mode(ground, PhysicsServer3D.BODY_MODE_STATIC)
PhysicsServer3D.body_add_shape(ground, shape)
PhysicsServer3D.body_set_space(ground, world.space)
var cap := CapsuleShape3D.new()
cap.radius = RADIUS
cap.height = HEIGHT
var body: RID = PhysicsServer3D.body_create()
PhysicsServer3D.body_set_mode(body, PhysicsServer3D.BODY_MODE_KINEMATIC)
PhysicsServer3D.body_add_shape(body, cap.get_rid())
PhysicsServer3D.body_set_space(body, world.space)
# Rest the capsule ON the ground: centre at half height, then settle it with
# one downward probe so the starting penetration is the engine's own.
var pos := Vector3(off - 30.0, HEIGHT * 0.5 + amp + 0.1, off)
var settle := PhysicsTestMotionParameters3D.new()
settle.from = Transform3D(Basis(), pos + Vector3(0, 0.5, 0))
settle.motion = Vector3(0, -1.0, 0)
var sres := PhysicsTestMotionResult3D.new()
PhysicsServer3D.body_set_state(body, PhysicsServer3D.BODY_STATE_TRANSFORM, settle.from)
if PhysicsServer3D.body_test_motion(body, settle, sres):
pos = settle.from.origin + Vector3(0, -1.0, 0) * sres.get_collision_safe_fraction()
var want_total := 0.0
var got_total := 0.0
var depth_sum := 0.0
var depth_n := 0
for s in range(STEPS):
# A small downward bias each step, exactly as gravity keeps a walker in
# contact. Without it the capsule floats free and the test measures
# nothing.
var motion := Vector3(SPEED * DT, -0.02, 0.0)
var params := PhysicsTestMotionParameters3D.new()
params.from = Transform3D(Basis(), pos)
params.motion = motion
var res := PhysicsTestMotionResult3D.new()
PhysicsServer3D.body_set_state(body, PhysicsServer3D.BODY_STATE_TRANSFORM, params.from)
var hit: bool = PhysicsServer3D.body_test_motion(body, params, res)
var before := pos
if hit:
depth_sum += res.get_collision_depth()
depth_n += 1
pos += motion * res.get_collision_safe_fraction()
# Slide the remainder along the contact plane. On FLAT ground the
# plane is horizontal, so this removes only the downward part and
# the horizontal request should survive intact.
var rem: Vector3 = motion * (1.0 - res.get_collision_safe_fraction())
pos += rem.slide(res.get_collision_normal())
else:
pos += motion
want_total += SPEED * DT
got_total += (pos - before).x
PhysicsServer3D.free_rid(body)
PhysicsServer3D.free_rid(ground)
PhysicsServer3D.free_rid(shape)
var ulp := 0.0
if off > 0.0:
ulp = (off * 1.0000001192092896) - off
return {
"got": got_total, "want": want_total,
"eff": got_total / maxf(want_total, 1e-9),
"depth": depth_sum / maxf(float(depth_n), 1.0),
"ulp": ulp,
}