Refactor geometry node groups for image depth processing
- Introduced new utility functions in common nodes for evaluating fields and capturing attributes. - Simplified volume field calculations in image depth cutout and panorama groups by utilizing new utility functions. - Enhanced readability and maintainability of the image depth cutout and panorama node groups by removing redundant code and improving function usage. - Adjusted default values for thickness and depth scale in image relief plane group for better user experience. - Improved handling of geometry attributes and connections in image depth plane group to streamline processing.
This commit is contained in:
@@ -6,6 +6,35 @@ import pytest
|
||||
from tools.nodes.groups.image_depth_cutout import build_volume_fields
|
||||
from tools.nodes.groups.image_depth_cutout import build_volume_rounding
|
||||
from anyimage.common.object import modifier_input_identifier, set_modifier_input
|
||||
from tools.nodes.common.nodes import capture_fields
|
||||
|
||||
|
||||
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
|
||||
|
||||
|
||||
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, rim, normal):
|
||||
result = build(group, rim, normal)
|
||||
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)
|
||||
|
||||
|
||||
@pytest.mark.parametrize("iterations", [0, 50])
|
||||
@@ -70,14 +99,17 @@ def test_rounding_shares_front_and_back_falloff(amount):
|
||||
obj = bpy.data.objects.new("Round profile", mesh)
|
||||
bpy.context.collection.objects.link(obj)
|
||||
group = bpy.data.node_groups.new("Round field test", "GeometryNodeTree")
|
||||
for name, kind, default in (("Geometry", "NodeSocketGeometry", None), ("Thickness", "NodeSocketFloat", 1), ("Edge Round", "NodeSocketFloat", amount), ("Normal Smooth", "NodeSocketInt", 20)):
|
||||
for name, kind, default in (("Geometry", "NodeSocketGeometry", None), ("Thickness", "NodeSocketFloat", 1), ("Uniform Thickness", "NodeSocketFloat", 0), ("Edge Round", "NodeSocketFloat", amount), ("Normal Smooth", "NodeSocketInt", 20)):
|
||||
socket = group.interface.new_socket(name=name, in_out="INPUT", socket_type=kind)
|
||||
if default is not None:
|
||||
socket.default_value = default
|
||||
group.interface.new_socket(name="Geometry", in_out="OUTPUT", socket_type="NodeSocketGeometry")
|
||||
source = group.nodes.new("NodeGroupInput")
|
||||
result = build_volume_fields(group, source.outputs["Geometry"])
|
||||
result = build_volume_rounding(group, result)
|
||||
fields = build_volume_fields(group)
|
||||
geometry, center = capture_fields(group, source.outputs["Geometry"], "POINT", center=("FLOAT", fields["center"]))
|
||||
mode = group.nodes.new("FunctionNodeInputBool")
|
||||
result, rounded = build_volume_rounding(group, geometry, {**fields, **center}, mode.outputs[0])
|
||||
result = store_test_fields(group, result, {**center, **rounded})
|
||||
group.links.new(result, group.nodes.new("NodeGroupOutput").inputs["Geometry"])
|
||||
obj.modifiers.new("Round", "NODES").node_group = group
|
||||
bpy.context.view_layer.update()
|
||||
@@ -85,7 +117,7 @@ def test_rounding_shares_front_and_back_falloff(amount):
|
||||
result = evaluated.to_mesh()
|
||||
try:
|
||||
def values(name):
|
||||
return np.array([v.value for v in result.attributes["o_balloon_surface_" + name].data])
|
||||
return np.array([v.value for v in result.attributes[name].data])
|
||||
center = values("center")
|
||||
u = balloon / balloon.max()
|
||||
weight = amount * (1 - u**3 * (u * (6*u - 15) + 10))
|
||||
@@ -94,7 +126,7 @@ def test_rounding_shares_front_and_back_falloff(amount):
|
||||
np.testing.assert_allclose(values("edge_weight"), weight, atol=1e-6)
|
||||
np.testing.assert_allclose(values("thickness"), original + delta + balloon * weight, atol=1e-6)
|
||||
np.testing.assert_allclose([v.co.z for v in result.vertices], depth + delta, atol=1e-6)
|
||||
normals = np.array([v.vector[:] for v in result.attributes["o_balloon_surface_normal"].data])
|
||||
normals = np.array([v.vector[:] for v in result.attributes["normal"].data])
|
||||
np.testing.assert_allclose(np.linalg.norm(normals, axis=1), 1, atol=1e-6)
|
||||
assert np.max(abs(normals[:, :2])) > 0.1
|
||||
finally:
|
||||
@@ -125,14 +157,15 @@ def test_smooth_back_rejects_front_noise_and_preserves_center_depth():
|
||||
socket.default_value = 1
|
||||
group.interface.new_socket(name="Geometry", in_out="OUTPUT", socket_type="NodeSocketGeometry")
|
||||
source = group.nodes.new("NodeGroupInput")
|
||||
result = build_volume_fields(group, source.outputs["Geometry"])
|
||||
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["o_balloon_surface_thickness"].data])
|
||||
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
|
||||
@@ -161,18 +194,14 @@ def test_depth_cutout_normal_map_only_affects_front_faces():
|
||||
|
||||
@pytest.mark.parametrize("structure", [0, 1, 4])
|
||||
@pytest.mark.parametrize("thickness", [0.0, 0.02, 0.1])
|
||||
def test_uniform_front_inflation_scales_with_distance(thickness, structure):
|
||||
def test_uniform_front_inflation_scales_with_distance(thickness, structure, monkeypatch):
|
||||
from tests.support.depth_surface import surface, evaluated, assert_closed
|
||||
set_test_structure(monkeypatch, structure)
|
||||
obj, set_value = surface(step=0, resolution=10)
|
||||
xy = np.array([v.co[:2] for v in obj.data.vertices])
|
||||
profile = 0.3 * np.sin(np.pi * xy[:, 0] / 2) * np.sin(np.pi * xy[:, 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
|
||||
store = next(n for n in group.nodes if n.bl_idname == "GeometryNodeStoreNamedAttribute" and n.inputs["Name"].default_value == "o_balloon_surface_structure")
|
||||
for link in list(store.inputs["Value"].links):
|
||||
group.links.remove(link)
|
||||
store.inputs["Value"].default_value = structure
|
||||
set_value("Thickness", thickness)
|
||||
set_value("Front Inflation", 0.0)
|
||||
baseline, faces = evaluated(obj)
|
||||
@@ -218,18 +247,15 @@ def test_normal_smooth_controls_front_and_rear_directions():
|
||||
assert np.max(abs(coordinates[1][list(ids)] - coordinates[0][list(ids)])) > 1e-5
|
||||
|
||||
|
||||
def test_balloon_suppression_is_stronger_without_a_startup_threshold():
|
||||
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)
|
||||
xy = np.array([v.co[:2] for v in obj.data.vertices])
|
||||
profile = 0.3 * np.sin(np.pi * xy[:, 0] / 2) * np.sin(np.pi * xy[:, 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
|
||||
store = next(n for n in group.nodes if n.bl_idname == "GeometryNodeStoreNamedAttribute" and n.inputs["Name"].default_value == "o_balloon_surface_structure")
|
||||
for link in list(store.inputs["Value"].links):
|
||||
group.links.remove(link)
|
||||
store.inputs["Value"].default_value = 1
|
||||
set_value("Mode", 0)
|
||||
set_value("Edge Round", 0)
|
||||
thickness_id = modifier_input_identifier(group, "Thickness", subtype="NONE")
|
||||
|
||||
@@ -144,6 +144,22 @@ def test_relief_reads_z_and_keeps_the_base(relief, thickness):
|
||||
assert obj.modifiers[0][inputs["Thickness"].identifier] == pytest.approx(thickness)
|
||||
|
||||
|
||||
@pytest.mark.parametrize("reference", [0.5, 1.25, 2.0])
|
||||
def test_zero_thickness_relief_keeps_far_depth_at_object_space_zero(relief, reference):
|
||||
obj, set_value, _, _, _ = relief
|
||||
base = evaluated(obj)
|
||||
texel_x = np.clip((base[0][:, 0] + 2) / 4 * 16, 0.5, 15.5)
|
||||
depth = (2 + texel_x / 16) * 0.5
|
||||
expected = np.maximum((reference - depth) * 1.5, 0)
|
||||
set_value("Reference Depth", reference)
|
||||
set_value("Depth Scale", 1.5)
|
||||
result = evaluated(obj)
|
||||
np.testing.assert_allclose(result[0][:, 2], expected, atol=1e-6)
|
||||
np.testing.assert_array_equal(result[0][:, :2], base[0][:, :2])
|
||||
assert result[1] == base[1]
|
||||
np.testing.assert_array_equal(result[2], base[2])
|
||||
|
||||
|
||||
@pytest.mark.parametrize("normal_smooth", [0, 2, 50])
|
||||
def test_curved_shell_smooths_normals_without_shrinking_thickness(surface, normal_smooth):
|
||||
from scipy.spatial import cKDTree
|
||||
@@ -197,7 +213,7 @@ def test_surface_interface_and_node_structure(surface):
|
||||
assert inputs["Split Threshold"].subtype == "FACTOR"
|
||||
assert inputs["Split Threshold"].parent == inputs["Smooth"].parent
|
||||
assert inputs["Split Threshold"].parent.name != "Options"
|
||||
assert inputs["Validity Threshold"].default_value == pytest.approx(0.1)
|
||||
assert inputs["Validity Threshold"].default_value == pytest.approx(0.9)
|
||||
assert inputs["Validity Threshold"].parent.name == "Options"
|
||||
assert inputs["Validity Threshold"].subtype == "FACTOR"
|
||||
assert inputs["Valid Only"].default_value is True
|
||||
@@ -223,9 +239,9 @@ def test_relief_interface_and_node_structure(relief):
|
||||
obj, _, _, inputs, _ = relief
|
||||
assert list(inputs) == ["Geometry", "Subdivide", "Thickness", "Depth Scale",
|
||||
"Reference Depth", "Uniform Scale", "Depth Image"]
|
||||
assert inputs["Thickness"].default_value == 0
|
||||
assert inputs["Thickness"].default_value == 0.5
|
||||
assert inputs["Subdivide"].default_value == 6
|
||||
assert inputs["Depth Scale"].default_value == 1
|
||||
assert inputs["Depth Scale"].default_value == 0.5
|
||||
assert all(item.structure_type == "SINGLE" for item in inputs.values())
|
||||
assert inputs["Thickness"].subtype == inputs["Reference Depth"].subtype == "DISTANCE"
|
||||
assert inputs["Uniform Scale"].hide_in_modifier and inputs["Depth Image"].hide_in_modifier
|
||||
|
||||
@@ -200,7 +200,7 @@ def test_stored_point_positions_prevent_depth_resampling_on_side_walls():
|
||||
|
||||
@pytest.mark.parametrize("thickness", [0.0, 0.2])
|
||||
@pytest.mark.parametrize("split", [0.0, 0.5])
|
||||
def test_temporary_attributes_are_removed_and_user_attributes_survive(thickness, split):
|
||||
def test_internal_fields_stay_anonymous_and_user_attributes_survive(thickness, split):
|
||||
obj, set_value = surface(wrapper=True)
|
||||
obj.data.attributes.new("o_depth_probe", "FLOAT", "POINT")
|
||||
attribute = obj.data.attributes.new("o_user_weight", "FLOAT", "POINT")
|
||||
@@ -212,7 +212,7 @@ def test_temporary_attributes_are_removed_and_user_attributes_survive(thickness,
|
||||
result = obj.evaluated_get(bpy.context.evaluated_depsgraph_get())
|
||||
mesh = result.to_mesh()
|
||||
try:
|
||||
assert not any(a.name.startswith("o_depth_") for a in mesh.attributes)
|
||||
assert {a.name for a in mesh.attributes if a.name.startswith(("o_depth_", "o_balloon_surface_"))} == {"o_depth_probe"}
|
||||
assert "UVMap" in mesh.attributes
|
||||
assert all(
|
||||
v.value == pytest.approx(0.75)
|
||||
|
||||
@@ -15,6 +15,8 @@ def test_saved_asset_inventory():
|
||||
assert node_group.use_fake_user and node_group.users > 0
|
||||
if name in GEOMETRY_NAMES:
|
||||
assert node_group.is_modifier
|
||||
stores = [node for node in node_group.nodes if node.bl_idname == "GeometryNodeStoreNamedAttribute"]
|
||||
assert all(node.inputs["Name"].default_value in {"UVMap", "o_normal_scale"} for node in stores), name
|
||||
if bpy.app.version >= (5, 0, 0):
|
||||
assert node_group.library is not None
|
||||
if name in {"O Image Depth Layer", "O Shadeless"}:
|
||||
|
||||
Reference in New Issue
Block a user