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)))