OverviewFilesCommitsIssuesDocsReleasesPeers

proc-nodes / demo / fog_shader_gate.gd

99 lines · gd ref:
## 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