extends SceneTree
## Does BridgedDensityMaterial turn the bridged mountain's VisualShader into a
## .gdshader that carries the generator's density field AND COMPILES?
##
## godot --headless --path . --script res://scenes/test/bridged_density_splice_gate.gd
##
## ⚠ THE TRANSFORMS LIVE IN scenes/maps/bridged_density_material.gd, NOT HERE.
## This gate owns no copy of them. A test that carries its own transcription of
## the thing it tests can only ever agree with itself -- the most expensive
## recurring bug in this repo, and one that certified a wrong euler order twice.
##
## ⛔ IT DOES NOT GO THROUGH map_flat. Headless, map_flat frees the mountain
## ("MountainTerrain freed (gamestate-sync only)") so the node carries no
## generator and no material. Both texts are built here from the SAME bridged
## scene map_flat reads, which makes this a statement about the ASSET rather
## than about one map's streaming setup.
##
## ⚠ WHAT "COMPILES" MEANS HERE. Headless runs the dummy rasterizer, so this
## exercises Godot's shader-language FRONT END -- which is what produced every
## error found while building this: "Global non-constant variables are not
## supported", "Expected a ',' or ';'", "No matching function found". It does
## NOT hand GLSL to the GPU driver. For that, run the map.
## preload, not the class_name global: a global class is only registered after
## a project --import, and this gate runs standalone via --script.
const BDM := preload("res://scenes/maps/bridged_density_material.gd")
const MOUNTAIN_SCENE := "res://blender/mountain_voronoi2.tscn"
const MOUNTAIN_GRAPH_NODE := ^"Mountain/ProcGeoNodes"
const MOUNTAIN_MESH_NODE := ^"Mountain"
const WORLD_SCALE := 100.0
var OUT := "/tmp/bridged_spliced.gdshader"
var _fail := 0
var _checks := 0
## Kept for _composition(), which cannot run until the tree is up.
var _built_material: ShaderMaterial = null
func _initialize() -> void:
var args := OS.get_cmdline_user_args()
for i in range(args.size() - 1):
if args[i] == "--out": OUT = args[i + 1]
_run()
## ⛔ THE COMPOSITION CHECK RUNS HERE, NOT IN _initialize, AND THAT IS THIS
## GATE'S OWN BUG FIXED. During _initialize the tree is not up: root.add_child()
## leaves the node with is_inside_tree() == false, so a position set on it does
## not take and DCTerrain::terrain_origin() correctly answers ZERO. The check
## then read an identity matrix and blamed the product. Measured -- the gate
## printed `inside_tree=false global_position=(0,0,0)` while the push had
## plainly run (on=1.0). dc-terrain's own gate_terrain_transform.gd records
## exactly this trap about itself; I rebuilt it one repo over.
func _process(_delta: float) -> bool:
_composition()
print("\n%d/%d checks passed" % [_checks - _fail, _checks])
quit(1 if _fail > 0 else 0)
return true
func _ck(name: String, ok: bool, detail: String = "") -> void:
_checks += 1
if not ok:
_fail += 1
print(" %s %s%s" % ["ok " if ok else "FAIL", name,
("" if detail == "" else " [" + detail + "]")])
func _run() -> void:
var ps: PackedScene = load(MOUNTAIN_SCENE)
if ps == null:
_ck("bridged scene loads", false, MOUNTAIN_SCENE)
return
var inst: Node = ps.instantiate()
var mi: Node = inst.get_node_or_null(MOUNTAIN_MESH_NODE)
var holder: Node = inst.get_node_or_null(MOUNTAIN_GRAPH_NODE)
var mat: Material = mi.get("material_override") if mi != null else null
var graph: Resource = holder.get("graph") if holder != null else null
inst.free()
_ck("bridged scene has a graph at %s" % MOUNTAIN_GRAPH_NODE, graph != null)
_ck("bridged material is a ShaderMaterial", mat is ShaderMaterial, str(mat))
_ck("its shader is a VisualShader",
mat is ShaderMaterial and (mat as ShaderMaterial).shader is VisualShader)
var gen = null
if graph != null and ClassDB.class_exists("DCTerrainGeneratorProcNodesBounded"):
gen = ClassDB.instantiate("DCTerrainGeneratorProcNodesBounded")
gen.set_world_scale(WORLD_SCALE)
gen.set_heightfield(true)
gen.set_graph(graph)
_ck("generator compiled the graph", gen != null and gen.has_compiled_shader(),
"" if gen == null else str(gen.get_last_compile_error()))
# The RAW emitted density GLSL, before any of this file's transforms. This
# is what the MESHER compiles, so it is the text to measure the emitter's
# cost against -- measuring the spliced material instead mixes in the macro
# pre-expansion and gives a number that describes neither.
if gen != null:
var rf := FileAccess.open("/tmp/density_raw.glsl", FileAccess.WRITE)
rf.store_string(gen.get_compiled_glsl())
rf.flush()
rf.close()
print("[GATE] wrote /tmp/density_raw.glsl (%d bytes)"
% gen.get_compiled_glsl().length())
# ---- the conversion under test -------------------------------------
# ⚠ TIMED, because headless cannot tell a fast conversion from a fast
# conversion followed by a shader compile that never returns. Run this gate
# WITHOUT --headless (DISPLAY=:0 --rendering-driver vulkan) and the same
# numbers describe the real driver compile.
print("[GATE] compile-begin — the ONE compile that describes what renders")
var t0 := Time.get_ticks_msec()
var built := BDM.build(mat as ShaderMaterial, gen)
var t_build := Time.get_ticks_msec() - t0
print("[GATE] compile-end build() took %d ms" % t_build)
print("[GATE] stage-timings %s" % str(built.get("timings", {})))
if built["material"] is ShaderMaterial:
_built_material = built["material"]
_ck("build() returned a ShaderMaterial", built["material"] is ShaderMaterial,
", ".join(built["log"]))
_ck("macros were pre-expanded", int(built["macros"]) > 0,
"%d invocations" % int(built["macros"]))
_ck("mutable globals were constified", int(built["consts"]) > 0,
"%d" % int(built["consts"]))
_ck("the .gdshader text is non-trivial", String(built["code"]).length() > 100000,
"%d bytes" % String(built["code"]).length())
_ck("it calls evaluate_density from fragment()",
String(built["code"]).contains("evaluate_density(wp.x + e"))
var names: Array[String] = []
if built["material"] != null:
for u in (built["material"] as ShaderMaterial).shader.get_shader_uniform_list():
names.append(str(u["name"]))
_ck("detail-normal uniforms exist on the result",
names.has("dn_blend") and names.has("dn_eps"),
"%d uniforms: %s" % [names.size(), ", ".join(names.slice(0, 8))])
_ck("the bridge's own uniforms survived", names.has("bb_coord_scale"))
# ---- the object-space hook -----------------------------------------
# ⛔ OBJECT SPACE DOES NOT EXIST ON A CHUNKED TERRAIN. Every DCTerrain chunk
# is its own RenderingServer instance with CHUNK-LOCAL vertices, so
# inverse(MODEL_MATRIX) in the bridge's Generated expression returns a
# position within that chunk (0..64 m, resetting at every boundary) rather
# than one spanning the mountain. The material renders as a flat wash and
# NOTHING ERRORS -- hyper-games #83 §6b. blender-bridge v0.161.0 declares
# bb_world_to_object / bb_world_to_object_on for a consumer to supply the
# transform, and DCTerrain pushes it (dc-terrain
# demo/gate_custom_material_object_space.gd, 8/8).
#
# ⚠ THIS CHECKS THE PUBLISHED ASSET. The uniform is emitted by the addon's
# material_serializer.py, and the expression text is BAKED INTO the .tscn at
# publish time -- so changing the emitter does not reach this mountain until
# it is republished from Blender. A failure here means exactly that, and
# says so rather than looking like a code defect.
_ck("the published asset carries the object-space hook",
names.has("bb_world_to_object") and names.has("bb_world_to_object_on"),
"republish mountain_voronoi2 from Blender 5.2 (addon v0.161.0+) -- "
+ "the expression text lives in the .tscn, not in the addon")
_ck("the detail normal samples the density in the TERRAIN frame",
bool(built.get("object_space_hook", false)),
"evaluate_density is terrain-local; map_flat puts the mountain 4 km "
+ "out, so a world position asks about open air beside it")
# ---- ...and the transform itself, WITHOUT waiting for a republish ---
# The two checks above are statements about an ASSET. These are statements
# about THIS FILE, so they hold today: given a source that declares the
# uniform, the emitted body must convert; given one that does not, it must
# fall back to the world position rather than naming an identifier that
# would not compile.
const FAKE_DENSITY := "float evaluate_density(float x, float y, float z) { return 0.0; }"
var with_hook := BDM.splice("shader_type spatial;\n"
+ "uniform mat4 bb_world_to_object = mat4(1.0);\n"
+ "uniform float bb_world_to_object_on = 0.0;\n"
+ "void fragment() {\n}\n", FAKE_DENSITY)
var no_hook := BDM.splice("shader_type spatial;\nvoid fragment() {\n}\n", FAKE_DENSITY)
_ck("a source WITH the uniform converts world -> terrain",
with_hook.contains("bb_world_to_object * vec4(_gwp, 1.0)"))
_ck("a source WITHOUT it falls back to the world position",
no_hook.contains("vec3 wp = _gwp;")
and not no_hook.contains("bb_world_to_object"),
"naming an undeclared uniform would not compile")
var framed := Shader.new()
framed.code = with_hook
_ck("the converted body compiles", framed.get_shader_uniform_list().size() > 0,
"the ternary on mat4 is valid Godot shader language")
# ---- MUTATION: the gate must be able to FAIL -----------------------
# Without this, "17/17" only proves the script ran. Skipping the const
# transform must break the compile -- if it does not, the transform is not
# load-bearing and this gate is measuring nothing.
var vs: VisualShader = (mat as ShaderMaterial).shader
if vs.has_method("_update_shader"):
vs.call("_update_shader")
var no_const := BDM.splice(vs.code,
BDM.expand_function_macros(
gen.get_compiled_glsl())["code"])
print("[GATE] mutation-begin (errors here are the POINT)")
var mut := Shader.new()
mut.code = no_const
var mut_ok := mut.get_shader_uniform_list().size() > 0
print("[GATE] mutation-end")
_ck("MUTATION: dropping constify_globals breaks the compile", not mut_ok,
"it compiled anyway — the transform is not load-bearing")
# ---- #include, with a negative control -----------------------------
# Proves Shader::set_code runs the preprocessor on runtime code with no
# resource path (scene/resources/shader.cpp:107). The FIRST version of this
# check asserted "gained uniforms", and terrain_density.gdshaderinc declares
# ZERO uniforms -- it reported the same 0 either way. A broken ruler.
var inc := Shader.new()
inc.code = "shader_type spatial;\n" \
+ "#include \"res://shaders/terrain_density.gdshaderinc\"\n" \
+ "void fragment() { ALBEDO = vec3(clamp01(0.5)); }\n"
_ck("#include resolves in a runtime Shader with no resource path",
inc.get_shader_uniform_list() != null, "calls clamp01() from the inc")
print("[GATE] negative-control-begin (the error here is the POINT)")
var neg := Shader.new()
neg.code = "shader_type spatial;\n" \
+ "void fragment() { ALBEDO = vec3(clamp01(0.5)); }\n"
var _u = neg.get_shader_uniform_list()
print("[GATE] negative-control-end")
if String(built["code"]) != "":
var f := FileAccess.open(OUT, FileAccess.WRITE)
f.store_string(String(built["code"]))
f.flush()
f.close()
print("\nwrote %s (%d bytes)" % [OUT, String(built["code"]).length()])
func _composition() -> void:
# ---- COMPOSITION: the real terrain, the real asset -----------------
# Two gates passing separately is not the same claim as the two FITTING.
# dc-terrain writes the parameter names in C++; the addon declares them in
# Python; nothing makes a typo on either side show up in that repo's own
# gate, which would agree with itself perfectly. So: a real DCTerrain, placed
# where map_flat places the mountain, handed the REPUBLISHED material -- and
# the transform has to arrive, under the names the shader actually declares.
if _built_material != null and ClassDB.class_exists("DCTerrain") and _built_material is ShaderMaterial:
var t: Node = ClassDB.instantiate("DCTerrain")
t.set("use_gpu", false)
root.add_child(t)
# map_flat: tile (1, 0) * extent, and extent is world_scale * 40.
var tile_at := Vector3(WORLD_SCALE * 40.0, 0.0, 0.0)
(t as Node3D).position = tile_at
t.set("custom_material", _built_material)
var arrived = (_built_material).get_shader_parameter(
"bb_world_to_object")
# ⚠ NOT "is it a Projection". The republished .tscn SAVES
# bb_world_to_object = identity as a shader parameter and build() copies
# the source material's parameters, so the type check passes on the
# COPIED DEFAULT whether dc-terrain pushed or not -- measured. The flag
# is the discriminator: only dc-terrain sets it to 1.0.
var on = (_built_material).get_shader_parameter(
"bb_world_to_object_on")
_ck("DCTerrain reaches the asset's own uniform names",
arrived is Projection and float(on) == 1.0,
"type=%s on=%s inside_tree=%s global_position=%v"
% [type_string(typeof(arrived)), str(on),
str((t as Node3D).is_inside_tree()),
(t as Node3D).global_position])
if arrived is Projection:
# The point it is all for: a world position ON the mountain must come
# back as a position WITHIN the authored box, not 4 km out.
var probe := Vector3(tile_at.x + 137.0, 42.0, -88.0)
var o: Vector4 = (arrived as Projection) * Vector4(probe.x, probe.y, probe.z, 1.0)
_ck("a world point on the mountain lands inside the asset",
absf(o.x - 137.0) < 1e-3 and absf(o.z + 88.0) < 1e-3,
"world %v -> object (%.1f, %.1f, %.1f), and NOT %.0f m out"
% [probe, o.x, o.y, o.z, tile_at.x])
t.queue_free()