OverviewFilesCommitsIssuesDocsReleasesPeers

proc-nodes / demo / bake_bridged_graph.gd

84 lines · gd ref:
## Bake a bridged Blender graph against its REAL cage, and save the result.
##
## The end-to-end path: a .blend's geometry tree and the object's own mesh both
## cross, proc-nodes evaluates the graph in Blender's frame with that mesh on
## Group Input, and the finished mesh is converted once. What comes out should
## be the mesh Blender shows.
##
## Args: <graph.json> <cage.json> <out-dir>
extends SceneTree

func _init() -> void:
	var a := OS.get_cmdline_user_args()
	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
	print("graph: %d nodes, %d connections" % [g.get_node_count(), g.get_connection_count()])

	var cage: Dictionary = JSON.parse_string(FileAccess.get_file_as_string(a[1]))
	var verts := PackedVector3Array()
	for p in cage["positions"]:
		verts.append(Vector3(p[0], p[1], p[2]))
	var idx := PackedInt32Array()
	for i in cage["indices"]:
		idx.append(int(i))
	var arr := []
	arr.resize(Mesh.ARRAY_MAX)
	arr[Mesh.ARRAY_VERTEX] = verts
	arr[Mesh.ARRAY_INDEX] = idx
	var cage_mesh := ArrayMesh.new()
	cage_mesh.add_surface_from_arrays(Mesh.PRIMITIVE_TRIANGLES, arr)
	print("cage: %d verts, %d triangles" % [verts.size(), idx.size() / 3])

	var host := ProcGeometryNode.new()
	# GPU on/off from the command line. It was hardcoded false, so this harness
	# could only ever measure the CPU path -- and measuring the CPU path to
	# explain why a node ran on the CPU proves nothing.
	host.use_gpu = a.size() > 3 and a[3] == "gpu"
	host.set_input_mesh(cage_mesh)
	host.set_graph(g)
	get_root().add_child(host)

	var t0 := Time.get_ticks_msec()
	var m: ArrayMesh = host.bake_mesh()
	var ms := Time.get_ticks_msec() - t0
	if m == null or m.get_surface_count() == 0:
		print("BAKE: no mesh"); quit(1); return
	var v: PackedVector3Array = m.surface_get_arrays(0)[Mesh.ARRAY_VERTEX]
	var lo := Vector3(INF, INF, INF)
	var hi := Vector3(-INF, -INF, -INF)
	for p in v:
		lo = lo.min(p); hi = hi.max(p)
	print("BAKE: %d verts in %d ms  x %.3f..%.3f  y %.3f..%.3f  z %.3f..%.3f"
		% [v.size(), ms, lo.x, hi.x, lo.y, hi.y, lo.z, hi.z])

	# Moments as well as bounds: two surfaces can share a bounding box and be
	# different shapes. Reported in BLENDER's axes -- M maps our y back to
	# Blender's z -- so the two are directly comparable.
	#
	# ⛔ ACCUMULATE IN PLAIN FLOATS, NOT A Vector3. Vector3's components are
	# real_t = float32, and summing 2.58M heights into one reaches ~2.8e7 where
	# a float32 ulp is ~2. Measured on the voronoi mountain: the Vector3 form
	# reported sum(z)=1792324.375 and sumsq(z)=28291398 where the true values
	# are 1792386.704 and 28382639 -- an instrument error of 62 and 91241,
	# against a real disagreement with Blender of 7.05 and 90.9. The accumulator
	# was an order of magnitude noisier than the signal it exists to detect, so
	# it read as a failing comparison on a mesh that matches. GDScript's own
	# float IS a double; the sum belongs in one.
	var s1z := 0.0
	var s2z := 0.0
	for p in v:
		var bz := float(p.y) # M is self-inverse; Blender z is our y
		s1z += bz
		s2z += bz * bz
	print("MOMENTS   sum(z)=%.4f sumsq(z)=%.4f" % [s1z, s2z])

	# The graph as a Godot resource, so it opens in the proc-nodes editor, and
	# the baked mesh, so it can be dropped into a scene.
	var out_dir: String = a[2]
	DirAccess.make_dir_recursive_absolute(out_dir)
	ResourceSaver.save(g, out_dir.path_join("mountain_graph.tres"))
	ResourceSaver.save(m, out_dir.path_join("mountain_baked.res"))
	print("SAVED: %s" % out_dir)
	quit()