OverviewFilesCommitsIssuesDocsReleasesPeers

hyper-tps / scenes / test / gate_undersea_geiss_graph.gd

189 lines · gd ref:
extends SceneTree
## GATE — our Geiss density graph, as REBUILT 2026-10-06 (undersea_geiss_graph.gd header:
## masks -> pre-warp -> ground + hard floor -> shelf -> continuous terraces -> ridges ->
## eight octaves). The previous checks tested the replaced design (coast mask, a 10-100 m
## random floor staircase, `warp` / `floor_strength` knobs) and no longer parsed.
##   U1  the shipped graph (Position) compiles to one GLSL proc_density
##   U2  the bridged scene's graph compiles to proc_density and labels its G term
##   U3  CONTINUOUS: along y the field never jumps (the old sawtooth floors did, and
##       meshed as floating razor sheets) — |Δd| over 1 cm steps stays small
##   U4  air far above the ground plane, solid far below; every value finite
##   U5  G == d + p_rel.y exactly (the field without its flat ground plane)
##   U6  prewarp = 0 -> wp == p_rel (no warp); octave_gain = 0 -> d loses exactly oct
##   U7  detail_of() cuts exactly G (vs the builder's DETAIL mode); the scene's term
##       compiles
##   U8  the KNOBS are labelled nodes that survive a save, and a tuned one changes d
##   U9  install_detail() gives a real DCTerrainGeneratorProcNodes the scene's term
## Sampled on the CPU through evaluate_socket on a graph built by the SAME builder with
## a constant Vector in place of Position.
##
## Run: CI=true HYPERTPS_DAEMON_TCP=127.0.0.1:1 "$HYPER_TPS_GODOT" --headless --path . --script res://scenes/test/gate_undersea_geiss_graph.gd


var ok := true
var checks := 0


func _p(c: bool, m: String, got: Variant = "") -> void:
	checks += 1
	print(("ok   " if c else "FAIL: ") + m + ("" if str(got) == "" else "  (" + str(got) + ")"))
	if not c:
		ok = false


func _init() -> void:
	_run()
	print("\n%d checks — %s" % [checks, "PASS" if ok else "FAIL"])
	quit(0 if ok else 1)


## A sampler: the builder with a constant Vector; move it with at().
class Sampler:
	var b := UnderseaGeissGraph.new()
	var ids: Dictionary
	func _init(knobs: Dictionary = {}, mode: String = UnderseaGeissGraph.FULL) -> void:
		ids = b.build(ProcNodeGraph.new(), Vector3.ZERO, knobs, mode)
	func at(p: Vector3, what: String = "out", sock: int = 0) -> float:
		b.g.get_node(ids["p"]).set("value", p)
		return float(b.g.evaluate_socket(ids[what], sock))


