- Introduced a new proposal for depth cutout symmetry, allowing for the reuse of depth replacement and geometry operations. - Added requirements for maintaining default thickness values for cutout nodes and ensuring proper initialization during object creation. - Enhanced symmetry node to handle existing geometry, including alignment, mirroring, and boundary filling. - Implemented fill smoothing to control the geometry of generated walls while preserving original surfaces. - Created tests to validate the functionality of the new symmetry features against baseline geometry. - Updated existing assets and node groups to support the new symmetry capabilities without introducing additional dependencies.
126 lines
5.7 KiB
Python
126 lines
5.7 KiB
Python
"""Verify split profile shaping and thickness-weighted normals."""
|
|
|
|
import bpy
|
|
import numpy as np
|
|
import pytest
|
|
|
|
from anyimage.common.object import modifier_input_identifier, set_modifier_input
|
|
from tests.support.depth_surface import surface, evaluated
|
|
from tests.tools.nodes.test_boundary_smoothing import diagonal_surface, boundary_neighbors, edge_band
|
|
from tools.nodes.common.cutout_boundary import remove_boundary_triangles
|
|
from tools.nodes.groups.image_depth_cutout import build_normal_weight, blend_normal
|
|
|
|
|
|
def profile_values(obj):
|
|
result = obj.evaluated_get(bpy.context.evaluated_depsgraph_get())
|
|
mesh = result.to_mesh()
|
|
try:
|
|
return np.array([v.value for v in mesh.attributes["o_balloon"].data])
|
|
finally:
|
|
result.to_mesh_clear()
|
|
|
|
|
|
def test_boundary_triangle_cleanup_is_one_pass():
|
|
obj, _ = surface(resolution=2)
|
|
group = obj.modifiers[0].node_group
|
|
mesh = bpy.data.meshes.new("Boundary ears")
|
|
mesh.from_pydata([(0,0,0),(1,0,0),(1,1,0),(0,1,0),(.5,.5,0),(2,0,0)], [],
|
|
[(0,1,4),(1,2,4),(2,3,4),(3,0,4),(1,5,2)])
|
|
obj.data = mesh
|
|
source = group.nodes.new("NodeGroupInput")
|
|
cleaned = remove_boundary_triangles(group, source.outputs["Geometry"])
|
|
group.links.new(cleaned, next(n for n in group.nodes if n.type == "GROUP_OUTPUT").inputs["Geometry"])
|
|
obj.update_tag(refresh={"DATA"})
|
|
bpy.context.view_layer.update()
|
|
_, faces = evaluated(obj)
|
|
assert len(faces) == 4
|
|
assert all(len(f) == 3 for f in faces)
|
|
|
|
|
|
@pytest.mark.parametrize("iterations", [0, 16])
|
|
def test_cleaned_sawtooth_smoothing_preserves_face_orientation(iterations):
|
|
obj, set_value = diagonal_surface(triangles=True)
|
|
original, faces = evaluated(obj)
|
|
set_value("Boundary Smooth", iterations)
|
|
points, actual = evaluated(obj)
|
|
assert actual == faces
|
|
indices = np.array(faces)
|
|
def normals(p):
|
|
return np.cross(p[indices[:,1]]-p[indices[:,0]], p[indices[:,2]]-p[indices[:,0]])
|
|
assert np.all(np.sum(normals(original)*normals(points), axis=1) > 0)
|
|
|
|
|
|
@pytest.mark.parametrize("resolution,aspect", [(12, .5), (24, 2)])
|
|
def test_split_profile_tapers_within_boundary_band(resolution, aspect):
|
|
obj, set_value = surface(step=6, resolution=resolution)
|
|
for vertex in obj.data.vertices:
|
|
vertex.co.x *= aspect
|
|
image = bpy.data.images["Camera"]
|
|
pixels = np.array(image.pixels[:], np.float32).reshape(32, 64, 4)
|
|
pixels[..., 0] *= aspect
|
|
image.pixels.foreach_set(pixels.ravel())
|
|
image.update()
|
|
obj.data.attributes.new("o_balloon", "FLOAT", "POINT").data.foreach_set(
|
|
"value", np.full(len(obj.data.vertices), .3, dtype=np.float32))
|
|
set_value("Depth Scale", 0)
|
|
np.testing.assert_allclose(profile_values(obj), .3)
|
|
set_value("Depth Split", .1)
|
|
points, faces = evaluated(obj)
|
|
boundary = boundary_neighbors(faces)
|
|
cut = [i for i in boundary if 0 < points[i,1] < 1 and .5*aspect < points[i,0] < 1.5*aspect]
|
|
assert cut
|
|
band = edge_band(faces, cut)
|
|
outside = [i for i in range(len(points)) if i not in band]
|
|
assert outside
|
|
values = profile_values(obj)
|
|
np.testing.assert_array_equal(values[cut], 0)
|
|
np.testing.assert_allclose(values[outside], .3)
|
|
assert np.isfinite(values).all()
|
|
assert values.max() == pytest.approx(.3)
|
|
set_value("Depth Split", 0)
|
|
np.testing.assert_allclose(profile_values(obj), .3)
|
|
|
|
|
|
@pytest.mark.parametrize("mode", [0, 1])
|
|
@pytest.mark.parametrize("reference", [0, 2])
|
|
def test_normal_blend_weights_balloon_and_keeps_full_shell_smoothing(mode, reference):
|
|
obj, set_value = surface(resolution=2)
|
|
group = obj.modifiers[0].node_group
|
|
# The production interface renames the Shell thickness socket after building.
|
|
socket = next(s for s in group.interface.items_tree if s.item_type == "SOCKET" and s.in_out == "INPUT"
|
|
and s.name == "Thickness" and s.subtype == "DISTANCE")
|
|
socket.name = "Shell Thickness"
|
|
is_shell = group.nodes.new("FunctionNodeInputBool")
|
|
is_shell.boolean = bool(mode)
|
|
weight = build_normal_weight(group, is_shell.outputs[0])
|
|
raw = group.nodes.new("ShaderNodeCombineXYZ")
|
|
raw.inputs["Z"].default_value = 1
|
|
blurred = group.nodes.new("ShaderNodeCombineXYZ")
|
|
blurred.inputs["X"].default_value = 1
|
|
result = blend_normal(group, raw.outputs[0], blurred.outputs[0], weight)
|
|
store = group.nodes.new("GeometryNodeStoreNamedAttribute")
|
|
store.data_type, store.domain = "FLOAT_VECTOR", "POINT"
|
|
store.inputs["Name"].default_value = "normal_result"
|
|
source = group.nodes.new("NodeGroupInput")
|
|
group.links.new(source.outputs["Geometry"], store.inputs["Geometry"])
|
|
group.links.new(result, store.inputs["Value"])
|
|
group.links.new(store.outputs[0], next(n for n in group.nodes if n.type == "GROUP_OUTPUT").inputs["Geometry"])
|
|
modifier = obj.modifiers[0]
|
|
identifier = socket.identifier if mode else modifier_input_identifier(group, "Thickness", subtype="NONE")
|
|
set_value("Reference Depth", reference)
|
|
results = []
|
|
for amount in (0, .001, .01, 1):
|
|
set_modifier_input(modifier, identifier, amount * (max(reference, 1e-8) if mode else 1))
|
|
obj.update_tag(refresh={"DATA"}); bpy.context.view_layer.update()
|
|
ev = obj.evaluated_get(bpy.context.evaluated_depsgraph_get()); mesh = ev.to_mesh()
|
|
try:
|
|
results.append(np.array(mesh.attributes["normal_result"].data[0].vector[:]))
|
|
finally:
|
|
ev.to_mesh_clear()
|
|
np.testing.assert_array_equal(results[-1], [1,0,0])
|
|
if mode:
|
|
np.testing.assert_array_equal(results, [[1,0,0]] * len(results))
|
|
else:
|
|
np.testing.assert_array_equal(results[0], [0,0,1])
|
|
assert np.linalg.norm(results[1]-results[0]) < np.linalg.norm(results[2]-results[0]) < .001
|