OverviewFilesCommitsIssuesDocsReleasesPeers

hyper-tps / scenes / test / far_coord_walk_gate.gd

210 lines · gd ref:
extends SceneTree
## DOES WALKING DEGRADE WITH HORIZONTAL DISTANCE FROM THE ORIGIN?
##
##   godot --headless --path . --script res://scenes/test/far_coord_walk_gate.gd
##
## The diffusion map bakes its heightfield at world x 38370..69150,
## z 23010..53790, so the player walks 40-70 km from the origin. The erosion map
## keeps you within a few km. Measured travel efficiency: 6% there, ~95% here.
## The scenes are otherwise identical, and the SAME scene with a broken script
## and a completely different generator is also hard to walk on -- so the cause
## is not the terrain source.
##
## float32 at 53,753 m has an ULP of 3.91 mm. CharacterBody3D's safe_margin is
## 1 mm. A margin finer than the coordinate resolution cannot mean anything, and
## the penetration measured in play sat at 1.88 mm -- about half an ULP.
##
## hyper-games #47 recorded "horizontal distance does not break physics -- clean
## at 100 km", measured with large_coord_physics_gate. This asks a different
## question: not whether a cast lands, but whether a capsule DRIVEN ALONG a
## triangulated slope keeps its speed. Those can differ, and the play data says
## they do.
##
## Exit 0 if efficiency at the far offsets stays near the offset-0 control.

const RADIUS := 0.3
const HEIGHT := 1.58
const SLOPE_DEG := 15.0
const STEPS := 240
const DT := 1.0 / 60.0
const SPEED := 8.0
const GRAVITY := 9.8


func _initialize() -> void:
	# THE CONTACT NORMAL, whose right answer is known exactly.
	#
	# The first version of this gate simulated walking and its control failed at
	# offset 0 (60.8%), which makes every other row unusable -- a hand-rolled
	# slide loop is a bad walker, and a harness that cannot do the thing at the
	# origin cannot report on doing it far away.
	#
	# This asks something with an exact answer instead: drop the shipping capsule
	# onto a 15.00 degree slope and read the normal the physics server reports.
	# It should be 15.00 degrees at every distance. Any deviation is contact
	# resolution failing, and needs no model of walking to interpret.
	print("capsule r=%.2f h=%.2f dropped onto a %.2f deg trimesh slope" % [RADIUS, HEIGHT, SLOPE_DEG])
	print("the reported contact normal should read %.2f deg at EVERY offset" % SLOPE_DEG)
	print()
	print(" offset (m) |  ULP (m) | contact normal | error (deg) | depth (m)")
	var worst_err := 0.0
	var worst_off := 0.0
	var base_err := -1.0
	for off in [0.0, 1000.0, 5000.0, 20000.0, 38400.0, 53750.0, 100000.0]:
		var r := _probe_normal(off)
		if not bool(r["hit"]):
			print(" %10.0f | %8.5f | NO CONTACT AT ALL" % [off, r["ulp"]])
			continue
		var err: float = absf(float(r["deg"]) - SLOPE_DEG)
		if base_err < 0.0:
			base_err = err
		if err > worst_err:
			worst_err = err
			worst_off = off
		print(" %10.0f | %8.5f | %13.3f  | %10.3f  | %.6f"
				% [off, r["ulp"], r["deg"], err, r["depth"]])

	print()
	# CONTROL: at the origin the answer must be right, or nothing below it counts.
	var ok := base_err >= 0.0 and base_err < 0.05
	print("CONTROL at offset 0: error %.4f deg -> %s"
			% [base_err, "PASS" if ok else "FAIL — the harness is wrong, ignore every row"])
	if ok:
		print("WORST: %.3f deg error at %.0f m" % [worst_err, worst_off])
		if worst_err > 1.0:
			print("=> CONTACT RESOLUTION DEGRADES WITH DISTANCE FROM THE ORIGIN.")
			print("   No terrain generator, no heightfield, no GPU: a flat analytic")
			print("   slope and the shipping capsule.")
		else:
			print("=> The contact normal holds at every distance. Coordinate magnitude")
			print("   does NOT explain the play measurement, and I should stop saying it does.")
	quit(0 if ok else 2)


