153 lines
6.3 KiB
Python
153 lines
6.3 KiB
Python
"""Verify split profile shaping and displacement direction shaping."""
|
|
|
|
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
|
|
from tools.nodes.common.cutout_boundary import remove_boundary_triangles
|
|
from tools.nodes.groups.image_depth_cutout import build_normal_direction, build_normal_weight
|
|
|
|
|
|
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 vertex_uv(obj):
|
|
result = obj.evaluated_get(bpy.context.evaluated_depsgraph_get())
|
|
mesh = result.to_mesh()
|
|
try:
|
|
uv = np.zeros((len(mesh.vertices), 2))
|
|
for loop in mesh.loops:
|
|
uv[loop.vertex_index] = mesh.uv_layers["UVMap"].data[loop.index].uv
|
|
return uv
|
|
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)
|
|
|
|
|
|
def test_split_profile_tapers_within_boundary_band():
|
|
resolution, aspect = 12, .5
|
|
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", 1)
|
|
np.testing.assert_allclose(profile_values(obj), .3)
|
|
set_value("Depth Split", .1)
|
|
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)
|
|
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)
|
|
|
|
|
|
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 = 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(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]
|
|
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)
|