## Does a proc-nodes volume actually compile as a Godot FOG shader?
##
## ⚠ THIS GATE MUST NOT RUN HEADLESS. --headless uses the dummy rendering
## driver, which never compiles a shader, so "no SHADER ERROR" there means
## nothing at all. It is run on a real driver (DISPLAY=:0) and the runner greps
## stderr for SHADER ERROR.
##
## The question it answers: our proc_density is emitted for GLSL 450 -- the same
## text dc-terrain hands glslc -- and a FogVolume needs GODOT SHADING LANGUAGE.
## They overlap; they are not the same. Which graphs land in the intersection is
## a measurement, not a guess.
##
## MEASURED 2026-09-06, on a real vulkan driver:
##
## sdf 1106 chars COMPILES
## simplex 7112 chars COMPILES
## erosion 44921 chars FAILS -- "Global non-constant variables are not
## supported. Expected 'const' keyword"
##
## The first run found two bugs that were NOT language differences:
##
## * `Unknown identifier: 'n1_s0'` -- NoiseTextureNode assigned a hardcoded
## local `noise_val` and never assigned its OUTPUT SOCKET, so any consumer
## resolving it through GlslEmitContext::out() read a name nothing declared.
## That broke the density path too; nothing had consumed the node.
## * `Redefinition of 'TAU'` -- Godot RESERVES TAU as a builtin. The .inc
## declared its own. Renamed PN_TAU.
##
## WHAT IS LEFT, and it is structural rather than a bug: get_erosion_library_glsl
## declares ~18 MUTABLE GLOBALS (pn_ero_gully_depth and friends) so a wrapper can
## set them per chunk. GLSL 450 allows that; Godot shading language does not.
## Making them `const` would break the wrapper; making them `uniform` for the fog
## path would work AND put them on the material's inspector, which is arguably
## where a cloud's knobs belong. Not done -- a cloud does not use the erosion
## filter, so this blocks nothing the feature is for.
extends SceneTree
var _built := false
var _frames := 0
func _volume(kind: String) -> ProcVolume:
var g := ProcNodeGraph.new()
var density := -1
if kind == "sdf":
var s1 := g.add_node(SphereSdfNode.new())
var s2 := g.add_node(SphereSdfNode.new())
var bo := g.add_node(VolumeBooleanNode.new())
g.connect_sockets(s1, 0, bo, 0)
g.connect_sockets(s2, 0, bo, 1)
density = bo
elif kind == "simplex":
# A cloud, roughly: a sphere carved by noise. This is the case that
# pulls a PRELUDE -- proc_simplex.inc, with its #ifndef guard.
var sp := g.add_node(SphereSdfNode.new())
var nz := g.add_node(NoiseTextureNode.new())
var bo := g.add_node(VolumeBooleanNode.new())
g.get_node(bo).set("operation", 2) # subtract
g.connect_sockets(sp, 0, bo, 0)
g.connect_sockets(nz, 0, bo, 1)
density = bo
elif kind == "erosion":
# The case predicted to FAIL: the erosion library declares mutable
# globals at file scope, which Godot shading language rejects.
var gn := g.add_node(GradientNoiseNode.new())
var en := g.add_node(ErosionNoiseNode.new())
g.connect_sockets(gn, 0, en, 7)
g.connect_sockets(gn, 1, en, 8)
density = en
var vtm := g.add_node(VolumeToMeshNode.new())
g.connect_sockets(density, 0, vtm, 0)
# evaluate_socket runs the graph, so this goes through the real execute()
# that fills the ProcVolume -- not a side door built for the test.
var geo = g.evaluate_socket(vtm, 0)
if geo == null or not (geo is ProcGeometry):
return null
return geo.get_volume()
func _process(_d: float) -> bool:
if not _built:
_built = true
for kind in ["sdf", "simplex", "erosion"]:
var vol := _volume(kind)
if vol == null or vol.get_glsl_source().is_empty():
print(" %-10s NO VOLUME (graph did not compile to GLSL)" % kind)
continue
var mat := vol.make_fog_material()
var fv := FogVolume.new()
fv.size = Vector3(64, 64, 64)
fv.material = mat
root.add_child(fv)
print(" %-10s fog shader %d chars, FogVolume in tree -- watch for SHADER ERROR"
% [kind, vol.build_fog_shader_code().length()])
_frames += 1
if _frames > 8:
print("done")
quit()
return true
return false