## Drop the capsule straight down onto the slope and report the contact.
func _probe_normal(off: float) -> Dictionary:
	var world := World3D.new()
	var shape: RID = PhysicsServer3D.concave_polygon_shape_create()
	PhysicsServer3D.shape_set_data(shape, {"faces": _slope_faces(off)})
	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)

	# Start 2 m above the slope at its centre, fall 4 m.
	var start := Vector3(off, 2.0, off)
	var params := PhysicsTestMotionParameters3D.new()
	params.from = Transform3D(Basis(), start)
	params.motion = Vector3(0, -4.0, 0)
	PhysicsServer3D.body_set_state(body, PhysicsServer3D.BODY_STATE_TRANSFORM, params.from)
	var res := PhysicsTestMotionResult3D.new()
	var hit: bool = PhysicsServer3D.body_test_motion(body, params, res)
	var out := {"hit": hit, "deg": 0.0, "depth": 0.0, "ulp": 0.0}
	if hit:
		var n: Vector3 = res.get_collision_normal()
		out["deg"] = rad_to_deg(acos(clampf(n.dot(Vector3.UP), -1.0, 1.0)))
		out["depth"] = res.get_collision_depth()
	if off > 0.0:
		out["ulp"] = (off * 1.0000001192092896) - off
	PhysicsServer3D.free_rid(body)
	PhysicsServer3D.free_rid(ground)
	PhysicsServer3D.free_rid(shape)
	return out


## A triangulated slope patch, 60 x 60 m, centred at (off, 0, off), meshed at
## 1 m like the terrain's LOD-0 voxels so the triangle scale matches.
func _slope_faces(off: float) -> PackedVector3Array:
	var f := PackedVector3Array()
	var t := tan(deg_to_rad(SLOPE_DEG))
	var n := 60
	for i in range(n):
		for j in range(n):
			var x0 := off - 30.0 + float(i)
			var z0 := off - 30.0 + float(j)
			var p00 := Vector3(x0, (x0 - off) * t, z0)
			var p10 := Vector3(x0 + 1.0, (x0 + 1.0 - off) * t, z0)
			var p01 := Vector3(x0, (x0 - off) * t, z0 + 1.0)
			var p11 := Vector3(x0 + 1.0, (x0 + 1.0 - off) * t, z0 + 1.0)
			# Winding measured by trimesh_winding_gate: a standable floor has
			# (b-a)x(c-a) pointing DOWN.
			f.append_array([p00, p10, p11])
			f.append_array([p00, p11, p01])
	return f


func _walk(off: float) -> Dictionary:
	var world := World3D.new()
	var shape: RID = PhysicsServer3D.concave_polygon_shape_create()
	PhysicsServer3D.shape_set_data(shape, {"faces": _slope_faces(off)})
	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)

	var pos := Vector3(off - 20.0, (off - 20.0 - off) * tan(deg_to_rad(SLOPE_DEG)) + HEIGHT, off)
	var want_total := 0.0
	var got_total := 0.0
	var contacts := 0.0
	var vy := 0.0

	for s in range(STEPS):
		# Gravity, then a horizontal push along +X. A miniature move_and_slide:
		# attempt the motion, and on a hit slide the remainder along the plane.
		vy -= GRAVITY * DT
		var motion := Vector3(SPEED * DT, vy * DT, 0.0)
		var want := absf(SPEED * DT)
		var before := pos
		for _slide in range(4):
			var params := PhysicsTestMotionParameters3D.new()
			params.from = Transform3D(Basis(), pos)
			params.motion = motion
			var res := PhysicsTestMotionResult3D.new()
			var hit: bool = PhysicsServer3D.body_test_motion(body, params, res)
			if not hit:
				pos += motion
				break
			contacts += 1.0
			pos += motion * res.get_collision_safe_fraction()
			var n: Vector3 = res.get_collision_normal()
			if n.dot(Vector3.UP) > 0.5:
				vy = 0.0
			var remainder: Vector3 = motion * (1.0 - res.get_collision_safe_fraction())
			motion = remainder.slide(n)
			if motion.length_squared() < 1e-12:
				break
		want_total += want
		got_total += (pos - before).x
		PhysicsServer3D.body_set_state(body, PhysicsServer3D.BODY_STATE_TRANSFORM,
				Transform3D(Basis(), pos))

	PhysicsServer3D.free_rid(body)
	PhysicsServer3D.free_rid(ground)
	PhysicsServer3D.free_rid(shape)
	var ulp := 0.0
	if off > 0.0:
		var fo := float(off)
		ulp = (fo * 1.0000001192092896) - fo    # one float32 step at that magnitude
	return {
		"got": got_total, "want": want_total,
		"eff": got_total / maxf(want_total, 1e-9),
		"contacts": contacts / float(STEPS),
		"ulp": ulp,
	}