Add depth surface processing and layout tools
- Implemented `cutout_depth_surface.py` for face separation and connected thickness calculations for depth surface assets. - Created `cutout_depth_surface_layout.py` to arrange nodes for staggered calculation branches in depth surface processing. - Authored `geometry_layouts.py` to define layouts for various geometry assets, including image cutouts and depth planes. - Introduced `node_layout.py` for shared presentation rules, facilitating node arrangement and validation in geometry-node layouts.
This commit is contained in:
@@ -1,15 +1,20 @@
|
||||
import sys
|
||||
from collections import Counter
|
||||
from itertools import pairwise
|
||||
from pathlib import Path
|
||||
|
||||
import bpy
|
||||
|
||||
PROJECT_ROOT = Path(__file__).parents[2]
|
||||
sys.path.insert(0, str(Path(__file__).parent))
|
||||
sys.path.insert(0, str(PROJECT_ROOT / "src"))
|
||||
sys.path.insert(
|
||||
0,
|
||||
str(next((PROJECT_ROOT / "src" / "anyimage" / "wheels").glob("blendjob-*.whl"))),
|
||||
)
|
||||
|
||||
from node_layout import validate_node_layout
|
||||
|
||||
from anyimage.common.object import (
|
||||
modifier_input_identifier,
|
||||
set_modifier_input,
|
||||
@@ -24,6 +29,130 @@ def evaluated_mesh(obj):
|
||||
return evaluated, result
|
||||
|
||||
|
||||
def validate_depth_surface_layout(group):
|
||||
"""Check the geometry path, stage spacing and drawn node bounds."""
|
||||
nodes = [node for node in group.nodes if node.bl_idname != "NodeReroute"]
|
||||
main = [
|
||||
group.nodes[name]
|
||||
for name in (
|
||||
"Store Named Attribute",
|
||||
"Store Named Attribute.001",
|
||||
"Store Named Attribute.002",
|
||||
"Store Named Attribute.003",
|
||||
"Split Edges",
|
||||
"Switch",
|
||||
"Delete Geometry",
|
||||
"Store Named Attribute.004",
|
||||
"Set Position",
|
||||
"Store Named Attribute.005",
|
||||
"Set Position.001",
|
||||
"Store Named Attribute.006",
|
||||
"Store Named Attribute.007",
|
||||
"Repeat Input",
|
||||
"Extrude Mesh",
|
||||
"Separate Geometry",
|
||||
"Repeat Output",
|
||||
"Flip Faces.001",
|
||||
"Join Geometry.001",
|
||||
"Set Position.003",
|
||||
"Merge by Distance",
|
||||
"Set Position.004",
|
||||
"Switch.002",
|
||||
"Remove Named Attribute",
|
||||
"Set Shade Smooth",
|
||||
"Group Output",
|
||||
)
|
||||
]
|
||||
assert len({node.location.y for node in main}) == 1
|
||||
assert all(right.location.x > left.location.x for left, right in pairwise(main))
|
||||
for left, right in (
|
||||
("Switch", "Delete Geometry"),
|
||||
("Delete Geometry", "Store Named Attribute.004"),
|
||||
("Set Position", "Store Named Attribute.005"),
|
||||
("Switch.002", "Remove Named Attribute"),
|
||||
):
|
||||
a, b = group.nodes[left], group.nodes[right]
|
||||
assert b.location.x - (a.location.x + a.width) >= 280
|
||||
assert not any(n.bl_idname == "FunctionNodeInputString" for n in nodes)
|
||||
assert min(n.location.x for n in nodes) < main[0].location.x
|
||||
|
||||
def height(node):
|
||||
if node.dimensions.y:
|
||||
return node.dimensions.y
|
||||
if node.hide or node.bl_idname == "NodeReroute":
|
||||
return 30.0
|
||||
if node.bl_idname == "GeometryNodeJoinGeometry":
|
||||
return 60.0
|
||||
return node.height
|
||||
|
||||
for index, node in enumerate(nodes):
|
||||
width = node.dimensions.x or node.width
|
||||
for other in nodes[index + 1 :]:
|
||||
other_width = other.dimensions.x or other.width
|
||||
overlap = (
|
||||
node.location.x < other.location.x + other_width
|
||||
and node.location.x + width > other.location.x
|
||||
and node.location.y - height(node) < other.location.y
|
||||
and node.location.y > other.location.y - height(other)
|
||||
)
|
||||
assert not overlap, (node.name, other.name)
|
||||
repeat_input = next(
|
||||
node for node in nodes if node.bl_idname == "GeometryNodeRepeatInput"
|
||||
)
|
||||
repeat_output = repeat_input.paired_output
|
||||
inside = {repeat_output}
|
||||
pending = [repeat_input]
|
||||
while pending:
|
||||
node = pending.pop()
|
||||
if node in inside:
|
||||
continue
|
||||
inside.add(node)
|
||||
pending.extend(link.to_node for link in group.links if link.from_node == node)
|
||||
zone_bottom = min(node.location.y - height(node) for node in inside) - 60.0
|
||||
zone_top = max(node.location.y for node in inside) + 60.0
|
||||
for node in nodes:
|
||||
if repeat_input.location.x <= node.location.x <= repeat_output.location.x:
|
||||
assert (
|
||||
node in inside
|
||||
or node.location.y <= zone_bottom
|
||||
or node.location.y - height(node) >= zone_top
|
||||
), node.name
|
||||
|
||||
|
||||
def validate_depth_surface_attributes(group):
|
||||
"""Check temporary fields are typed, locally read and removed at output."""
|
||||
expected = {
|
||||
"o_depth_face_center": ("FLOAT_VECTOR", "FACE"),
|
||||
"o_depth_face_camera": ("FLOAT_VECTOR", "FACE"),
|
||||
"o_depth_corner_camera": ("FLOAT_VECTOR", "CORNER"),
|
||||
"o_depth_cut_vertex": ("BOOLEAN", "CORNER"),
|
||||
"o_depth_camera": ("FLOAT_VECTOR", "POINT"),
|
||||
"o_depth_surface_position": ("FLOAT_VECTOR", "POINT"),
|
||||
"o_depth_source_vertex": ("INT", "POINT"),
|
||||
"o_depth_wall_layers": ("FLOAT", "POINT"),
|
||||
}
|
||||
stores = {
|
||||
n.inputs["Name"].default_value: (n.data_type, n.domain)
|
||||
for n in group.nodes
|
||||
if n.bl_idname == "GeometryNodeStoreNamedAttribute"
|
||||
}
|
||||
assert stores == expected
|
||||
assert not any(n.bl_idname == "GeometryNodeCaptureAttribute" for n in group.nodes)
|
||||
removes = [n for n in group.nodes if n.bl_idname == "GeometryNodeRemoveAttribute"]
|
||||
assert len(removes) == 1
|
||||
assert removes[0].pattern_mode == "WILDCARD"
|
||||
assert removes[0].inputs["Name"].default_value == "o_depth_*"
|
||||
for node in group.nodes:
|
||||
if node.bl_idname == "GeometryNodeInputNamedAttribute":
|
||||
name = node.inputs["Name"].default_value
|
||||
assert name == "UVMap" or node.data_type == expected[name][0]
|
||||
assert all(
|
||||
abs(link.to_node.location.x - node.location.x) <= 600
|
||||
for link in group.links
|
||||
if link.from_node == node
|
||||
)
|
||||
|
||||
|
||||
def input_names(group):
|
||||
return [
|
||||
item.name
|
||||
@@ -83,6 +212,7 @@ def main():
|
||||
shape: bpy.data.node_groups[name]
|
||||
for shape, name in shapes.CUTOUT_NODE_GROUP_NAMES.items()
|
||||
}
|
||||
validate_depth_surface_layout(groups["DEPTH_SURFACE"])
|
||||
assert all(not group.is_modifier for group in groups.values())
|
||||
|
||||
menu_switch = next(
|
||||
@@ -157,7 +287,7 @@ def main():
|
||||
"Smooth Iterations",
|
||||
"Cleanup",
|
||||
"Smooth",
|
||||
"Stretch Limit",
|
||||
"Split",
|
||||
"Reference Depth",
|
||||
"Depth Direction",
|
||||
"Uniform Scale",
|
||||
@@ -180,7 +310,8 @@ def main():
|
||||
"Geometry",
|
||||
"Thickness",
|
||||
"Depth Scale",
|
||||
"Stretch Limit",
|
||||
"Split",
|
||||
"Cleanup",
|
||||
"Reference Depth",
|
||||
"Uniform Scale",
|
||||
"Depth Image",
|
||||
@@ -200,9 +331,10 @@ def main():
|
||||
)
|
||||
dependencies.setdefault(link.from_socket.name, set()).add(shape)
|
||||
assert dependencies == {
|
||||
"Geometry": {"DEPTH_SURFACE"},
|
||||
"Thickness": set(shapes.CUTOUT_SHAPES),
|
||||
"Depth Scale": {"DEPTH_BALLOON", "DEPTH_SURFACE"},
|
||||
"Stretch Limit": {"DEPTH_SURFACE"},
|
||||
"Split": {"DEPTH_SURFACE"},
|
||||
"Double Sided": {"DEPTH_BALLOON"},
|
||||
"Reference Depth": {"DEPTH_BALLOON", "DEPTH_SURFACE"},
|
||||
"Depth Direction": {"DEPTH_BALLOON"},
|
||||
@@ -232,7 +364,8 @@ def main():
|
||||
)
|
||||
assert all(
|
||||
not any(node.bl_idname == "GeometryNodeInputMeshIsland" for node in group.nodes)
|
||||
for group in groups.values()
|
||||
for shape, group in groups.items()
|
||||
if shape != "DEPTH_SURFACE"
|
||||
)
|
||||
assert any(node.bl_idname == "GeometryNodeBlurAttribute" for node in wrapper.nodes)
|
||||
smooth = next(
|
||||
@@ -252,77 +385,47 @@ def main():
|
||||
)
|
||||
assert cleanup.default_value == 0.0
|
||||
assert cleanup.subtype == "NONE"
|
||||
stretch_limit = next(
|
||||
split_control = next(
|
||||
item
|
||||
for item in wrapper.interface.items_tree
|
||||
if item.item_type == "SOCKET" and item.name == "Stretch Limit"
|
||||
if item.item_type == "SOCKET" and item.name == "Split"
|
||||
)
|
||||
child_stretch_limit = next(
|
||||
child_split = next(
|
||||
item
|
||||
for item in groups["DEPTH_SURFACE"].interface.items_tree
|
||||
if item.item_type == "SOCKET" and item.name == "Stretch Limit"
|
||||
if item.item_type == "SOCKET" and item.name == "Split"
|
||||
)
|
||||
assert stretch_limit.default_value == 0.0
|
||||
assert stretch_limit.min_value == 0.0
|
||||
assert stretch_limit.max_value == 1.0
|
||||
assert stretch_limit.subtype == "FACTOR"
|
||||
assert getattr(stretch_limit.parent, "name", "") == "Options"
|
||||
assert child_stretch_limit.default_value == 0.0
|
||||
assert child_stretch_limit.min_value == 0.0
|
||||
assert child_stretch_limit.max_value == 1.0
|
||||
assert child_stretch_limit.subtype == "FACTOR"
|
||||
assert getattr(child_stretch_limit.parent, "name", "") == "Options"
|
||||
assert split_control.default_value == 0.0
|
||||
assert split_control.min_value == 0.0
|
||||
assert split_control.max_value == 1.0
|
||||
assert split_control.subtype == "FACTOR"
|
||||
assert getattr(split_control.parent, "name", "") == "Options"
|
||||
assert child_split.default_value == 0.0
|
||||
assert child_split.min_value == 0.0
|
||||
assert child_split.max_value == 1.0
|
||||
assert child_split.subtype == "FACTOR"
|
||||
assert getattr(child_split.parent, "name", "") == "Options"
|
||||
depth_surface_nodes = groups["DEPTH_SURFACE"].nodes
|
||||
captures = [
|
||||
node
|
||||
for node in depth_surface_nodes
|
||||
if node.bl_idname == "GeometryNodeCaptureAttribute"
|
||||
]
|
||||
assert len(captures) == 1
|
||||
assert captures[0].domain == "POINT"
|
||||
assert [item.name for item in captures[0].capture_items] == [
|
||||
"Original Edge Length"
|
||||
]
|
||||
captured_distance = next(
|
||||
link.from_node
|
||||
for link in groups["DEPTH_SURFACE"].links
|
||||
if link.to_node == captures[0]
|
||||
and link.to_socket.name == "Original Edge Length"
|
||||
)
|
||||
assert captured_distance.bl_idname == "ShaderNodeVectorMath"
|
||||
assert captured_distance.operation == "DISTANCE"
|
||||
ratio = next(
|
||||
link.to_node
|
||||
for link in groups["DEPTH_SURFACE"].links
|
||||
if link.from_node == captured_distance
|
||||
and link.to_node.bl_idname == "ShaderNodeMath"
|
||||
and link.to_node.operation == "DIVIDE"
|
||||
)
|
||||
assert any(
|
||||
link.from_node == captures[0]
|
||||
and link.to_node == ratio
|
||||
and link.to_socket == ratio.inputs[0]
|
||||
for link in groups["DEPTH_SURFACE"].links
|
||||
node.bl_idname == "GeometryNodeSplitEdges" for node in depth_surface_nodes
|
||||
)
|
||||
assert any(
|
||||
link.from_node == captured_distance
|
||||
and link.to_node == ratio
|
||||
and link.to_socket == ratio.inputs[1]
|
||||
for link in groups["DEPTH_SURFACE"].links
|
||||
)
|
||||
samples = [
|
||||
node
|
||||
for node in depth_surface_nodes
|
||||
if node.bl_idname == "GeometryNodeSampleIndex"
|
||||
]
|
||||
assert not samples
|
||||
delete_domains = [
|
||||
validate_depth_surface_attributes(groups["DEPTH_SURFACE"])
|
||||
assert [
|
||||
node.domain
|
||||
for node in depth_surface_nodes
|
||||
if node.bl_idname == "GeometryNodeDeleteGeometry"
|
||||
] == ["FACE"]
|
||||
assert "Stretch Limit" not in [
|
||||
item.name for item in groups["DEPTH_SURFACE"].interface.items_tree
|
||||
]
|
||||
assert delete_domains.count("EDGE") == 1
|
||||
assert delete_domains.count("POINT") == 2
|
||||
assert all(
|
||||
socket.is_linked or socket.hide
|
||||
for node in depth_surface_nodes
|
||||
for socket in node.outputs
|
||||
)
|
||||
for node in depth_surface_nodes:
|
||||
if node.bl_idname == "GeometryNodeRepeatInput":
|
||||
assert node.paired_output.location.x > node.location.x
|
||||
cleanup_scale = next(
|
||||
link.to_node
|
||||
for link in wrapper.links
|
||||
@@ -342,26 +445,7 @@ def main():
|
||||
)
|
||||
|
||||
for group in (wrapper, *groups.values()):
|
||||
assert all(not node.label for node in group.nodes)
|
||||
assert all(
|
||||
link.from_node.location.x <= link.to_node.location.x for link in group.links
|
||||
)
|
||||
group_nodes = list(group.nodes)
|
||||
assert len({tuple(node.location) for node in group_nodes}) == len(group_nodes)
|
||||
for index, node in enumerate(group_nodes):
|
||||
for other in group_nodes[index + 1 :]:
|
||||
assert not (
|
||||
abs(node.location.x - other.location.x) < 150.0
|
||||
and abs(node.location.y - other.location.y) < 100.0
|
||||
), (group.name, node.name, other.name)
|
||||
for group_input in (
|
||||
node for node in group_nodes if node.bl_idname == "NodeGroupInput"
|
||||
):
|
||||
assert all(
|
||||
output.is_linked or output.hide
|
||||
for output in group_input.outputs
|
||||
if output.name
|
||||
)
|
||||
validate_node_layout(group)
|
||||
for item in group.interface.items_tree:
|
||||
if item.item_type == "SOCKET" and item.in_out == "INPUT":
|
||||
assert item.structure_type == "SINGLE"
|
||||
@@ -525,14 +609,14 @@ def main():
|
||||
|
||||
set_modifier_input(
|
||||
modifier,
|
||||
modifier_input_identifier(modifier.node_group, "Stretch Limit"),
|
||||
modifier_input_identifier(modifier.node_group, "Split"),
|
||||
0.6,
|
||||
)
|
||||
depth_surface.update_tag(refresh={"DATA"})
|
||||
bpy.context.view_layer.update()
|
||||
evaluated, result = evaluated_mesh(depth_surface)
|
||||
try:
|
||||
assert 0 < len(result.polygons) < 8
|
||||
assert len(result.polygons) == 8
|
||||
face_edges = {
|
||||
tuple(sorted((start, end)))
|
||||
for polygon in result.polygons
|
||||
@@ -543,9 +627,7 @@ def main():
|
||||
}
|
||||
assert all(tuple(sorted(edge.vertices)) in face_edges for edge in result.edges)
|
||||
face_vertices = {
|
||||
vertex
|
||||
for polygon in result.polygons
|
||||
for vertex in polygon.vertices
|
||||
vertex for polygon in result.polygons for vertex in polygon.vertices
|
||||
}
|
||||
assert face_vertices == set(range(len(result.vertices)))
|
||||
finally:
|
||||
@@ -565,6 +647,25 @@ def main():
|
||||
finally:
|
||||
evaluated.to_mesh_clear()
|
||||
|
||||
set_modifier_input(
|
||||
modifier, modifier_input_identifier(modifier.node_group, "Depth Scale"), 1.0
|
||||
)
|
||||
set_modifier_input(
|
||||
modifier, modifier_input_identifier(modifier.node_group, "Thickness"), 0.2
|
||||
)
|
||||
depth_surface.update_tag(refresh={"DATA"})
|
||||
bpy.context.view_layer.update()
|
||||
evaluated, result = evaluated_mesh(depth_surface)
|
||||
try:
|
||||
edge_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 edge_uses and set(edge_uses.values()) == {2}
|
||||
finally:
|
||||
evaluated.to_mesh_clear()
|
||||
|
||||
print(
|
||||
f"Validated Blender {bpy.app.version_string}: "
|
||||
f"{wrapper.name} wrapping "
|
||||
|
||||
Reference in New Issue
Block a user