OverviewFilesCommitsIssuesDocsReleasesPeers

proc-nodes / demo / glsl_density_gate_dump.gd

75 lines · gd ref:
## Stage 1 of the emitted-DENSITY gate: the CPU's density beside the GLSL the
## engine emits for the same graph.
##
## WHY THIS EXISTS SEPARATELY FROM glsl_op_gate_dump.
##
## The op gate asks "does this node's arithmetic survive emission" and answers it
## at a single position. That cannot see anything about the graph AS A WHOLE --
## which axis the density is read from, whether the emitter honours the frame a
## bridged graph was authored in, whether the value cone reaches the density at
## all. Those are properties of the whole function, and they are where a graph
## that emits 30 correct nodes can still emit a field that is identically zero.
##
## THE INVARIANT: proc_density(p) from the shader must equal
## evaluate_density_batch([p]) from the CPU, for every p. Not "close in
## character" -- the same number. Two paths that disagree are two different
## worlds, and the terrain gets whichever one is running.
##
## Usage: godot --headless --path demo --script res://glsl_density_gate_dump.gd \
##            -- <graph.json> <out-dir>
extends SceneTree

func _init() -> void:
	var a := OS.get_cmdline_user_args()
	if a.size() < 2:
		push_error("need <graph.json> <out-dir>"); quit(1); return
	var g: ProcNodeGraph = GraphImporter.import_from_json(FileAccess.get_file_as_string(a[0]))
	if g == null:
		push_error("import returned null"); quit(1); return

	var glsl: String = g.compile_to_glsl_function("proc_density", false, PackedStringArray())
	if glsl.is_empty():
		# A refusal is a legitimate answer and not this gate's business -- it
		# already printed which node. Say so and leave, rather than writing an
		# empty case stage 2 would report as a compile failure.
		print("DENSITY_GATE_DUMP: EMISSION REFUSED, nothing to compare")
		quit(2); return

	# A LATTICE, not a line. A frame error is a permutation of the axes, and a
	# sample set that is symmetric in two of them cannot see one: points along
	# (t, 0, t) have x == z, so a shader that swapped them would agree
	# everywhere. The steps are deliberately unequal per axis for the same
	# reason.
	var pts := PackedVector3Array()
	for ix in range(5):
		for iy in range(4):
			for iz in range(5):
				pts.append(Vector3(-18.0 + 9.0 * ix, -1.0 + 7.0 * iy, -16.0 + 8.5 * iz))

	var cpu: PackedFloat32Array = g.evaluate_density_batch(pts)
	if cpu.size() != pts.size():
		push_error("density batch returned %d for %d points" % [cpu.size(), pts.size()])
		quit(1); return

	var dir: String = a[1]
	DirAccess.make_dir_recursive_absolute(dir)
	FileAccess.open(dir + "/density.glsl", FileAccess.WRITE).store_string(glsl)
	var manifest := ""
	for i in range(pts.size()):
		manifest += "%d %s %s %s %s\n" % [i,
				String.num(pts[i].x, 9), String.num(pts[i].y, 9),
				String.num(pts[i].z, 9), String.num(cpu[i], 9)]
	FileAccess.open(dir + "/manifest.txt", FileAccess.WRITE).store_string(manifest)
	# The graph's own extent, if it declares one. Reported here because a bounds
	# error is silent downstream -- an inverted box reads as "the terrain is
	# missing", not as a frame error.
	var b: Dictionary = g.get_density_bounds()
	if b.is_empty():
		print("DENSITY_GATE_DUMP: no bounds declared (unbounded field)")
	else:
		print("DENSITY_GATE_DUMP: bounds min=%v max=%v voxel=%.4f"
				% [b["min"], b["max"], b["voxel_size"]])
	print("DENSITY_GATE_DUMP: %d points, blender_frame=%s -> %s"
			% [pts.size(), str(g.get_source_frame() == 1), dir])
	quit()