OverviewFilesCommitsIssuesDocsReleasesPeers

proc-nodes / demo / parity_driver.gd

103 lines · gd ref:
## Evaluates proc-nodes value nodes from a JSON spec, for cross-engine parity tests.
##
## Reads a spec, builds each graph, evaluates the requested output sockets via
## ProcNodeGraph.evaluate_socket, and writes the results as flat float arrays so
## the other side can compare numbers without knowing Godot's types.
##
##   Quaternion -> [x, y, z, w]      Projection -> 16 floats, column-major
##   Vector3    -> [x, y, z]         Color -> [r, g, b, a]
##   float/int/bool -> [v]
##
## Usage: godot --headless --path demo --script res://parity_driver.gd -- <spec> <out>
extends SceneTree


func _flatten(v: Variant) -> Array:
	match typeof(v):
		TYPE_QUATERNION:
			return [v.x, v.y, v.z, v.w]
		TYPE_VECTOR3:
			return [v.x, v.y, v.z]
		TYPE_COLOR:
			return [v.r, v.g, v.b, v.a]
		TYPE_PROJECTION:
			var out := []
			for c in range(4):
				var col: Vector4 = v[c]
				for r in range(4):
					out.append(col[r])
			return out
		TYPE_BOOL:
			return [1.0 if v else 0.0]
		TYPE_INT, TYPE_FLOAT:
			return [float(v)]
	return []


## Convert a JSON default to the socket's DECLARED type.
##
## Guessing from the array length cannot work: a Color and a Rotation are both
## four floats, and a Vector and a Color are told apart only by what the socket
## says it is. Keying on the length silently handed a Color socket a raw Array,
## which reads back as black -- so every colour case compared black against
## Blender and failed for a reason that had nothing to do with the node.
func _to_default(v: Variant, socket_type: int) -> Variant:
	if typeof(v) != TYPE_ARRAY:
		return v
	match socket_type:
		3: # Vector
			if v.size() >= 3:
				return Vector3(v[0], v[1], v[2])
		4: # Color
			if v.size() >= 4:
				return Color(v[0], v[1], v[2], v[3])
			if v.size() == 3:
				return Color(v[0], v[1], v[2], 1.0)
		8: # Rotation
			if v.size() >= 4:
				return Quaternion(v[0], v[1], v[2], v[3])
	if v.size() == 3:
		return Vector3(v[0], v[1], v[2])
	return v


func _init() -> void:
	var args := OS.get_cmdline_user_args()
	if args.size() < 2:
		printerr("need <spec.json> <out.json>")
		quit(1)
		return
	var spec: Array = JSON.parse_string(FileAccess.get_file_as_string(args[0]))
	var results := {}
	for case in spec:
		var g := ProcNodeGraph.new()
		var idx := []
		for nd in case["nodes"]:
			var obj = ClassDB.instantiate(nd["cls"])
			if obj == null:
				printerr("cannot instantiate ", nd["cls"])
				quit(1)
				return
			var i: int = g.add_node(obj)
			idx.append(i)
			# Node PROPERTIES -- gradient_type, operation, input_type. A node
			# whose behaviour is chosen by a property cannot be parity-checked
			# without them, and the driver used to carry defaults only.
			for pk in nd.get("props", {}).keys():
				obj.set(pk, nd["props"][pk])
			for k in nd.get("defaults", {}).keys():
				var si := int(k)
				g.get_node(i).set_input_default(
					si, _to_default(nd["defaults"][k], g.get_node(i).get_input_type(si)))
		for l in case.get("links", []):
			g.connect_sockets(idx[int(l[0])], int(l[1]), idx[int(l[2])], int(l[3]))
		var vals := []
		for rd in case["read"]:
			vals.append_array(_flatten(g.evaluate_socket(idx[int(rd[0])], int(rd[1]))))
		results[case["name"]] = vals
	var f := FileAccess.open(args[1], FileAccess.WRITE)
	f.store_string(JSON.stringify(results))
	f.close()
	print("PARITY_DRIVER wrote ", results.size(), " cases")
	quit()