extends SceneTree
## Does the ENGINE's character physics and the shipping SphereSweep still work at
## the coordinates a 917 km world puts you at?
##
## WHY THIS GATE EXISTS. Two symptoms were reported on the big maps (DC Erosion,
## the planetoid): the player gets stuck and cannot travel, and the foot IK is
## garbled. Those look like two bugs. They share one candidate cause -- float32
## coordinate magnitude -- and that candidate is testable WITHOUT any terrain,
## which is the whole point of this gate: it changes ONE variable, the world
## coordinate, against ground whose exact height is known analytically.
##
## float32 has a 24-bit significand, so the spacing between representable values
## at |p| is about |p| * 2^-23:
##
## |p| = 0 m exact
## |p| = 1,000 m 0.00006 m
## |p| = 10,000 m 0.0010 m <- equals CharacterBody3D's default safe_margin
## |p| = 39,000 m 0.0039 m <- the DC Erosion spawn
## |p| = 458,000 m 0.0625 m <- the world edge; larger than the IK sweep sphere
##
## CharacterBody3D depenetrates using safe_margin (default 0.001 m) and FootIK
## sweeps a 0.05 m sphere over a 0.8 m span. Both are BELOW the representable
## spacing out there, so the prediction is: travel per second falls short, resting
## height jitters, and the sweep's hit point stops agreeing with the plane.
##
## THE INSTRUMENT IS A LAYER TOO. Offset 0 is the known-good input: if the numbers
## are not clean there, the ruler is broken and nothing below it means anything.
##
## TWO GROUNDS, because the shipping ground is not a box. DCCollisionTrimesh
## (dc_collision_trimesh.h:172-191) builds a ConcavePolygonShape3D whose faces are
## CHUNK-LOCAL and puts the world offset in the BODY TRANSFORM. A BoxShape3D at a
## global position is the other arrangement. If they differ, the difference is
## itself the finding -- it says where the precision is actually lost.
##
## Run:
## "$HYPER_TPS_GODOT" --headless --script res://scenes/test/large_coord_physics_gate.gd
# Capsule + body settings copied from player_procedural.tscn (lines 13-15, 41-43,
# 47-49). Not approximated -- a body with different dimensions is a different
# experiment.
const CAP_RADIUS := 0.3
const CAP_HEIGHT := 1.58
const CAP_Y := 0.79
const FLOOR_MAX_ANGLE := 1.047
const FLOOR_SNAP := 0.2
# FootIK's sweep geometry (foot_ik.gd:20-23).
const SPHERE_RADIUS := 0.05
const SWEEP_UP := 0.2
const SWEEP_DOWN := 0.6
const GROUND_OFFSET := 0.02
const DRIVE_SPEED := 8.0 # run_speed, player_procedural.gd:95
const SETTLE_FRAMES := 60
const DRIVE_FRAMES := 120
const PROBE_SAMPLES := 16
# The cases. (31337, 9700, -21500) is the ACTUAL DC Erosion spawn column
# (map_dc_erosion.gd:180 + the 9700 m authored in map_dc_erosion.tscn:33), not a
# round number chosen to look like one.
var _cases: Array = [
{"name": "origin ", "pos": Vector3(0, 0, 0)},
{"name": "1 km ", "pos": Vector3(1000, 300, 1000)},
{"name": "10 km ", "pos": Vector3(10000, 3000, 10000)},
{"name": "EROSION SPAWN ", "pos": Vector3(31337, 9700, -21500)},
{"name": "100 km ", "pos": Vector3(100000, 10000, 100000)},
{"name": "world edge ", "pos": Vector3(458000, 15000, 458000)},
]
var _ground_kinds: Array = ["box", "trimesh"]
var _kind_idx: int = 0
var _case_idx: int = 0
var _phase: int = 0 # 0 settle, 1 drive, 2 report
var _frame: int = 0
var _body: CharacterBody3D = null
var _ground: StaticBody3D = null
var _plane_y: float = 0.0
var _drive_start: Vector3 = Vector3.ZERO
var _settle_y_min: float = 0.0
var _settle_y_max: float = 0.0
var _floor_frames: int = 0
var _rows: Array = []
var _tick: float = 1.0 / 60.0
func _initialize() -> void:
_tick = 1.0 / float(Engine.physics_ticks_per_second)
print("[gate] physics tick = %.5f s, drive = %d frames, expected travel = %.3f m"
% [_tick, DRIVE_FRAMES, DRIVE_SPEED * DRIVE_FRAMES * _tick])
print("[gate] double precision build: %s" % str(_is_double()))
_arith_control()
_build_case()
## Pure coordinate arithmetic -- no physics, no collision. If a 0.133 m step
## survives here it is representable at that magnitude, and any shortfall the
## physics rows show is the PHYSICS, not the number.
func _arith_control() -> void:
print("")
print("[arith] adding %.5f m per frame, %d frames, no physics" % [DRIVE_SPEED * _tick, DRIVE_FRAMES])
for c in _cases:
var base: Vector3 = c["pos"]
var p: Vector3 = base
var n: int = 0
while n < DRIVE_FRAMES:
p.x += DRIVE_SPEED * _tick
n += 1
var moved: float = p.x - base.x
var expect: float = DRIVE_SPEED * float(DRIVE_FRAMES) * _tick
print("[arith] %s moved %.5f m of %.5f m (%.2f%%)" % [c["name"], moved, expect, 100.0 * moved / expect])
print("")
func _is_double() -> bool:
# 1.0 + 2^-30 survives a round trip only in float64. GDScript floats are always
# 64-bit; what this asks is whether the ENGINE was built with real_t = double,
# which is what the physics server actually uses.
if Engine.has_method("is_editor_hint"):
pass
var v: Variant = ProjectSettings.get_setting("application/config/features")
if v == null:
return false
return str(v).contains("Double Precision")
func _build_case() -> void:
var kind: String = _ground_kinds[_kind_idx]
var base: Vector3 = _cases[_case_idx]["pos"]
_plane_y = base.y
_ground = StaticBody3D.new()
var cs := CollisionShape3D.new()
if kind == "box":
# A single global-space box. The shape carries the world position.
var box := BoxShape3D.new()
box.size = Vector3(400.0, 4.0, 400.0)
cs.shape = box
_ground.add_child(cs)
root.add_child(_ground)
_ground.global_position = Vector3(base.x, base.y - 2.0, base.z)
else:
# The SHIPPING arrangement: chunk-local faces, world offset in the body
# transform. Mirrors dc_collision_trimesh.h:176-191.
var faces := PackedVector3Array()
var n := 30
var step := 4.0
var i := 0
while i < n:
var j := 0
while j < n:
var x0 := float(i) * step
var z0 := float(j) * step
var x1 := x0 + step
var z1 := z0 + step
faces.push_back(Vector3(x0, 0.0, z0))
faces.push_back(Vector3(x0, 0.0, z1))
faces.push_back(Vector3(x1, 0.0, z1))
faces.push_back(Vector3(x0, 0.0, z0))
faces.push_back(Vector3(x1, 0.0, z1))
faces.push_back(Vector3(x1, 0.0, z0))
j += 1
i += 1
var tri := ConcavePolygonShape3D.new()
# BOTH SIDES. A one-sided concave shape makes winding a second variable,
# and the first run of this gate fell through the patch at EVERY offset
# including the origin -- which is the rig failing, not the coordinate.
tri.backface_collision = true
tri.set_faces(faces)
cs.shape = tri
_ground.add_child(cs)
root.add_child(_ground)
# Centre the patch on the case position, exactly as the chunk origin does.
_ground.global_position = Vector3(base.x - float(n) * step * 0.5, base.y,
base.z - float(n) * step * 0.5)
_body = CharacterBody3D.new()
_body.floor_max_angle = FLOOR_MAX_ANGLE
_body.floor_snap_length = FLOOR_SNAP
_body.floor_constant_speed = true
var bcs := CollisionShape3D.new()
var cap := CapsuleShape3D.new()
cap.radius = CAP_RADIUS
cap.height = CAP_HEIGHT
bcs.shape = cap
bcs.position = Vector3(0, CAP_Y, 0)
_body.add_child(bcs)
root.add_child(_body)
_body.global_position = Vector3(base.x, base.y + 1.0, base.z)
_phase = 0
_frame = 0
_floor_frames = 0
_settle_y_min = 1e30
_settle_y_max = -1e30
func _physics_process(_delta: float) -> bool:
if _body == null:
return true
if _phase == 0:
_body.velocity = Vector3(0.0, -4.0, 0.0)
_body.move_and_slide()
if _body.is_on_floor():
var y: float = _body.global_position.y
if y < _settle_y_min:
_settle_y_min = y
if y > _settle_y_max:
_settle_y_max = y
_frame += 1
if _frame >= SETTLE_FRAMES:
_phase = 1
_frame = 0
_drive_start = _body.global_position
return false
if _phase == 1:
_body.velocity = Vector3(DRIVE_SPEED, -1.0, 0.0)
_body.move_and_slide()
if _body.is_on_floor():
_floor_frames += 1
_frame += 1
if _frame >= DRIVE_FRAMES:
_measure()
_teardown()
if not _advance():
_report()
return true
_build_case()
return false
return true
func _measure() -> void:
var kind: String = _ground_kinds[_kind_idx]
var cname: String = _cases[_case_idx]["name"]
var base: Vector3 = _cases[_case_idx]["pos"]
var travelled: float = (_body.global_position - _drive_start).length()
var expected: float = DRIVE_SPEED * float(DRIVE_FRAMES) * _tick
var jitter: float = 0.0
if _settle_y_max > -1e29:
jitter = _settle_y_max - _settle_y_min
# ---- FootIK-scale sweep, using the SHIPPING SphereSweep ----
var space: PhysicsDirectSpaceState3D = _body.get_world_3d().direct_space_state
var excludes: Array[RID] = [_body.get_rid()]
var miss: int = 0
var err_max: float = 0.0
var err_sum: float = 0.0
var k: int = 0
while k < PROBE_SAMPLES:
var ang: float = TAU * float(k) / float(PROBE_SAMPLES)
var p := Vector3(base.x + cos(ang) * 0.4, base.y + 0.05, base.z + sin(ang) * 0.4)
var res: Dictionary = SphereSweep.sweep(space, p + Vector3.UP * SWEEP_UP,
p - Vector3.UP * SWEEP_DOWN, SPHERE_RADIUS, excludes)
if res.is_empty():
miss += 1
else:
var hit: Vector3 = res["point"]
var e: float = hit.y - _plane_y
err_sum += e
if absf(e) > absf(err_max):
err_max = e
k += 1
var hits: int = PROBE_SAMPLES - miss
var err_mean: float = 0.0
if hits > 0:
err_mean = err_sum / float(hits)
print("[case] %s %s start=%s end=%s" % [kind, cname, str(_drive_start), str(_body.global_position)])
var ulp: float = maxf(maxf(absf(base.x), absf(base.y)), absf(base.z)) * pow(2.0, -23.0)
_rows.push_back({
"kind": kind, "name": cname, "ulp": ulp,
"travel": travelled, "expected": expected,
"pct": 100.0 * travelled / expected,
"jitter": jitter, "floor_pct": 100.0 * float(_floor_frames) / float(DRIVE_FRAMES),
"ik_miss": miss, "ik_err_mean": err_mean, "ik_err_max": err_max,
})
func _teardown() -> void:
if _body != null:
_body.queue_free()
_body = null
if _ground != null:
_ground.queue_free()
_ground = null
func _advance() -> bool:
_case_idx += 1
if _case_idx < _cases.size():
return true
_case_idx = 0
_kind_idx += 1
return _kind_idx < _ground_kinds.size()
## The origin row of the same ground is the reference. Whatever constant offset
## get_rest_info reports for a sphere resting on a plane, it reports it at the
## origin too -- so only the DEPARTURE from that is evidence.
func _ik_baseline(kind: String) -> float:
for r in _rows:
if r["kind"] == kind and r["name"] == _cases[0]["name"]:
return r["ik_err_max"]
return 0.0
func _report() -> void:
print("")
print("ground case float32 ulp travel/exp travel y-jitter on_floor IK miss IK err mean IK err max")
print("---------------------------------------------------------------------------------------------------------------------")
for r in _rows:
print("%-8s %s %10.5f m %6.1f%% %7.3f m %8.4f m %5.1f%% %2d/%d %9.5f m %9.5f m" % [
r["kind"], r["name"], r["ulp"], r["pct"], r["travel"],
r["jitter"], r["floor_pct"], r["ik_miss"], PROBE_SAMPLES,
r["ik_err_mean"], r["ik_err_max"]])
print("")
# Pass/fail is stated against the ORIGIN row of the same ground, not against an
# absolute -- so a quirk of the rig cannot read as a coordinate effect.
var fails: int = 0
for r in _rows:
var bad: bool = false
if r["pct"] < 99.0:
bad = true
if r["ik_miss"] > 0:
bad = true
if absf(r["ik_err_max"] - _ik_baseline(r["kind"])) > SPHERE_RADIUS * 0.2:
bad = true
if bad:
fails += 1
print("FAIL %s %s travel %.1f%% IK miss %d IK err max %.5f m"
% [r["kind"], r["name"], r["pct"], r["ik_miss"], r["ik_err_max"]])
if fails == 0:
print("PASS every case travels and sweeps cleanly -- coordinate magnitude is NOT the cause")
else:
print("%d of %d cases failed" % [fails, _rows.size()])