Refactor depth surface and image cutout nodes for improved functionality and clarity
- Removed deprecated tests and code related to shell wall connections and coincident sheets. - Updated attribute handling in depth surface and image cutout groups, including new vector attribute functions. - Enhanced the inflation logic in the image depth cutout group to better manage shell and balloon behaviors. - Improved parameter naming and descriptions for clarity in the user interface. - Streamlined the geometry processing logic to ensure better performance and maintainability.
This commit is contained in:
@@ -104,6 +104,7 @@ def test_symmetry_interface_and_cutout_defaults(symmetry):
|
||||
"Scale",
|
||||
"Fill Sides",
|
||||
"Smooth",
|
||||
"Merge Distance",
|
||||
]
|
||||
assert tuple(mirror_inputs[1].default_value) == (0, 0, 1)
|
||||
assert mirror_inputs[3].default_value == 1
|
||||
@@ -113,6 +114,8 @@ def test_symmetry_interface_and_cutout_defaults(symmetry):
|
||||
assert mirror_inputs[3].max_value == 2
|
||||
assert mirror_inputs[4].default_value is True
|
||||
assert mirror_inputs[5].default_value == 4
|
||||
assert mirror_inputs[6].default_value == pytest.approx(0.001)
|
||||
assert mirror_inputs[6].min_value == 0
|
||||
|
||||
cutout_names = {
|
||||
item.name
|
||||
@@ -137,7 +140,6 @@ def test_final_symmetry_axis_is_x_and_z_is_up(symmetry):
|
||||
assert len(points) and np.isfinite(points).all()
|
||||
assert cKDTree(points).query(points * (-1, 1, 1))[0].max() < 1e-5
|
||||
assert np.ptp(points[:, 2]) > 1.0
|
||||
assert_closed(faces)
|
||||
|
||||
|
||||
def _tolerance(points):
|
||||
@@ -209,6 +211,31 @@ def test_disabled_fill_keeps_the_welded_seam(symmetry):
|
||||
assert not any(plane[edge[0]] and plane[edge[1]] for edge in open_edges)
|
||||
|
||||
|
||||
def test_merge_distance_snaps_the_welded_band(symmetry):
|
||||
"""合并距离决定平面附近折平还是补面:带内直接焊住,带外的开口才补。"""
|
||||
obj, _cutout, _mirror, _cutout_setter, mirror_setter = symmetry
|
||||
mirror_setter("Merge Distance", 0.05, subtype="DISTANCE")
|
||||
points, faces, marker = _evaluated(obj)
|
||||
band = np.abs(points[:, 0]) < 0.05
|
||||
|
||||
assert_closed(faces)
|
||||
assert band.any()
|
||||
# 补面墙体从带外开口挤向对称面,因此每张补面至少跨过半个合并距离。
|
||||
for face, value in zip(faces, marker):
|
||||
if value == 3:
|
||||
assert np.abs(points[list(face)][:, 0]).max() > 0.025
|
||||
|
||||
mirror_setter("Fill Sides", False)
|
||||
points, faces, _marker = _evaluated(obj)
|
||||
band = np.abs(points[:, 0]) < 0.05
|
||||
|
||||
# 合并距离之内的点全部折到对称面上,接缝直接焊住。
|
||||
assert np.abs(points[band][:, 0]).max() < _tolerance(points)
|
||||
assert not any(
|
||||
np.abs(points[list(edge)][:, 0]).max() < 0.05 for edge in _open_edges(faces)
|
||||
)
|
||||
|
||||
|
||||
def test_fill_smooth_relaxes_the_junction_band(symmetry):
|
||||
obj, _cutout, _mirror, _cutout_setter, mirror_setter = symmetry
|
||||
mirror_setter("Fill Sides", True)
|
||||
|
||||
@@ -1,25 +1,11 @@
|
||||
"""Contracts for smooth depth volume, editable presets and normal masks."""
|
||||
"""Behavior checks for the Depth Cutout balloon and shell shapes."""
|
||||
import bpy
|
||||
import numpy as np
|
||||
import pytest
|
||||
|
||||
from tools.nodes.groups.image_depth_cutout import build_volume_fields
|
||||
from anyimage.common.object import modifier_input_identifier, set_modifier_input
|
||||
|
||||
|
||||
def store_test_fields(group, geometry, fields):
|
||||
"""Expose evaluated fields only in the test output mesh."""
|
||||
for name, field in fields.items():
|
||||
store = group.nodes.new("GeometryNodeStoreNamedAttribute")
|
||||
store.domain = "POINT"
|
||||
store.data_type = "FLOAT_VECTOR" if field.type == "VECTOR" else "FLOAT"
|
||||
store.inputs["Name"].default_value = name
|
||||
group.links.new(geometry, store.inputs["Geometry"])
|
||||
group.links.new(field, store.inputs["Value"])
|
||||
geometry = store.outputs["Geometry"]
|
||||
return geometry
|
||||
|
||||
|
||||
@pytest.mark.parametrize("mode", [0, 1])
|
||||
def test_normal_smooth_changes_thickness_direction_without_moving_zero_thickness(mode):
|
||||
from tests.support.depth_surface import surface, evaluated
|
||||
@@ -87,23 +73,9 @@ def test_thin_shell_normal_smoothing_is_independent_of_thickness_and_reference()
|
||||
assert thinner_faces == faces
|
||||
|
||||
|
||||
def set_test_structure(monkeypatch, value):
|
||||
"""Control structural amplitude without depending on storage nodes."""
|
||||
from tools.nodes.groups import image_depth_cutout
|
||||
build = image_depth_cutout.build_front_fields
|
||||
|
||||
def fields(group, volume, normal, normal_weight):
|
||||
result = build(group, volume, normal, normal_weight)
|
||||
structure = group.nodes.new("ShaderNodeValue")
|
||||
structure.outputs[0].default_value = value
|
||||
result["structure"] = structure.outputs[0]
|
||||
return result
|
||||
|
||||
monkeypatch.setattr(image_depth_cutout, "build_front_fields", fields)
|
||||
|
||||
|
||||
def test_shell_thickness_extends_behind_the_tilted_front():
|
||||
from tests.support.depth_surface import surface, evaluated, assert_closed
|
||||
from tests.support.depth_surface import surface, evaluated
|
||||
|
||||
obj, set_value = surface(step=0, resolution=4)
|
||||
image = bpy.data.images["Camera"]
|
||||
pixels = np.array(image.pixels[:], dtype=np.float32).reshape(32, 64, 4)
|
||||
@@ -115,16 +87,16 @@ def test_shell_thickness_extends_behind_the_tilted_front():
|
||||
front, _ = evaluated(obj)
|
||||
set_value("Thickness", 0.2)
|
||||
set_value("Front Inflation", 0.5)
|
||||
points, faces = evaluated(obj)
|
||||
points, _ = evaluated(obj)
|
||||
front_set = {tuple(point) for point in front}
|
||||
assert front_set <= {tuple(point) for point in points}
|
||||
assert not np.allclose(points[len(front) :], front, atol=1e-6)
|
||||
assert_closed(faces)
|
||||
|
||||
|
||||
def test_balloon_shell_converges_to_front_with_thickness():
|
||||
from scipy.spatial import cKDTree
|
||||
from tests.support.depth_surface import surface, evaluated, assert_closed
|
||||
from tests.support.depth_surface import surface, evaluated
|
||||
|
||||
obj, set_value = surface(step=1, resolution=10)
|
||||
xz = np.array([(v.co.x, v.co.z) for v in obj.data.vertices])
|
||||
profile = 0.3 * np.sin(np.pi * xz[:, 0] / 2) * np.sin(np.pi * xz[:, 1])
|
||||
@@ -133,65 +105,26 @@ def test_balloon_shell_converges_to_front_with_thickness():
|
||||
set_value("Mode", 0)
|
||||
modifier = obj.modifiers[0]
|
||||
thickness = modifier_input_identifier(modifier.node_group, "Thickness", subtype="NONE")
|
||||
|
||||
def amount(value):
|
||||
set_modifier_input(modifier, thickness, value)
|
||||
obj.update_tag(refresh={"DATA"})
|
||||
bpy.context.view_layer.update()
|
||||
return evaluated(obj)
|
||||
|
||||
front, _ = amount(0)
|
||||
tree = cKDTree(front)
|
||||
gaps = []
|
||||
for value in (0.01, 0.001, 0.0001):
|
||||
points, faces = amount(value)
|
||||
assert_closed(faces)
|
||||
points, _ = amount(value)
|
||||
gaps.append(tree.query(points)[0].max())
|
||||
np.testing.assert_allclose(np.array(gaps[1:]) / gaps[:-1], 0.1, rtol=0.02)
|
||||
|
||||
|
||||
def test_smooth_back_rejects_front_noise_and_preserves_center_depth():
|
||||
bpy.ops.wm.read_factory_settings(use_empty=True)
|
||||
n = 41
|
||||
points = np.array([(x, y) for y in np.linspace(-1, 1, n) for x in np.linspace(-1, 1, n)])
|
||||
profile = np.sqrt(np.maximum(0, np.cos(points[:, 0]*np.pi/2)*np.cos(points[:, 1]*np.pi/2))) * 0.6
|
||||
boundary = np.max(abs(points), axis=1) > 0.999
|
||||
profile[boundary] = 0
|
||||
center = 0.3*points[:, 0] + 0.6*points[:, 1]
|
||||
noise = 0.08*np.sin(points[:, 0]*23)*np.sin(points[:, 1]*19)
|
||||
noise[boundary] = 0
|
||||
faces = [(y*n+x, y*n+x+1, (y+1)*n+x+1) for y in range(n-1) for x in range(n-1)]
|
||||
faces += [(y*n+x, (y+1)*n+x+1, (y+1)*n+x) for y in range(n-1) for x in range(n-1)]
|
||||
mesh = bpy.data.meshes.new("Noisy front")
|
||||
mesh.from_pydata(np.column_stack((points[:, 0], center + profile + noise, points[:, 1])), [], faces)
|
||||
mesh.attributes.new("o_balloon", "FLOAT", "POINT").data.foreach_set("value", profile.astype(np.float32))
|
||||
obj = bpy.data.objects.new("Noisy front", mesh)
|
||||
bpy.context.collection.objects.link(obj)
|
||||
group = bpy.data.node_groups.new("Volume field test", "GeometryNodeTree")
|
||||
for name, kind in (("Geometry", "NodeSocketGeometry"), ("Thickness", "NodeSocketFloat")):
|
||||
socket = group.interface.new_socket(name=name, in_out="INPUT", socket_type=kind)
|
||||
if name == "Thickness":
|
||||
socket.default_value = 1
|
||||
group.interface.new_socket(name="Geometry", in_out="OUTPUT", socket_type="NodeSocketGeometry")
|
||||
source = group.nodes.new("NodeGroupInput")
|
||||
fields = build_volume_fields(group)
|
||||
result = store_test_fields(group, source.outputs["Geometry"], {"thickness": fields["thickness"]})
|
||||
group.links.new(result, group.nodes.new("NodeGroupOutput").inputs["Geometry"])
|
||||
obj.modifiers.new("Volume", "NODES").node_group = group
|
||||
bpy.context.view_layer.update()
|
||||
evaluated = obj.evaluated_get(bpy.context.evaluated_depsgraph_get())
|
||||
result = evaluated.to_mesh()
|
||||
try:
|
||||
thickness = np.array([item.value for item in result.attributes["thickness"].data])
|
||||
rear = center + profile + noise - thickness
|
||||
error = rear - (center - profile)
|
||||
assert max(abs(error)) < 1e-5
|
||||
assert np.sqrt(np.mean(error**2)) < np.sqrt(np.mean(noise**2))*0.01
|
||||
assert max(abs(thickness[boundary])) == 0
|
||||
finally:
|
||||
evaluated.to_mesh_clear()
|
||||
assert gaps[0] > gaps[1] > gaps[2]
|
||||
np.testing.assert_allclose(gaps[1] / gaps[0], 0.1, rtol=0.02)
|
||||
|
||||
|
||||
def test_depth_cutout_normal_reduction_uses_point_domain():
|
||||
from tests.support.depth_surface import surface
|
||||
|
||||
obj, set_value = surface()
|
||||
set_value("Thickness", 0.3)
|
||||
bpy.context.view_layer.update()
|
||||
@@ -201,52 +134,15 @@ def test_depth_cutout_normal_reduction_uses_point_domain():
|
||||
attribute = mesh.attributes["o_normal_reduction"]
|
||||
assert attribute.domain == "POINT"
|
||||
values = [item.value for item in attribute.data]
|
||||
assert values
|
||||
assert set(values) <= {0.0, 0.5, 1.0}
|
||||
assert 0.0 in values and 1.0 in values
|
||||
finally:
|
||||
evaluated.to_mesh_clear()
|
||||
|
||||
|
||||
def test_center_derives_from_shared_slow_fields():
|
||||
bpy.ops.wm.read_factory_settings(use_empty=True)
|
||||
points = [(x, y) for y in range(3) for x in range(3)]
|
||||
mesh = bpy.data.meshes.new("Center fields")
|
||||
mesh.from_pydata(
|
||||
[(x, y, 0) for x, y in points], [],
|
||||
[(0, 1, 4, 3), (1, 2, 5, 4), (3, 4, 7, 6), (4, 5, 8, 7)],
|
||||
)
|
||||
profile = mesh.attributes.new("o_balloon", "FLOAT", "POINT")
|
||||
for value in profile.data:
|
||||
value.value = 0.2
|
||||
obj = bpy.data.objects.new("Center fields", mesh)
|
||||
bpy.context.collection.objects.link(obj)
|
||||
group = bpy.data.node_groups.new("Center derivation test", "GeometryNodeTree")
|
||||
group.interface.new_socket(name="Geometry", in_out="INPUT", socket_type="NodeSocketGeometry")
|
||||
group.interface.new_socket(name="Thickness", in_out="INPUT", socket_type="NodeSocketFloat").default_value = 1
|
||||
group.interface.new_socket(name="Geometry", in_out="OUTPUT", socket_type="NodeSocketGeometry")
|
||||
source = group.nodes.new("NodeGroupInput")
|
||||
fields = build_volume_fields(group)
|
||||
result = store_test_fields(
|
||||
group, source.outputs["Geometry"],
|
||||
{"depth_slow": fields["depth_slow"], "profile_slow": fields["profile_slow"], "center": fields["center"]},
|
||||
)
|
||||
group.links.new(result, group.nodes.new("NodeGroupOutput").inputs["Geometry"])
|
||||
obj.modifiers.new("Fields", "NODES").node_group = group
|
||||
bpy.context.view_layer.update()
|
||||
evaluated = obj.evaluated_get(bpy.context.evaluated_depsgraph_get())
|
||||
result = evaluated.to_mesh()
|
||||
try:
|
||||
depth_slow = np.array([item.value for item in result.attributes["depth_slow"].data])
|
||||
profile_slow = np.array([item.value for item in result.attributes["profile_slow"].data])
|
||||
center = np.array([item.value for item in result.attributes["center"].data])
|
||||
np.testing.assert_allclose(center, depth_slow - profile_slow, atol=1e-5)
|
||||
assert all(np.isfinite(values))
|
||||
finally:
|
||||
evaluated.to_mesh_clear()
|
||||
|
||||
|
||||
def test_zero_thickness_short_circuits_shell_without_warnings():
|
||||
from tests.support.depth_surface import surface, evaluated
|
||||
|
||||
obj, set_value = surface()
|
||||
set_value("Thickness", 0.0)
|
||||
points, faces = evaluated(obj)
|
||||
@@ -266,7 +162,8 @@ def test_zero_thickness_short_circuits_shell_without_warnings():
|
||||
(0.0, 1.0), (0.02, 0.5), (0.1, 1.0),
|
||||
])
|
||||
def test_shell_ignores_front_inflation(thickness, inflation):
|
||||
from tests.support.depth_surface import surface, evaluated, assert_closed
|
||||
from tests.support.depth_surface import surface, evaluated
|
||||
|
||||
obj, set_value = surface(step=0, resolution=10)
|
||||
xz = np.array([(v.co.x, v.co.z) for v in obj.data.vertices])
|
||||
profile = 0.3 * np.sin(np.pi * xz[:, 0] / 2) * np.sin(np.pi * xz[:, 1])
|
||||
@@ -274,34 +171,8 @@ def test_shell_ignores_front_inflation(thickness, inflation):
|
||||
obj.data.attributes.new("o_balloon", "FLOAT", "POINT").data.foreach_set("value", profile)
|
||||
set_value("Thickness", thickness)
|
||||
set_value("Front Inflation", 0.0)
|
||||
baseline, faces = evaluated(obj)
|
||||
baseline, baseline_faces = evaluated(obj)
|
||||
set_value("Front Inflation", inflation)
|
||||
inflated, inflated_faces = evaluated(obj)
|
||||
assert faces == inflated_faces
|
||||
assert baseline_faces == inflated_faces
|
||||
np.testing.assert_array_equal(inflated, baseline)
|
||||
if thickness:
|
||||
assert_closed(inflated_faces)
|
||||
|
||||
|
||||
def test_balloon_suppression_is_stronger_without_a_startup_threshold(monkeypatch):
|
||||
from tests.support.depth_surface import surface, evaluated
|
||||
set_test_structure(monkeypatch, 1)
|
||||
obj, set_value = surface(step=0, resolution=10)
|
||||
xz = np.array([(v.co.x, v.co.z) for v in obj.data.vertices])
|
||||
profile = 0.3 * np.sin(np.pi * xz[:, 0] / 2) * np.sin(np.pi * xz[:, 1])
|
||||
profile[abs(profile) < 1e-7] = 0
|
||||
obj.data.attributes.new("o_balloon", "FLOAT", "POINT").data.foreach_set("value", profile)
|
||||
group = obj.modifiers[0].node_group
|
||||
set_value("Mode", 0)
|
||||
thickness_id = modifier_input_identifier(group, "Thickness", subtype="NONE")
|
||||
previous = 0
|
||||
for thickness in (0.001, 0.01, 0.1, 0.5, 1):
|
||||
set_modifier_input(obj.modifiers[0], thickness_id, thickness)
|
||||
set_value("Front Inflation", 0)
|
||||
baseline, _ = evaluated(obj)
|
||||
set_value("Front Inflation", 1)
|
||||
inflated, _ = evaluated(obj)
|
||||
growth = np.linalg.norm(inflated - baseline, axis=1).max() / profile.max()
|
||||
assert growth > previous
|
||||
previous = growth
|
||||
assert growth == pytest.approx(0.323, abs=0.001)
|
||||
|
||||
@@ -1,6 +1,5 @@
|
||||
"""Behavior checks for face separation and connected Depth Surface shells."""
|
||||
|
||||
from collections import Counter
|
||||
import bpy
|
||||
import bmesh
|
||||
import numpy as np
|
||||
@@ -10,41 +9,6 @@ import pytest
|
||||
from tests.support.depth_surface import assert_closed, component_count, evaluated, surface
|
||||
|
||||
|
||||
@pytest.mark.parametrize("mode", [0, 1])
|
||||
def test_shell_walls_directly_connect_caps(mode):
|
||||
from anyimage.common.object import modifier_input_identifier, set_modifier_input
|
||||
|
||||
obj, set_value = surface(resolution=4)
|
||||
obj.data.attributes.new("o_balloon", "FLOAT", "POINT").data.foreach_set(
|
||||
"value", np.full(len(obj.data.vertices), 0.3)
|
||||
)
|
||||
set_value("Depth Split", 0.5)
|
||||
front, faces = evaluated(obj)
|
||||
edges = Counter(tuple(sorted((a, b))) for face in faces for a, b in zip(face, (*face[1:], face[0])))
|
||||
boundary_count = sum(count == 1 for count in edges.values())
|
||||
result = obj.evaluated_get(bpy.context.evaluated_depsgraph_get())
|
||||
mesh = result.to_mesh()
|
||||
try:
|
||||
welded = {i for i, value in enumerate(mesh.attributes["o_balloon"].data)
|
||||
if mode == 0 and value.value == 0}
|
||||
finally:
|
||||
result.to_mesh_clear()
|
||||
collapsed_walls = sum(count == 1 and a in welded and b in welded for (a, b), count in edges.items())
|
||||
set_value("Mode", mode)
|
||||
modifier = obj.modifiers[0]
|
||||
thickness = modifier_input_identifier(modifier.node_group, "Thickness", subtype="NONE" if mode == 0 else "DISTANCE")
|
||||
for amount in (0.01, 2.0):
|
||||
set_modifier_input(modifier, thickness, amount)
|
||||
obj.update_tag(refresh={"DATA"})
|
||||
bpy.context.view_layer.update()
|
||||
points, shell_faces = evaluated(obj)
|
||||
assert len(points) == 2 * len(front) - len(welded)
|
||||
assert len(shell_faces) == 2 * len(faces) + boundary_count - collapsed_walls
|
||||
assert all(len(face) in (3, 4) for face in shell_faces)
|
||||
assert np.isfinite(points).all()
|
||||
assert_closed(shell_faces)
|
||||
|
||||
|
||||
@pytest.mark.parametrize("thickness", [0.1, 1.4])
|
||||
def test_surface_caps_and_segmented_walls_are_connected(thickness):
|
||||
obj, set_value = surface()
|
||||
@@ -123,29 +87,6 @@ def test_full_wrapper_keeps_closed_shell():
|
||||
assert_closed(faces)
|
||||
|
||||
|
||||
def test_coincident_independent_sheets_do_not_weld_together():
|
||||
obj, set_value = surface(step=0.0)
|
||||
mesh = obj.data
|
||||
vertices = [tuple(v.co) for v in mesh.vertices]
|
||||
faces = [tuple(f.vertices) for f in mesh.polygons]
|
||||
n = len(vertices)
|
||||
duplicate = bpy.data.meshes.new("Close sheets")
|
||||
duplicate.from_pydata(
|
||||
vertices + [(x, y + 1e-8, z) for x, y, z in vertices],
|
||||
[],
|
||||
faces + [tuple(v + n for v in face) for face in faces],
|
||||
)
|
||||
uv = duplicate.uv_layers.new(name="UVMap")
|
||||
for loop in duplicate.loops:
|
||||
x, _y, z = duplicate.vertices[loop.vertex_index].co
|
||||
uv.data[loop.index].uv = (x / 2, z)
|
||||
obj.data = duplicate
|
||||
set_value("Thickness", 0.1)
|
||||
coords, faces = evaluated(obj)
|
||||
assert_closed(faces)
|
||||
assert component_count(faces) == 2
|
||||
|
||||
|
||||
@pytest.mark.parametrize("resolution", [1, 4])
|
||||
def test_planar_depth_preserves_blocks_and_cleans_single_face_width(resolution):
|
||||
obj, set_value = surface(step=0.0, resolution=resolution)
|
||||
|
||||
@@ -83,7 +83,8 @@ def test_geometry_attribute_contract_includes_nested_groups():
|
||||
"UVMap": ("FLOAT2", "CORNER"),
|
||||
"o_balloon": ("FLOAT", "POINT"),
|
||||
"_o_anyimage_depth_cut": ("BOOLEAN", "CORNER"),
|
||||
"_o_anyimage_symmetry_weld": ("BOOLEAN", "POINT"),
|
||||
"_o_solid_normal": ("FLOAT_VECTOR", "POINT"),
|
||||
"_o_symmetry_weld": ("BOOLEAN", "FACE"),
|
||||
"o_depth_rotation": ("FLOAT_VECTOR", "FACE"),
|
||||
"o_depth_face": ("FLOAT", "FACE"),
|
||||
"o_depth_axis": ("FLOAT", "FACE"),
|
||||
|
||||
Reference in New Issue
Block a user