OverviewFilesCommitsIssuesDocsReleasesPeers

hyper-tps / scenes / test / far_coord_flat_gate.gd

160 lines · gd ref:
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,
	}