OverviewFilesCommitsIssuesDocsReleasesPeers

hyper-tps / scenes / test / gate_mountain_asset.gd

141 lines · gd ref:
extends SceneTree
## The mountain asset still reaches dc-terrain: the bridged scene has a graph, the
## graph compiles to a density, and it declares a box the right way up.
##
## WHY A GATE AND NOT A LOOK. Every failure on this path is SILENT and looks like
## something else:
##
##   - blender-bridge rewrites mountain_voronoi1.tscn from the .blend every time
##     the mountain is re-imported. If the node shape changes, `graph` reads null
##     and map_flat gets no mountain, with a warning nobody is watching for.
##   - proc-nodes REFUSES to emit a graph containing a node with no GLSL rather
##     than dropping it. The generator then has no shader and the terrain is
##     EMPTY — which is indistinguishable, in the world, from a missing node.
##   - M negates x, so a bounds pair converted out of Blender's frame is not
##     sorted. An inverted box meshes nothing and reads as "the terrain is gone".
##
## ⛔ IT DOES NOT ASSERT THE FIELD. That is what proc-nodes' own density-parity
## gate is for, and it needs a RenderingDevice; this one runs under --headless
## and asks only whether the asset ARRIVES.
##
##   godot --headless --path . --script res://scenes/test/gate_mountain_asset.gd

const SCENE := "res://blender/mountain_voronoi1.tscn"
const GRAPH_NODE := ^"Mountain/ProcGeoNodes"

var _pass := 0
var _fail := 0
var _done := false


func _check(name: String, ok: bool, note: String = "") -> void:
	if ok:
		_pass += 1
		print("  %-46s PASS  %s" % [name, note])
	else:
		_fail += 1
		print("  %-46s FAIL  %s" % [name, note])


# _process, not _init: an autoload-free SceneTree script has no tree during _init,
# and ClassDB.instantiate on a GDExtension class wants the extension loaded.
func _process(_delta: float) -> bool:
	if _done:
		return true
	_done = true

	_check("the bridged scene exists", ResourceLoader.exists(SCENE), SCENE)
	if not ResourceLoader.exists(SCENE):
		print("\n%d/%d passed" % [_pass, _pass + _fail])
		quit(1)
		return true

	var inst: Node = (load(SCENE) as PackedScene).instantiate()
	var holder: Node = inst.get_node_or_null(GRAPH_NODE)
	_check("the scene still has its graph node", holder != null, String(GRAPH_NODE))
	var graph: Resource = holder.get("graph") if holder != null else null
	_check("the node still carries a graph", graph != null,
			"%d nodes" % (graph.get_node_count() if graph != null and graph.has_method("get_node_count") else 0))
	inst.free()
	if graph == null:
		print("\n%d/%d passed" % [_pass, _pass + _fail])
		quit(1)
		return true

	_check("dc-terrain offers the bounded generator",
			ClassDB.class_exists("DCTerrainGeneratorProcNodesBounded"))
	if not ClassDB.class_exists("DCTerrainGeneratorProcNodesBounded"):
		print("\n%d/%d passed" % [_pass, _pass + _fail])
		quit(1)
		return true

	var gen: Resource = ClassDB.instantiate("DCTerrainGeneratorProcNodesBounded")
	gen.set_graph(graph)
	_check("the graph compiles to a density", gen.has_compiled_shader(),
			gen.get_last_compile_error() if not gen.has_compiled_shader()
			else "%d chars of GLSL" % gen.get_compiled_glsl().length())
	# ⛔ THIS USED TO ASSERT A LITERAL, `proc_density(vec3(x, y, z))`, and dc-terrain
	# c2040ea replaced that emission with a two-step form -- so it went red and said
	# "no rescale on this path" about a generator that had grown one. A gate that
	# names a spelling fails when the code is merely rewritten and passes when the
	# BEHAVIOUR breaks; this asserts the behaviour instead.
	#
	# The contract is f_world(p) = f_node(p / s) * s, and the pair is what matters:
	# #81 recorded that dropping the trailing multiply leaves the surface correct
	# and every distance wrong, which nothing downstream would report.
	_check("at the default scale the field is read in the graph's own units",
			_scale_factors(gen.get_compiled_glsl()) == Vector2(1.0, 1.0),
			"in/out %v" % _scale_factors(gen.get_compiled_glsl()))

	var scaled: Resource = ClassDB.instantiate("DCTerrainGeneratorProcNodesBounded")
	scaled.set_world_scale(100.0)
	scaled.set_graph(graph)
	var f: Vector2 = _scale_factors(scaled.get_compiled_glsl())
	_check("world_scale divides going in and multiplies coming out",
			absf(f.x - 0.01) < 1e-6 and absf(f.y - 100.0) < 1e-3,
			"x100 emitted in=%.6f out=%.3f (want 0.01 and 100)" % [f.x, f.y])
	_check("a scaled box is the authored box times the scale",
			scaled.get_bounds_max().is_equal_approx(gen.get_bounds_max() * 100.0),
			"%v vs %v" % [scaled.get_bounds_max(), gen.get_bounds_max() * 100.0])

	_check("it declares a box", gen.has_bounds())
	if gen.has_bounds():
		var lo: Vector3 = gen.get_bounds_min()
		var hi: Vector3 = gen.get_bounds_max()
		# Sorted, not merely present: M negates x, so an unsorted conversion
		# inverts that axis and an inverted box meshes exactly nothing.
		_check("the box is not inverted", hi.x > lo.x and hi.y > lo.y and hi.z > lo.z,
				"min %v max %v" % [lo, hi])
		var span := hi - lo
		# Blender's up is z and must land on Godot's y. Asserted as a SHAPE, never
		# as a number: a hardcoded height only ever matches the one .blend it was
		# written against.
		_check("Blender's up became Godot's y",
				span.y > span.x * 0.25 and span.y < span.x * 2.0,
				"%.2f tall over a %.2f footprint" % [span.y, span.x])
		_check("it carries its authored voxel size", gen.get_authored_voxel_size() > 0.0,
				"%.3f m" % gen.get_authored_voxel_size())
		# This used to assert the graph's 0.16 against a 0.16 hand-authored on the
		# scene's DCTerrain node, to catch the two drifting apart. map_flat now
		# DERIVES the node's voxel from the graph (authored x world_scale), so there
		# is no second number to drift -- and asserting the old literal would pin
		# the asset to one resolution forever. What still has to hold is that the
		# asset declares one at all, which the check above does.

	print("\n%d/%d passed" % [_pass, _pass + _fail])
	quit(1 if _fail > 0 else 0)
	return true


## The (in, out) scale factors the generator emitted, as `vec3 p = ... * IN` and
## `return d * OUT`. Parsed rather than string-matched so the assertion survives
## the emitter being rewritten again.
func _scale_factors(glsl: String) -> Vector2:
	var re_in := RegEx.create_from_string("vec3\\(x, y, z\\) \\* ([0-9.eE+-]+)")
	var re_out := RegEx.create_from_string("return d \\* ([0-9.eE+-]+)")
	var mi := re_in.search(glsl)
	var mo := re_out.search(glsl)
	if mi == null or mo == null:
		return Vector2(-1.0, -1.0)
	return Vector2(float(mi.get_string(1)), float(mo.get_string(1)))