Add new functionality for image rectification and depth metadata handling

- Implemented a preview tool for rectifying images with interactive aspect controls in `preview.py`.
- Added tests for depth metadata loading to ensure consistent schema usage in `test_depth_metadata.py`.
- Developed comprehensive tests for cutout calibration processes in `test_cutout_calibration.py`, including stability checks and depth calibration.
- Created tests for frame compositing operations in `test_frame_compositing.py`, ensuring correct handling of depth and transparency.
- Introduced material slot inheritance functionality in `material_slots.py` to manage geometry material assignments.
- Built shared shader nodes for material processing in `shader.py`, including roughness and bump mapping setups.
This commit is contained in:
2026-09-12 19:20:06 +08:00
parent e8268504c2
commit 938d95db0f
175 changed files with 3812 additions and 6864 deletions

View File

@@ -4,67 +4,36 @@ import bpy
import numpy as np
import pytest
from tools.nodes.groups.image_cutout import build_image_cutout_group
from tests.support.nodes import CUTOUT_BUILDERS
from tools.nodes.arrangement.flow import origin_socket
from anyimage.operators.cutout_tool.object import load_cutout_node_group
from anyimage.common.object import modifier_input_identifier, set_modifier_input
from tests.support.nodes import validate_node_structure
@pytest.mark.parametrize("source", ["definition", "asset"])
def test_cutout_interfaces_and_thickness_modes(source):
@pytest.mark.parametrize("source", ("definition", "asset"))
@pytest.mark.parametrize("shape", ("SOLID", "DEPTH_SOLID", "DEPTH_BALLOON"))
def test_depth_axis_is_visible_in_options(shape, source):
bpy.ops.wm.read_factory_settings(use_empty=True)
group = CUTOUT_BUILDERS[shape]() if source == "definition" else load_cutout_node_group(shape)
inputs = {item.name: item for item in group.interface.items_tree
if item.item_type == "SOCKET" and item.in_out == "INPUT"}
axis = inputs["Depth Axis"]
assert axis.parent.name == "Options"
assert axis.menu_expanded
assert not axis.hide_in_modifier
assert axis.default_value == "-Z"
assert "Inward Axis" not in inputs
options = [item.name for item in inputs.values() if item.parent == axis.parent]
predecessor = "Smooth Weight" if shape == "SOLID" else "Reference Depth"
assert options[options.index(predecessor) + 1] == "Depth Axis"
if shape == "SOLID":
assert not any(item.item_type == "PANEL" and item.name == "Data" for item in group.interface.items_tree)
def test_cutout_thickness_modes():
bpy.ops.wm.read_factory_settings(use_empty=True)
groups = {shape: (CUTOUT_BUILDERS[shape]() if source == "definition" else 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_structure(group)
revision = 23 if group == groups["DEPTH_SOLID"] else 12 if group == groups["DEPTH_BALLOON"] else 11
assert group.is_modifier and group.get("anyimage_asset_revision") == revision
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["Split Threshold"].default_value, 0.1)
assert not {"Edge Round", "Normal Smooth"} & inputs.keys()
normal_blurs = [n for n in group.nodes if n.bl_idname == "GeometryNodeBlurAttribute" and n.data_type == "FLOAT_VECTOR"
and not n.inputs["Iterations"].is_linked and n.inputs["Iterations"].default_value == 100]
assert len(normal_blurs) == 2
assert inputs["Front Inflation"].default_value == 0
assert inputs["Front Inflation"].subtype == "FACTOR"
assert (inputs["Front Inflation"].min_value, inputs["Front Inflation"].max_value) == (0, 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 += ["Smooth Weight", "Reference Depth", "Boundary Smooth", "Outline Depth Fix", "Front Inflation"]
assert inputs["Outline Depth Fix"].default_value == 16
assert not set(inputs) & {"Ratio Lower", "Ratio Upper", "Weight Blur Iterations", "Weight Update Rounds", "Depth Steps Per Round"}
elif group == groups["DEPTH_BALLOON"]:
expected += ["Smooth Weight", "Reference Depth"]
if group not in (groups["DEPTH_SOLID"], groups["DEPTH_BALLOON"]):
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.to_node.bl_idname != "NodeReroute"
and origin_socket(link.from_socket).node.bl_idname == "NodeGroupInput"
and origin_socket(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")
@@ -72,7 +41,7 @@ def test_cutout_interfaces_and_thickness_modes(source):
obj = bpy.data.objects.new("Profile", mesh)
bpy.context.collection.objects.link(obj)
modifier = obj.modifiers.new("Cutout", "NODES")
modifier.node_group = groups["SOLID"]
modifier.node_group = build_image_cutout_group()
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
@@ -88,8 +57,8 @@ def test_cutout_interfaces_and_thickness_modes(source):
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)
back, front = (-0.6, 0.6) if mode == 0 else (-thickness, 0.0)
assert np.isclose(z.min(), back) and np.isclose(z.max(), front)
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: