refactor: update depth cutout and surface handling for improved geometry manipulation and clarity
This commit is contained in:
@@ -1,4 +1,4 @@
|
||||
"""Verify split profile shaping and thickness-weighted normals."""
|
||||
"""Verify split profile shaping and displacement direction shaping."""
|
||||
|
||||
import bpy
|
||||
import numpy as np
|
||||
@@ -6,9 +6,9 @@ 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 tests.tools.nodes.test_boundary_smoothing import diagonal_surface
|
||||
from tools.nodes.common.cutout_boundary import remove_boundary_triangles
|
||||
from tools.nodes.groups.image_depth_cutout import build_normal_weight, blend_normal
|
||||
from tools.nodes.groups.image_depth_cutout import build_normal_direction, build_normal_weight
|
||||
|
||||
|
||||
def profile_values(obj):
|
||||
@@ -77,63 +77,76 @@ def test_split_profile_tapers_within_boundary_band():
|
||||
set_value("Depth Scale", 1)
|
||||
np.testing.assert_allclose(profile_values(obj), .3)
|
||||
set_value("Depth Split", .1)
|
||||
points, faces = evaluated(obj)
|
||||
boundary = boundary_neighbors(faces)
|
||||
uv = vertex_uv(obj)
|
||||
# The depth step sits at u=0.5, so that column is the split line.
|
||||
cut = [i for i in boundary if abs(uv[i,0] - .5) < 1e-4 and 1e-4 < uv[i,1] < 1 - 1e-4]
|
||||
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)
|
||||
uv = vertex_uv(obj)
|
||||
# The depth step sits at u=0.5, so only that column is tapered.
|
||||
cut = (np.abs(uv[:, 0] - .5) < 1e-4) & (uv[:, 1] > 1e-3) & (uv[:, 1] < 1 - 1e-3)
|
||||
assert cut.any()
|
||||
np.testing.assert_array_equal(values[cut], 0)
|
||||
np.testing.assert_allclose(values[outside], .3)
|
||||
untouched = (np.abs(uv[:, 0] - .25) < 1e-3) | (np.abs(uv[:, 0] - .75) < 1e-3)
|
||||
assert untouched.any()
|
||||
np.testing.assert_allclose(values[untouched], .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])
|
||||
def test_normal_blend_weights_balloon_and_keeps_full_shell_smoothing(mode):
|
||||
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)
|
||||
def sampled(obj, group, value, name):
|
||||
"""Store one field on the points and read it back from the evaluated mesh."""
|
||||
data_type, reader = {"VECTOR": ("FLOAT_VECTOR", "vector"), "VALUE": ("FLOAT", "value")}[value.type]
|
||||
store = group.nodes.new("GeometryNodeStoreNamedAttribute")
|
||||
store.data_type, store.domain = "FLOAT_VECTOR", "POINT"
|
||||
store.inputs["Name"].default_value = "normal_result"
|
||||
store.data_type, store.domain = data_type, "POINT"
|
||||
store.inputs["Name"].default_value = name
|
||||
source = group.nodes.new("NodeGroupInput")
|
||||
group.links.new(source.outputs["Geometry"], store.inputs["Geometry"])
|
||||
group.links.new(result, store.inputs["Value"])
|
||||
group.links.new(value, store.inputs["Value"])
|
||||
group.links.new(store.outputs[0], next(n for n in group.nodes if n.type == "GROUP_OUTPUT").inputs["Geometry"])
|
||||
obj.update_tag(refresh={"DATA"})
|
||||
bpy.context.view_layer.update()
|
||||
result = obj.evaluated_get(bpy.context.evaluated_depsgraph_get())
|
||||
mesh = result.to_mesh()
|
||||
try:
|
||||
return np.array([getattr(item, reader) for item in mesh.attributes[name].data])
|
||||
finally:
|
||||
result.to_mesh_clear()
|
||||
|
||||
|
||||
def test_normal_weight_follows_profile_only_in_balloon_mode():
|
||||
obj, _ = surface(resolution=2)
|
||||
group = obj.modifiers[0].node_group
|
||||
is_shell = group.nodes.new("FunctionNodeInputBool")
|
||||
profile = group.nodes.new("ShaderNodeValue")
|
||||
profile.outputs[0].default_value = .25
|
||||
weight = build_normal_weight(group, is_shell.outputs[0], profile.outputs[0])
|
||||
np.testing.assert_allclose(sampled(obj, group, weight, "normal_weight"), .25)
|
||||
is_shell.boolean = True
|
||||
np.testing.assert_allclose(sampled(obj, group, weight, "normal_weight"), 0.0)
|
||||
|
||||
|
||||
def test_normal_direction_turns_the_edge_with_edge_turn():
|
||||
obj, _ = surface(resolution=2)
|
||||
group = obj.modifiers[0].node_group
|
||||
smoothed = group.nodes.new("ShaderNodeCombineXYZ")
|
||||
smoothed.inputs["Z"].default_value = 1
|
||||
average = group.nodes.new("ShaderNodeCombineXYZ")
|
||||
average.inputs["X"].default_value = 1
|
||||
weight = group.nodes.new("ShaderNodeValue")
|
||||
controls = group.nodes.new("NodeGroupInput")
|
||||
direction = build_normal_direction(
|
||||
group, controls, smoothed.outputs[0], average.outputs[0], weight.outputs[0],
|
||||
)
|
||||
modifier = obj.modifiers[0]
|
||||
identifier = socket.identifier if mode else modifier_input_identifier(group, "Thickness", subtype="NONE")
|
||||
reference = 2
|
||||
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
|
||||
turn = modifier_input_identifier(group, "Edge Turn")
|
||||
|
||||
def result(weight_value, turn_value):
|
||||
weight.outputs[0].default_value = weight_value
|
||||
set_modifier_input(modifier, turn, turn_value)
|
||||
obj.update_tag(refresh={"DATA"})
|
||||
bpy.context.view_layer.update()
|
||||
return sampled(obj, group, direction, "normal_direction")[0]
|
||||
|
||||
np.testing.assert_allclose(result(0.0, 0.0), (0, 0, 1), atol=1e-6)
|
||||
np.testing.assert_allclose(result(0.0, 1.0), (1, 0, 0), atol=1e-6)
|
||||
np.testing.assert_allclose(result(0.0, 2.0), (2, 0, -1) / np.sqrt(5), atol=1e-6)
|
||||
np.testing.assert_allclose(result(1.0, 2.0), (1, 0, 0), atol=1e-6)
|
||||
|
||||
Reference in New Issue
Block a user