extends SceneTree
## DOES A DRAWN WIRE CARRY A VALUE?
##
## godot --headless --path . --script res://scenes/test/graph_wire_gate.gd
##
## Until proc-nodes' GPU compiler was given the graph, it did not. It walked
## `nodes[]` in array order and pasted each node's text into one function, so
## `connect_sockets(noise, 0, erosion, 7)` -- the wire both terrain maps draw --
## meant nothing: HeightfieldNode declared a variable called `gn_height` and
## ErosionNoiseNode read one, and the composition was a coincidence of spelling.
## docs/spec-graph-composition.md is the design; this is its acceptance.
##
## THE CONTROL IS AN ADD OF ZERO. Inserting Math(Add, B=0) between the base
## layer and the erosion filter must change the surface by nothing at all, and
## the erosion node must now read the ADD's output rather than the base's. One
## case, and it proves both halves: that the wire is followed, and that
## following it did not perturb the field.
##
## AND A NEGATIVE CONTROL, because a gate that cannot fail is not a gate: the
## same graph with B = 1000 must come out DIFFERENT. Without it, a compiler that
## ignored the Add entirely would pass the first check perfectly.
var _fails: Array[String] = []
var _out_dir := "/tmp"
func _initialize() -> void:
var args := OS.get_cmdline_user_args()
for i in range(args.size() - 1):
if args[i] == "--out-dir":
_out_dir = args[i + 1]
var plain := _compile(_build(false, 0.0), "plain")
var add0 := _compile(_build(true, 0.0), "add_zero")
var add1k := _compile(_build(true, 1000.0), "add_1000")
# 1. all three must emit something at all
for pair in [["plain", plain], ["add_zero", add0], ["add_1000", add1k]]:
if String(pair[1]).is_empty():
_fails.append("%s: emitted NO GLSL" % pair[0])
# 2. the wire is followed: with no Add, erosion reads node 0's outputs
if not plain.contains("vec3(n0_o0, n0_o1.x, n0_o1.z)"):
_fails.append("plain: the erosion node does not read node 0's sockets — " +
"the wire from the base layer was not followed")
# 3. with the Add in the middle, erosion reads the ADD's output (node 2),
# and the Add reads the base. Both halves of one wire.
if not add0.contains("n2_o0 = n0_o0 +"):
_fails.append("add_zero: the Math node does not read node 0's Height output")
if not add0.contains("vec3(n2_o0, n0_o1.x, n0_o1.z)"):
_fails.append("add_zero: the erosion node still reads the base directly — " +
"the Math node was spliced in but nothing was rewired through it")
# 4. THE FIELD IS UNCHANGED. Adding zero is an identity, so every line of
# the two shaders must match except the Math node's own block.
var d0 := _diff_lines(plain, add0)
for line in d0:
var t := String(line).strip_edges()
if t.is_empty():
continue
if t.contains("Math") or t.contains("n2_o0") or t == "{" or t == "}":
continue
if t.contains("n2_o0, n0_o1.x") or t.contains("n0_o0, n0_o1.x"):
continue # the one call site that must change, checked above
_fails.append("add_zero: adding zero changed a line it should not have: %s" % t)
# 5. NEGATIVE CONTROL — the same insertion with a real value must differ.
if add0 == add1k:
_fails.append("NEGATIVE CONTROL FAILED: Add(0) and Add(1000) emit identical " +
"GLSL, so this gate cannot tell whether the Add is evaluated at all")
# The texts, for glslc to compile outside this process.
for pair in [["plain", plain], ["add_zero", add0], ["add_1000", add1k]]:
var f := FileAccess.open("%s/wire_%s.glsl" % [_out_dir, pair[0]], FileAccess.WRITE)
if f != null:
# A COMPILABLE UNIT, not just the function. Without #version glslc
# refuses before it reaches our text at all, and reports an internal
# error about built-ins -- which reads like a shader fault and is not
# one. The main() is there so the function is actually referenced.
# THE AMBIENT HOST INPUTS, both of them. dc-terrain supplies
# voxel_scale as a uniform ahead of the injected body and
# pn_max_octaves as a wrapper local; a standalone unit has neither,
# and ErosionNoiseNode reads voxel_scale to compute its own Nyquist
# limit. Without this the file does not compile and the failure is
# about the harness, not about the graph.
f.store_string("#version 450\n" +
"layout(local_size_x = 1) in;\n" +
"int pn_max_octaves = 99;\n" +
"float voxel_scale = 0.0;\n" + String(pair[1]) +
"\nvoid main() { proc_density(vec3(1.0, 2.0, 3.0)); }\n")
f.close()
if _fails.is_empty():
print("PASS — the wire carries the value, and adding zero changed nothing.")
print(" wrote %s/wire_{plain,add_zero,add_1000}.glsl for glslc" % _out_dir)
quit(0)
else:
print("FAIL — %d problem(s):" % _fails.size())
for m in _fails:
print(" " + m)
quit(1)
## node 0 = GradientNoise, node 1 = Erosion, node 2 = Math(Add) when present.
## Node 2 is added LAST on purpose: array order would put it after the erosion
## node, so if it comes out in the right place it is the topological sort that
## put it there, not the order it was added.
func _build(with_add: bool, addend: float) -> ProcNodeGraph:
var g := ProcNodeGraph.new()
var noise := g.add_node(GradientNoiseNode.new())
g.get_node(noise).set_input_default(1, 3.0)
g.get_node(noise).set_input_default(2, 3)
g.get_node(noise).set_input_default(3, 0.1)
var erosion := g.add_node(ErosionNoiseNode.new())
g.get_node(erosion).set_input_default(1, 5.0)
g.get_node(erosion).set_input_default(2, 0.15)
g.get_node(erosion).set_input_default(4, 0.7)
g.get_node(erosion).set_input_default(5, 7)
g.get_node(erosion).set_input_default(6, 0.5)
g.connect_sockets(noise, 1, erosion, 8) # Slope, direct in both cases
if with_add:
var add := g.add_node(MathNode.new())
g.get_node(add).set("operation", 0) # MathOp::Add
g.get_node(add).set_input_default(1, addend)
g.connect_sockets(noise, 0, add, 0) # Height -> A
g.connect_sockets(add, 0, erosion, 7) # Result -> external height
else:
g.connect_sockets(noise, 0, erosion, 7)
return g
func _compile(g: ProcNodeGraph, _label: String) -> String:
return g.compile_to_glsl_function("proc_density", true)
## Lines present in b but not in a.
func _diff_lines(a: String, b: String) -> Array:
var seen := {}
for l in a.split("\n"):
seen[l] = int(seen.get(l, 0)) + 1
var only := []
for l in b.split("\n"):
if int(seen.get(l, 0)) > 0:
seen[l] = int(seen[l]) - 1
else:
only.append(l)
return only