feat: Implement weight cutout boundary smoothing for image depth cutout

- Added a new proposal for boundary smoothing in the weight cutout process, differentiating between Split edges and Outline smoothing requirements.
- Introduced a new capability `cutout-boundary-smoothing` to control smoothing strength and default values.
- Updated specifications to define the behavior of boundary-specific smoothing strength and default settings.
- Created tasks to verify the implementation of boundary behavior and asset updates.
- Developed comprehensive tests for depth symmetry and normal scaling to ensure expected behavior and performance.
- Introduced new attributes for normal mapping to facilitate persistent face attributes sharing between depth symmetry and image materials.
- Built a new node group for O Image Depth Symmetry, integrating the new smoothing capabilities and ensuring compatibility with existing workflows.
This commit is contained in:
2026-09-14 12:44:20 +08:00
parent 09778c4b48
commit dc000e50b1
53 changed files with 1350 additions and 364 deletions

View File

@@ -175,22 +175,10 @@ def test_balloon_build_starts_from_the_unified_single_sheet():
profile_attribute.data.foreach_get("value", profile)
assert profile.min() == 0.0 and profile.max() > 0.0
material_tree = result.data.materials[0].node_tree
normal_scale_attribute = next(
node
for node in material_tree.nodes
if node.bl_idname == "ShaderNodeAttribute"
)
assert normal_scale_attribute.attribute_type == "GEOMETRY"
assert normal_scale_attribute.attribute_name == cutout_shape.NORMAL_SCALE_ATTRIBUTE_NAME
assert not any(node.bl_idname == "ShaderNodeAttribute" for node in material_tree.nodes)
image_layer = _group_node(material_data.material_node_group(), material_tree)
normal_scale_link = next(
link
for link in material_tree.links
if link.to_node == image_layer
and link.to_socket == image_layer.inputs["Normal Scale"]
)
assert normal_scale_link.from_node == normal_scale_attribute
assert normal_scale_link.from_socket == normal_scale_attribute.outputs["Fac"]
assert not image_layer.inputs["Normal Scale"].is_linked
assert image_layer.inputs["Normal Scale"].default_value == 1.0
assert result.modifiers[0].node_group.name == cutout_shape.CUTOUT_NODE_GROUP_NAMES["SOLID"]
inward = result.matrix_world.to_3x3() @ Vector((0.0, 0.0, -1.0))
assert np.allclose(tuple(inward), (0.0, 0.0, -1.0), atol=1e-6)
@@ -219,7 +207,7 @@ def test_teddy_preference_writes_the_shared_balloon_attribute():
profile = np.empty(len(result.data.vertices), dtype=np.float32)
attribute.data.foreach_get("value", profile)
assert profile.min() == 0.0 and profile.max() > 0.0
assert any(
assert not any(
node.bl_idname == "ShaderNodeAttribute"
for node in result.data.materials[0].node_tree.nodes
)
@@ -585,10 +573,11 @@ def test_cutout_cleanup_uses_absolute_area_with_many_small_islands():
assert len(_evaluated_positions(obj)) == 9
def test_depth_balloon_uses_metric_plane_inputs():
@pytest.mark.parametrize("gesture", ("LASSO", "POLYLINE"))
def test_depth_symmetry_uses_metric_plane_inputs(gesture):
bpy.ops.wm.read_factory_settings(use_empty=True)
image = bpy.data.images.new("Depth Balloon", width=100, height=100, alpha=True)
source = bpy.data.objects.new("Depth Balloon", None)
image = bpy.data.images.new("Depth Symmetry", width=100, height=100, alpha=True)
source = bpy.data.objects.new("Depth Symmetry", None)
source.empty_display_type = "IMAGE"
source.data = image
source.empty_display_size = 2.0
@@ -619,12 +608,13 @@ def test_depth_balloon_uses_metric_plane_inputs():
selection_values[:, :2] = 0.0
result = _create_cutout_shape(
source,
"DEPTH_BALLOON",
"DEPTH_SYMMETRY",
0.1,
depth_image=depth_image,
depth_metadata=depth_metadata,
bounds=crop_bounds,
selection_values=selection_values,
gesture=gesture,
)
assert all(abs(vertex.co.z) < 1e-8 for vertex in result.data.vertices)
@@ -634,6 +624,9 @@ def test_depth_balloon_uses_metric_plane_inputs():
result.name,
}
modifier = result.modifiers[0]
mode_id = object_data.modifier_input_identifier(modifier.node_group, "Mode")
assert modifier_value(modifier, mode_id) == 1
object_data.set_modifier_input(modifier, mode_id, 0)
depth_image_id = object_data.modifier_input_identifier(
modifier.node_group,
"Depth Image",