Files
workinf_Blender_Wasm/tools/web/generate-geometry-node-evaluator-golden.py
mes123456 0fe8d2bb56
Some checks failed
M6 deployable RC / quick (push) Has been cancelled
M6 deployable RC / chromium (push) Has been cancelled
M6 deployable RC / release (push) Has been cancelled
Advance M8-M11 parity workflows
2026-08-17 04:37:07 -04:00

370 lines
15 KiB
Python

import json
import hashlib
import math
import os
import sys
import bpy
ALLOWLIST = [
"NodeGroupInput",
"NodeGroupOutput",
"GeometryNodeTransform",
"GeometryNodeSetPosition",
"GeometryNodeJoinGeometry",
"GeometryNodeSeparateGeometry",
"GeometryNodeRealizeInstances",
"GeometryNodeStoreNamedAttribute",
"FunctionNodeInputInt",
"FunctionNodeInputVector",
"FunctionNodeCompare",
"ShaderNodeValue",
"ShaderNodeMath",
"GeometryNodeObjectInfo",
"GeometryNodeCollectionInfo",
"GeometryNodeImageInfo",
]
def reset_scene():
bpy.ops.wm.read_factory_settings(use_empty=True)
scene = bpy.context.scene
scene.frame_start = 1
scene.frame_end = 24
scene.frame_set(1)
return scene
def mesh_object(name, vertices, faces, location=(0.0, 0.0, 0.0), collection=None):
mesh = bpy.data.meshes.new(f"{name}Mesh")
mesh.from_pydata(vertices, [], faces)
mesh.update()
obj = bpy.data.objects.new(name, mesh)
(collection or bpy.context.collection).objects.link(obj)
obj.location = location
return obj
def cube_object(name, location=(0.0, 0.0, 0.0), collection=None):
return mesh_object(
name,
[
(-1, -1, -1),
(1, -1, -1),
(1, 1, -1),
(-1, 1, -1),
(-1, -1, 1),
(1, -1, 1),
(1, 1, 1),
(-1, 1, 1),
],
[
(0, 1, 2, 3),
(4, 7, 6, 5),
(0, 4, 5, 1),
(1, 5, 6, 2),
(2, 6, 7, 3),
(4, 0, 3, 7),
],
location,
collection,
)
def tetra_object(name, location=(0.0, 0.0, 0.0), collection=None):
return mesh_object(
name,
[(0, 0, 1.5), (-1, -1, 0), (1, -1, 0), (0, 1, 0)],
[(0, 1, 2), (0, 2, 3), (0, 3, 1), (1, 3, 2)],
location,
collection,
)
def geometry_group(name):
group = bpy.data.node_groups.new(name, "GeometryNodeTree")
group.interface.new_socket(name="Geometry", in_out="INPUT", socket_type="NodeSocketGeometry")
group.interface.new_socket(name="Geometry", in_out="OUTPUT", socket_type="NodeSocketGeometry")
group_input = group.nodes.new("NodeGroupInput")
group_output = group.nodes.new("NodeGroupOutput")
group_input.name = "Group Input"
group_output.name = "Group Output"
return group, group_input, group_output
def add_case(name, x, configure):
obj = cube_object(name, (x, 0, 0))
group, group_input, group_output = geometry_group(f"{name}Graph")
configure(group, group_input, group_output)
modifier = obj.modifiers.new(f"{name} Nodes", "NODES")
modifier.node_group = group
return obj, group
def link_geometry(group, source, target):
group.links.new(source.outputs["Geometry"], target.inputs["Geometry"])
def build_fixture(blend_path):
scene = reset_scene()
cases = []
def passthrough(group, group_input, group_output):
group.links.new(group_input.outputs["Geometry"], group_output.inputs["Geometry"])
cases.append(add_case("M10GN_Passthrough", 0, passthrough))
def transform_case(group, group_input, group_output):
node = group.nodes.new("GeometryNodeTransform")
node.name = "Transform"
node.inputs["Translation"].default_value = (0.25, -0.5, 1.0)
node.inputs["Rotation"].default_value = (0.0, 0.0, math.radians(15.0))
node.inputs["Scale"].default_value = (1.25, 0.75, 1.5)
link_geometry(group, group_input, node)
group.links.new(node.outputs["Geometry"], group_output.inputs["Geometry"])
cases.append(add_case("M10GN_Transform", 4, transform_case))
def set_position_case(group, group_input, group_output):
node = group.nodes.new("GeometryNodeSetPosition")
node.name = "Set Position"
node.inputs["Offset"].default_value = (0.5, 0.25, -0.75)
link_geometry(group, group_input, node)
group.links.new(node.outputs["Geometry"], group_output.inputs["Geometry"])
cases.append(add_case("M10GN_SetPosition", 8, set_position_case))
def join_case(group, group_input, group_output):
node = group.nodes.new("GeometryNodeJoinGeometry")
node.name = "Join Geometry"
translated = group.nodes.new("GeometryNodeTransform")
translated.name = "Join Branch Transform"
translated.inputs["Translation"].default_value = (3.0, 0.0, 0.0)
group.links.new(group_input.outputs["Geometry"], node.inputs["Geometry"])
group.links.new(group_input.outputs["Geometry"], translated.inputs["Geometry"])
group.links.new(translated.outputs["Geometry"], node.inputs["Geometry"])
group.links.new(node.outputs["Geometry"], group_output.inputs["Geometry"])
cases.append(add_case("M10GN_Join", 12, join_case))
def separate_case(group, group_input, group_output):
separate = group.nodes.new("GeometryNodeSeparateGeometry")
separate.name = "Separate Geometry"
separate.domain = "POINT"
integer_a = group.nodes.new("FunctionNodeInputInt")
integer_a.name = "Integer A"
integer_a.integer = 2
integer_b = group.nodes.new("FunctionNodeInputInt")
integer_b.name = "Integer B"
integer_b.integer = 1
compare = group.nodes.new("FunctionNodeCompare")
compare.name = "Compare"
compare.data_type = "INT"
compare.operation = "GREATER_THAN"
link_geometry(group, group_input, separate)
group.links.new(integer_a.outputs["Integer"], compare.inputs["A"])
group.links.new(integer_b.outputs["Integer"], compare.inputs["B"])
group.links.new(compare.outputs["Result"], separate.inputs["Selection"])
group.links.new(separate.outputs["Selection"], group_output.inputs["Geometry"])
cases.append(add_case("M10GN_SeparateIntCompare", 16, separate_case))
external_collection = bpy.data.collections.new("M10GN_ExternalCollection")
scene.collection.children.link(external_collection)
tetra_object("M10GN_CollectionTetra", (-1.5, 0, 0), external_collection)
tetra_object("M10GN_CollectionTetraOffset", (1.5, 0, 0), external_collection)
def collection_case(group, _group_input, group_output):
collection_info = group.nodes.new("GeometryNodeCollectionInfo")
collection_info.name = "Collection Info"
collection_info.inputs["Collection"].default_value = external_collection
collection_info.inputs["Separate Children"].default_value = True
realize = group.nodes.new("GeometryNodeRealizeInstances")
realize.name = "Realize Instances"
group.links.new(collection_info.outputs["Instances"], realize.inputs["Geometry"])
group.links.new(realize.outputs["Geometry"], group_output.inputs["Geometry"])
cases.append(add_case("M10GN_CollectionRealize", 20, collection_case))
def store_attribute_case(group, group_input, group_output):
store = group.nodes.new("GeometryNodeStoreNamedAttribute")
store.name = "Store Named Attribute"
store.data_type = "FLOAT"
store.domain = "POINT"
store.inputs["Name"].default_value = "m10_value"
store.inputs["Value"].default_value = 0.375
link_geometry(group, group_input, store)
group.links.new(store.outputs["Geometry"], group_output.inputs["Geometry"])
cases.append(add_case("M10GN_StoreAttribute", 24, store_attribute_case))
def vector_case(group, group_input, group_output):
vector = group.nodes.new("FunctionNodeInputVector")
vector.name = "Vector"
vector.vector = (0.125, 0.5, 1.25)
set_position = group.nodes.new("GeometryNodeSetPosition")
set_position.name = "Set Position"
link_geometry(group, group_input, set_position)
group.links.new(vector.outputs["Vector"], set_position.inputs["Offset"])
group.links.new(set_position.outputs["Geometry"], group_output.inputs["Geometry"])
cases.append(add_case("M10GN_InputVector", 28, vector_case))
def value_math_case(group, group_input, group_output):
first = group.nodes.new("ShaderNodeValue")
first.name = "Value A"
first.outputs["Value"].default_value = 0.25
second = group.nodes.new("ShaderNodeValue")
second.name = "Value B"
second.outputs["Value"].default_value = 0.5
math_node = group.nodes.new("ShaderNodeMath")
math_node.name = "Math Add"
math_node.operation = "ADD"
compare = group.nodes.new("FunctionNodeCompare")
compare.name = "Compare"
compare.data_type = "FLOAT"
compare.operation = "GREATER_THAN"
compare.inputs["B"].default_value = 0.5
set_position = group.nodes.new("GeometryNodeSetPosition")
set_position.name = "Set Position"
set_position.inputs["Offset"].default_value = (0.0, 0.0, 0.625)
link_geometry(group, group_input, set_position)
group.links.new(first.outputs["Value"], math_node.inputs[0])
group.links.new(second.outputs["Value"], math_node.inputs[1])
group.links.new(math_node.outputs["Value"], compare.inputs["A"])
group.links.new(compare.outputs["Result"], set_position.inputs["Selection"])
group.links.new(set_position.outputs["Geometry"], group_output.inputs["Geometry"])
cases.append(add_case("M10GN_ValueMath", 32, value_math_case))
object_target = tetra_object("M10GN_ObjectInfoTarget", (0.5, 0.25, 1.5))
def object_info_case(group, group_input, group_output):
object_info = group.nodes.new("GeometryNodeObjectInfo")
object_info.name = "Object Info"
object_info.inputs["Object"].default_value = object_target
set_position = group.nodes.new("GeometryNodeSetPosition")
set_position.name = "Set Position"
link_geometry(group, group_input, set_position)
group.links.new(object_info.outputs["Location"], set_position.inputs["Offset"])
group.links.new(set_position.outputs["Geometry"], group_output.inputs["Geometry"])
cases.append(add_case("M10GN_ObjectInfo", 36, object_info_case))
image = bpy.data.images.new("M10GN_Image", width=4, height=2, alpha=True)
def image_info_case(group, group_input, group_output):
image_info = group.nodes.new("GeometryNodeImageInfo")
image_info.name = "Image Info"
image_info.inputs["Image"].default_value = image
compare = group.nodes.new("FunctionNodeCompare")
compare.name = "Compare"
compare.data_type = "INT"
compare.operation = "GREATER_THAN"
compare.inputs["B"].default_value = 3
set_position = group.nodes.new("GeometryNodeSetPosition")
set_position.name = "Set Position"
set_position.inputs["Offset"].default_value = (0.0, -0.75, 0.8)
link_geometry(group, group_input, set_position)
group.links.new(image_info.outputs["Width"], compare.inputs["A"])
group.links.new(compare.outputs["Result"], set_position.inputs["Selection"])
group.links.new(set_position.outputs["Geometry"], group_output.inputs["Geometry"])
cases.append(add_case("M10GN_ImageInfo", 40, image_info_case))
bpy.ops.wm.save_as_mainfile(filepath=blend_path, compress=True)
return [obj.name for obj, _group in cases]
def evaluated_mesh_record(obj, depsgraph):
evaluated = obj.evaluated_get(depsgraph)
mesh = evaluated.to_mesh(preserve_all_data_layers=True, depsgraph=depsgraph)
try:
mesh.calc_loop_triangles()
positions = [component for vertex in mesh.vertices for component in vertex.co]
record = {
"object": obj.name,
"vertexCount": len(mesh.vertices),
"triangleCount": len(mesh.loop_triangles),
"positions": positions,
"indices": [index for triangle in mesh.loop_triangles for index in triangle.vertices],
"bounds": {
"min": [min(positions[axis::3]) for axis in range(3)] if positions else [0, 0, 0],
"max": [max(positions[axis::3]) for axis in range(3)] if positions else [0, 0, 0],
},
"attributes": {},
}
attribute = mesh.attributes.get("m10_value")
if attribute is not None:
record["attributes"]["m10_value"] = {
"domain": attribute.domain,
"dataType": attribute.data_type,
"values": [item.value for item in attribute.data],
}
return record
finally:
evaluated.to_mesh_clear()
def write_golden(blend_path, golden_path, case_names):
depsgraph = bpy.context.evaluated_depsgraph_get()
node_coverage = {}
cases = []
for case_name in case_names:
obj = bpy.data.objects[case_name]
group = obj.modifiers[0].node_group
node_types = [node.bl_idname for node in group.nodes]
cases.append({
"name": case_name,
"graph": group.name,
"nodeTypes": node_types,
"mesh": evaluated_mesh_record(obj, depsgraph),
})
for node_type in node_types:
node_coverage.setdefault(node_type, []).append(case_name)
missing = sorted(set(ALLOWLIST) - set(node_coverage))
unexpected = sorted(set(node_coverage) - set(ALLOWLIST))
if missing or unexpected:
raise RuntimeError(f"allowlist coverage mismatch missing={missing} unexpected={unexpected}")
output = {
"schemaVersion": 1,
"blenderVersion": bpy.app.version_string,
"fixture": os.path.relpath(
blend_path,
os.path.dirname(os.path.dirname(os.path.dirname(os.path.dirname(golden_path)))),
),
"fixtureSha256": hashlib.sha256(open(blend_path, "rb").read()).hexdigest(),
"allowlist": ALLOWLIST,
"nodeCoverage": node_coverage,
"tolerance": {
"maxPositionError": 1e-5,
"rmsPositionError": 1e-6,
"maxAttributeError": 1e-6,
"boundsError": 1e-5,
},
"cases": cases,
}
os.makedirs(os.path.dirname(golden_path), exist_ok=True)
with open(golden_path, "w", encoding="utf-8") as handle:
json.dump(output, handle, indent=2, sort_keys=True)
handle.write("\n")
def main():
if "--" not in sys.argv or len(sys.argv[sys.argv.index("--") + 1:]) != 2:
raise SystemExit(
"usage: blender -b --python generate-geometry-node-evaluator-golden.py -- fixture.blend golden.json"
)
blend_path, golden_path = [os.path.abspath(path) for path in sys.argv[sys.argv.index("--") + 1:]]
os.makedirs(os.path.dirname(blend_path), exist_ok=True)
case_names = build_fixture(blend_path)
write_golden(blend_path, golden_path, case_names)
print(f"geometry-node-evaluator-generated fixture={blend_path} golden={golden_path} cases={len(case_names)}")
if __name__ == "__main__":
main()