## 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()