Files
Papay-Shooter/debug/fidelity_probe.gd
2026-08-02 02:20:02 -04:00

148 lines
5.2 KiB
GDScript

extends SceneTree
## Deterministic set-piece for judging the LOOK, not the level.
##
## visual_capture.gd shoots the real game, which is the right tool for "does
## this still work" and the wrong one for "did that shader change help": the
## player spawns at a random spawn point, weapons idle-sway, and characters
## animate, so two runs of the same build produce different frames. Measuring
## those with tools/levels.py gives numbers that bounce by 15 and hide every
## delta smaller than the noise.
##
## This builds a fixed scene instead — known primitives on a known ground plane,
## a fixed sun angle, a fixed camera — so the ONLY thing that can move a
## statistic is the change under test. The shapes are chosen to exercise each
## part of the cel look:
##
## sphere / cylinder curved terminator (the band-edge anti-aliasing case)
## box flat faces meeting at hard angles (crisp break case)
## the low slab casts across the ground plane (cast-shadow case)
## the overhang deep occlusion right above a surface (contact/AO case)
## the stack object-on-object contact, the "does it float" case
##
## Run:
## godot --path . --windowed --resolution 1280x720 \
## -s res://debug/fidelity_probe.gd -- <out_dir>
##
## Writes probe_key.png (three-quarter hero angle) and probe_graze.png (low
## raking angle, where terminator crawl and outline weight show worst).
var _frames := 0
var _out_dir := "."
var _root: Node3D
var _cam: Camera3D
func _initialize() -> void:
var args := OS.get_cmdline_user_args()
if args.size() > 0:
_out_dir = args[0]
_build()
func _build() -> void:
_root = Node3D.new()
_root.name = "FidelityProbe"
root.add_child(_root)
LevelEnvironment.add_to(_root, "day")
# Pin the sun rather than take the shared default, so a change to the
# shared default cannot silently move the probe's baseline. Raking enough
# that everything throws a long, legible shadow across the plane.
var sun: DirectionalLight3D = _root.get_node("Sun")
sun.rotation_degrees = Vector3(-34, 42, 0)
sun.light_energy = 2.2
sun.directional_shadow_max_distance = 80.0
# Ground: one large plane, deliberately untextured flat cel colour. This is
# the surface the `detail` statistic is measured against — if a surface
# detail law is doing anything, it shows up here first.
var ground := MeshInstance3D.new()
var plane := PlaneMesh.new()
plane.size = Vector2(60, 60)
ground.mesh = plane
ground.material_override = LevelMaterials.flat(Color(0.52, 0.50, 0.58), "ground")
_root.add_child(ground)
var wall_col := Color(0.62, 0.58, 0.66)
var prop_col := Color(0.80, 0.78, 0.84)
# Back wall — a big flat expanse, the "dead%" case.
_box(Vector3(0, 4, -9), Vector3(30, 8, 0.6), wall_col, "wall")
# Low slab: throws a long shadow across open ground.
_box(Vector3(-6, 0.5, 2), Vector3(4, 1, 1.2), prop_col, "trim")
# Overhang: a lid on two legs, deep occlusion directly above the ground.
_box(Vector3(5, 2.6, 0), Vector3(5, 0.4, 4), prop_col, "panel")
_box(Vector3(3.2, 1.3, -1.4), Vector3(0.4, 2.2, 0.4), prop_col)
_box(Vector3(6.8, 1.3, 1.4), Vector3(0.4, 2.2, 0.4), prop_col)
# Stack: object resting on object, the classic "does it float" read.
_box(Vector3(0, 0.6, 3.5), Vector3(2.4, 1.2, 2.4), prop_col, "panel")
_box(Vector3(0, 1.6, 3.5), Vector3(1.2, 0.8, 1.2), Color(0.86, 0.52, 0.34))
# Curved surfaces: the terminator that crawls if the band edge is not
# anti-aliased in screen space.
_shape(SphereMesh.new(), Vector3(-2.5, 1.2, 0.5), prop_col, 1.2)
var cyl := CylinderMesh.new()
cyl.top_radius = 0.5
cyl.bottom_radius = 0.5
cyl.height = 3.4
_shape(cyl, Vector3(-4.5, 1.7, -3.0), Color(0.42, 0.62, 0.72), 1.0)
_cam = Camera3D.new()
_root.add_child(_cam)
_cam.current = true
_aim_key()
func _box(pos: Vector3, size: Vector3, col: Color, law: String = "") -> void:
var mi := MeshInstance3D.new()
var m := BoxMesh.new()
m.size = size
mi.mesh = m
mi.position = pos
mi.material_override = LevelMaterials.flat(col, law)
_root.add_child(mi)
func _shape(mesh: Mesh, pos: Vector3, col: Color, scale: float) -> void:
var mi := MeshInstance3D.new()
if mesh is SphereMesh:
mesh.radius = 1.0
mesh.height = 2.0
mi.mesh = mesh
mi.position = pos
mi.scale = Vector3.ONE * scale
mi.material_override = LevelMaterials.flat(col)
_root.add_child(mi)
## look_at_from_position, not position-then-look_at: _initialize() runs before
## the node is actually in the tree, and look_at needs a global transform.
func _aim_key() -> void:
_cam.look_at_from_position(Vector3(7.5, 5.2, 12.0), Vector3(0.0, 1.0, 0.5), Vector3.UP)
func _aim_graze() -> void:
_cam.look_at_from_position(Vector3(-9.0, 1.05, 7.5), Vector3(2.0, 1.4, -2.0), Vector3.UP)
func _process(_delta: float) -> bool:
_frames += 1
# 90 frames of settle: shaders compile on first use and the first frames
# render with the fallback material, which would show up as a huge and
# entirely fake improvement in every statistic.
if _frames == 90:
_shot("key")
_aim_graze()
elif _frames == 120:
_shot("graze")
return true
return false
func _shot(tag: String) -> void:
var img := root.get_viewport().get_texture().get_image()
var path := _out_dir + "/probe_" + tag + ".png"
img.save_png(path)
print("fidelity_probe: saved ", path)