func _run() -> void:
	# ---- U1 ----
	var shipped := UnderseaGeissGraph.make()
	var glsl: String = shipped.compile_to_glsl_function("proc_density", false, PackedStringArray())
	_p(glsl.contains("proc_density") and glsl.length() > 500, "U1 the graph compiles to proc_density", glsl.length())
	# ---- U2 ----
	var saved := UnderseaGeissGraph.load_graph()
	_p(saved != null, "U2 %s holds a graph at %s" % [UnderseaGeissGraph.SCENE, UnderseaGeissGraph.HOLDER])
	if saved != null:
		var g2: String = saved.compile_to_glsl_function("proc_density", false, PackedStringArray())
		_p(g2.contains("float proc_density") and g2.length() > 500, "U2 the scene's graph compiles to proc_density", g2.length())
		var has_detail := false
		for i in saved.get_node_count():
			var n: ProcNode = saved.get_node(i)
			if n != null and String(n.get("label")) == UnderseaGeissGraph.DETAIL_LABEL:
				has_detail = true
		_p(has_detail, "U2 the scene's graph labels its Geiss term '%s'" % UnderseaGeissGraph.DETAIL_LABEL)

	var s := Sampler.new()
	var rng := RandomNumberGenerator.new()
	rng.seed = 7
	# ---- U3 ----
	var jump := 0.0
	var jump_at := Vector3.ZERO
	for c in 6:
		var x := rng.randf_range(-400, 400)
		var z := rng.randf_range(-400, 400)
		var prev := s.at(Vector3(x, -60.0, z))
		var y := -60.0
		while y < 60.0:
			y += 0.01
			var v := s.at(Vector3(x, y, z))
			if absf(v - prev) > jump:
				jump = absf(v - prev)
				jump_at = Vector3(x, y, z)
			prev = v
	_p(jump < 0.5, "U3 continuous along y: largest change over 1 cm %.3f (a terrace step is >= terrace_str 3)" % jump, jump_at)
	# ---- U4 ----
	var air := 0
	var solid := 0
	var nonfinite := 0
	for i in 60:
		var x2 := rng.randf_range(-900, 900)
		var z2 := rng.randf_range(-900, 900)
		var up := s.at(Vector3(x2, 300.0, z2))
		var dn := s.at(Vector3(x2, -300.0, z2))
		if is_nan(up) or is_inf(up) or is_nan(dn) or is_inf(dn):
			nonfinite += 1
		if up < 0.0:
			air += 1
		if dn > 0.0:
			solid += 1
	_p(nonfinite == 0 and air == 60 and solid == 60, "U4 air 300 m above / solid 300 m below the ground plane, all finite",
		"air %d solid %d nonfinite %d" % [air, solid, nonfinite])
	# ---- U5 ----
	var gworst := 0.0
	for i in 40:
		var p := Vector3(rng.randf_range(-900, 900), rng.randf_range(-80.0, 80.0), rng.randf_range(-900, 900))
		gworst = maxf(gworst, absf(s.at(p, "G") - (s.at(p) + p.y - float(UnderseaGeissGraph.KNOBS["ground_y"]))))
	_p(gworst < 1e-3, "U5 G == d + p_rel.y", gworst)
	# ---- U6 ----
	var nowarp := Sampler.new({"prewarp": 0.0})
	var wworst := 0.0
	for i in 20:
		var p := Vector3(rng.randf_range(-900, 900), rng.randf_range(-80.0, 80.0), rng.randf_range(-900, 900))
		nowarp.b.g.get_node(nowarp.ids["p"]).set("value", p)
		var wp: Vector3 = nowarp.b.g.evaluate_socket(nowarp.ids["wp"], 0)
		wworst = maxf(wworst, wp.distance_to(p))    # ground_y 0: p_rel == p
	_p(wworst < 1e-3, "U6 prewarp = 0: the warped coordinate is p itself", wworst)
	var nooct := Sampler.new({"octave_gain": 0.0})
	var oworst := 0.0
	for i in 20:
		var p := Vector3(rng.randf_range(-900, 900), rng.randf_range(-80.0, 80.0), rng.randf_range(-900, 900))
		oworst = maxf(oworst, absf(nooct.at(p) - (s.at(p) - s.at(p, "oct"))))
	_p(oworst < 1e-3, "U6 octave_gain = 0 removes exactly the octave term", oworst)
	# ---- U7 ----
	var dg := UnderseaGeissGraph.make(null, {}, UnderseaGeissGraph.DETAIL)
	var dglsl: String = dg.compile_to_glsl_function("undersea_detail", false, PackedStringArray())
	_p(dglsl.contains("float undersea_detail"), "U7 the detail graph compiles", dglsl.length())
	var sdet := UnderseaGeissGraph.detail_of(saved)
	_p(sdet != null and sdet.compile_to_glsl_function("undersea_detail", false, PackedStringArray()).contains("float undersea_detail"),
		"U7 detail_of(the scene's graph) compiles to undersea_detail")
	var det := Sampler.new({}, UnderseaGeissGraph.DETAIL)
	# detail_of on a SAMPLER's full graph: the copy keeps node indices (the bridge JSON
	# round trip is exact), so the constant Vector is still ids["p"].
	var cut := UnderseaGeissGraph.detail_of(s.b.g)
	var cworst := 0.0
	for i in 40:
		var p := Vector3(rng.randf_range(-900, 900), rng.randf_range(-80.0, 80.0), rng.randf_range(-900, 900))
		cut.get_node(s.ids["p"]).set("value", p)
		det.b.g.get_node(det.ids["p"]).set("value", p)
		cworst = maxf(cworst, absf(float(cut.evaluate_socket(_output_source(cut), 0)) - det.at(p)))
	_p(cut != null and cworst < 1e-4, "U7 detail_of cuts exactly G (vs the builder's DETAIL graph)", cworst)
	# ---- U8 ----
	var shipped_knobs := UnderseaGeissGraph.knobs_from(saved)
	var missing: Array = []
	for key: String in UnderseaGeissGraph.KNOBS:
		if not shipped_knobs.has(key):
			missing.append(key)
	_p(missing.is_empty(), "U8 the scene's graph carries every knob as a labelled node (%d)" % shipped_knobs.size(), missing)
	var tuned := UnderseaGeissGraph.make(null, {"shelf_strength": 3.0})
	var tmp := "user://gate_undersea_knobs.tres"
	ResourceSaver.save(tuned, tmp)
	var back := UnderseaGeissGraph.knobs_for(load(tmp) as ProcNodeGraph)
	_p(is_equal_approx(float(back["shelf_strength"]), 3.0) and is_equal_approx(float(back["prewarp"]), float(UnderseaGeissGraph.KNOBS["prewarp"])),
		"U8 a tuned knob saved and read back is the tuned value; the rest are unchanged", back["shelf_strength"])
	var ts := Sampler.new({"shelf_strength": 3.0})
	var diff := 0
	for i in 20:
		var p := Vector3(rng.randf_range(-900, 900), rng.randf_range(-6.0, 4.0), rng.randf_range(-900, 900))
		if absf(ts.at(p) - s.at(p)) > 0.01:
			diff += 1
	_p(diff > 2, "U8 the tuned knob changes the field in the shelf band (%d / 20 samples)" % diff)
	DirAccess.remove_absolute(ProjectSettings.globalize_path(tmp))
	# ---- U9 ----
	var gen: Object = ClassDB.instantiate("DCTerrainGeneratorProcNodes")
	_p(gen != null, "U9 DCTerrainGeneratorProcNodes is registered")
	if gen != null:
		# A main graph first, as the maps do: the generator compiles the undersea term
		# only as part of its own shader, and only once that shader compiled. Any
		# GPU-compilable graph serves (graphs/terrain.tres is NOT one: SlopeGradient
		# has no GPU path), so the scene's own graph stands in.
		gen.call("set_graph", saved)
		var why := UnderseaGeissGraph.install_detail(gen)
		_p(why == "" and gen.call("get_undersea_graph") != null, "U9 install_detail gives the generator the scene's term", why)


## The node feeding a graph's Group Output (socket 0) — what a density evaluates.
func _output_source(g: ProcNodeGraph) -> int:
	var go := -1
	for i in g.get_node_count():
		if g.get_node(i) is GroupOutputNode:
			go = i
	var c := g.get_connections_flat()
	for i in range(0, c.size(), 4):
		if c[i + 2] == go and c[i + 3] == 0:
			return c[i]
	return -1