feat(node_assets): add image plane geometry nodes and validation scripts
- Implemented `image_plane.py` to build the Image Plane Geometry Nodes group with customizable parameters for geometry, subdivide, and thickness. - Created `image_plane_validation.py` to validate the functionality of the image plane, ensuring correct geometry generation and material assignment. - Developed `material_layers.py` to construct shader node groups for image materials, including depth and normal handling. - Authored `plane_layouts.py` to define node arrangements for image geometry assets. - Introduced `shadeless.py` to create a shadeless shader node group that displays color without scene lighting while preserving shadows and reflections.
This commit is contained in:
93
tools/node_assets/cutout_validation.py
Normal file
93
tools/node_assets/cutout_validation.py
Normal file
@@ -0,0 +1,93 @@
|
||||
"""Validate direct Cutout assets and their evaluated defaults."""
|
||||
from pathlib import Path
|
||||
import sys
|
||||
from collections import Counter
|
||||
import bpy
|
||||
import numpy as np
|
||||
|
||||
ROOT = Path(__file__).resolve().parents[2]
|
||||
sys.path.insert(0, str(ROOT / "src"))
|
||||
sys.path.insert(0, str(Path(__file__).parent))
|
||||
sys.path.insert(
|
||||
0,
|
||||
str(next((ROOT / "src" / "anyimage" / "wheels").glob("blendjob-*.whl"))),
|
||||
)
|
||||
from anyimage.operators.cutout_tool.object import load_cutout_node_group
|
||||
from anyimage.common.object import modifier_input_identifier, set_modifier_input
|
||||
from node_layout import validate_node_layout
|
||||
|
||||
|
||||
def main():
|
||||
bpy.ops.wm.read_factory_settings(use_empty=True)
|
||||
groups = {shape: load_cutout_node_group(shape) for shape in ("FLAT", "SOLID", "DEPTH_SOLID", "DEPTH_BALLOON")}
|
||||
assert groups["FLAT"] == groups["SOLID"]
|
||||
assert len(set(groups.values())) == 3
|
||||
for group in set(groups.values()):
|
||||
validate_node_layout(group)
|
||||
assert group.is_modifier and group.get("anyimage_asset_revision") == 11
|
||||
assert not any(node.bl_idname == "GeometryNodeGroup" for node in group.nodes)
|
||||
inputs = {item.name: item for item in group.interface.items_tree if item.item_type == "SOCKET" and item.in_out == "INPUT"}
|
||||
assert "Shape" not in inputs and "Rim Thickness" not in inputs
|
||||
assert next(s for s in group.interface.items_tree if s.name == "Thickness").default_value == 1
|
||||
assert all(item.structure_type == "SINGLE" for item in inputs.values())
|
||||
if "Mode" in inputs:
|
||||
assert inputs["Thickness"].subtype == "DISTANCE"
|
||||
assert inputs["Mode"].default_value == "Balloon"
|
||||
if "Double Sided" in inputs:
|
||||
assert inputs["Double Sided"].default_value
|
||||
if group == groups["DEPTH_SOLID"]:
|
||||
assert np.isclose(inputs["Edge Round"].default_value, 0.2)
|
||||
assert inputs["Edge Round"].parent.name == "Options"
|
||||
assert list(inputs)[list(inputs).index("Front Inflation") - 1] == "Edge Round"
|
||||
assert inputs["Normal Smooth"].default_value == 50
|
||||
assert inputs["Front Inflation"].default_value == 1
|
||||
assert inputs["Front Inflation"].parent.name == "Options"
|
||||
assert not set(inputs) & {"Edge Round Range", "Back Rim Range", "Normal Influence", "Edge Round Amount", "Back Rim Fullness"}
|
||||
options = [s.name for s in inputs.values() if s.parent and s.parent.name == "Options"]
|
||||
expected = ["Cleanup Threshold"]
|
||||
if group == groups["DEPTH_SOLID"]:
|
||||
expected += ["Split Threshold", "Reference Depth", "Edge Round", "Front Inflation", "Normal Smooth"]
|
||||
elif group == groups["DEPTH_BALLOON"]:
|
||||
expected += ["Reference Depth"]
|
||||
expected += ["Smooth Weight"]
|
||||
if group == groups["DEPTH_BALLOON"]:
|
||||
expected += ["Depth Direction"]
|
||||
assert options == expected, (group.name, options)
|
||||
assert inputs["Smooth"].socket_type == "NodeSocketInt" and inputs["Smooth"].default_value == 0
|
||||
assert inputs["Smooth Weight"].default_value == 1
|
||||
incoming = [link for link in group.links if link.from_node.bl_idname == "NodeGroupInput" and link.from_socket.name == "Geometry"]
|
||||
assert len(incoming) == 1 and incoming[0].to_node.bl_idname == "GeometryNodeDeleteGeometry"
|
||||
mesh = bpy.data.meshes.new("Profile")
|
||||
mesh.from_pydata([(-1,-1,0), (1,-1,0), (1,1,0), (-1,1,0), (0,0,0)], [], [(0,1,4), (1,2,4), (2,3,4), (3,0,4)])
|
||||
profile = mesh.attributes.new("o_balloon", "FLOAT", "POINT")
|
||||
profile.data.foreach_set("value", [0,0,0,0,0.6])
|
||||
obj = bpy.data.objects.new("Profile", mesh)
|
||||
bpy.context.collection.objects.link(obj)
|
||||
modifier = obj.modifiers.new("Cutout", "NODES")
|
||||
modifier.node_group = groups["SOLID"]
|
||||
for mode, thickness in ((0, 0), (0, 1), (1, 0), (1, 0.4)):
|
||||
for name, value in (("Mode", mode), ("Thickness", thickness)):
|
||||
subtype = ("DISTANCE" if mode else "NONE") if name == "Thickness" else None
|
||||
set_modifier_input(modifier, modifier_input_identifier(modifier.node_group, name, subtype=subtype), value)
|
||||
obj.update_tag(refresh={"DATA"})
|
||||
bpy.context.view_layer.update()
|
||||
evaluated = obj.evaluated_get(bpy.context.evaluated_depsgraph_get())
|
||||
result = evaluated.to_mesh()
|
||||
try:
|
||||
z = np.array([vertex.co.z for vertex in result.vertices])
|
||||
assert np.isfinite(z).all() and len(z)
|
||||
if thickness == 0:
|
||||
assert len(result.polygons) == 4 and np.max(abs(z)) < 1e-7
|
||||
assert all(abs(item.value - 1) < 1e-7 for item in result.attributes["o_normal_scale"].data)
|
||||
else:
|
||||
radius = 0.6 if mode == 0 else 0.2
|
||||
assert np.isclose(z.min(), -radius) and np.isclose(z.max(), radius)
|
||||
uses = Counter(tuple(sorted((a,b))) for face in result.polygons for a,b in zip(face.vertices, (*face.vertices[1:],face.vertices[0])))
|
||||
assert set(uses.values()) == {2}
|
||||
finally:
|
||||
evaluated.to_mesh_clear()
|
||||
print("Validated three Cutout assets, four presets, both thickness modes and closed defaults")
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
main()
|
||||
Reference in New Issue
Block a user