refactor: Update cutout and depth handling by removing unused functions and adjusting parameters
This commit is contained in:
Binary file not shown.
@@ -15,27 +15,22 @@ def test_depth_presets_always_generate_the_correct_normal_space(enabled):
|
|||||||
assert normal_mode_for_shape(shape, enabled) == ("TANGENT" if enabled else "NONE")
|
assert normal_mode_for_shape(shape, enabled) == ("TANGENT" if enabled else "NONE")
|
||||||
|
|
||||||
|
|
||||||
def test_new_cutout_does_not_reuse_a_saved_wrapper():
|
def test_new_cutout_reuses_a_saved_wrapper():
|
||||||
bpy.ops.wm.read_factory_settings(use_empty=True)
|
bpy.ops.wm.read_factory_settings(use_empty=True)
|
||||||
saved = bpy.data.node_groups.new("O Image Cutout", "GeometryNodeTree")
|
saved = bpy.data.node_groups.new("O Image Cutout", "GeometryNodeTree")
|
||||||
saved.interface.new_socket(name="Shape", in_out="INPUT", socket_type="NodeSocketMenu")
|
saved.interface.new_socket(name="Shape", in_out="INPUT", socket_type="NodeSocketMenu")
|
||||||
saved.use_fake_user = True
|
saved.use_fake_user = True
|
||||||
current = load_cutout_node_group("SOLID")
|
assert load_cutout_node_group("SOLID") == saved
|
||||||
assert current != saved and current.get("anyimage_asset_revision") == 13
|
assert load_cutout_node_group("FLAT") == saved
|
||||||
assert saved in bpy.data.node_groups.values()
|
assert saved.name == "O Image Cutout"
|
||||||
assert current == load_cutout_node_group("FLAT")
|
|
||||||
assert any(item.name == "Shape" for item in saved.interface.items_tree)
|
assert any(item.name == "Shape" for item in saved.interface.items_tree)
|
||||||
|
|
||||||
|
|
||||||
def test_depth_cutout_loads_uniform_thickness_revision_and_preserves_existing_group():
|
def test_depth_cutout_reuses_existing_group():
|
||||||
bpy.ops.wm.read_factory_settings(use_empty=True)
|
bpy.ops.wm.read_factory_settings(use_empty=True)
|
||||||
saved = bpy.data.node_groups.new("O Image Depth Cutout", "GeometryNodeTree")
|
saved = bpy.data.node_groups.new("O Image Depth Cutout", "GeometryNodeTree")
|
||||||
saved["anyimage_asset_revision"] = 11
|
assert load_cutout_node_group("DEPTH_SOLID") == saved
|
||||||
current = load_cutout_node_group("DEPTH_SOLID")
|
assert saved.name == "O Image Depth Cutout"
|
||||||
assert current != saved and current["anyimage_asset_revision"] == 26
|
|
||||||
assert saved in bpy.data.node_groups.values()
|
|
||||||
assert saved["anyimage_asset_revision"] == 11
|
|
||||||
assert load_cutout_node_group("DEPTH_SOLID") == current
|
|
||||||
|
|
||||||
|
|
||||||
def test_small_quality_mesh_has_positive_balloon_support():
|
def test_small_quality_mesh_has_positive_balloon_support():
|
||||||
|
|||||||
@@ -71,7 +71,7 @@ def test_camera_blur_preserves_rays_interior_protection_and_disconnected_surface
|
|||||||
|
|
||||||
|
|
||||||
|
|
||||||
def test_cutout_fine_outline_blurs_depth_without_moving_projection():
|
def test_cutout_fine_outline_smooths_only_within_boundary_band():
|
||||||
from tests.support.depth_surface import surface
|
from tests.support.depth_surface import surface
|
||||||
from anyimage.operators.cutout_tool.mesh import build_mesh
|
from anyimage.operators.cutout_tool.mesh import build_mesh
|
||||||
|
|
||||||
@@ -90,19 +90,16 @@ def test_cutout_fine_outline_blurs_depth_without_moving_projection():
|
|||||||
image.pixels.foreach_set(pixels.ravel())
|
image.pixels.foreach_set(pixels.ravel())
|
||||||
image.update()
|
image.update()
|
||||||
set_value("Boundary Smooth", 0)
|
set_value("Boundary Smooth", 0)
|
||||||
set_value("Smooth Weight", 0)
|
set_value("Smooth Weight", 1)
|
||||||
before, original_faces = evaluated(obj)
|
before, original_faces = evaluated(obj)
|
||||||
set_value("Boundary Smooth", 12)
|
set_value("Boundary Smooth", 12)
|
||||||
after, result_faces = evaluated(obj)
|
after, result_faces = evaluated(obj)
|
||||||
assert result_faces == original_faces
|
assert result_faces == original_faces
|
||||||
assert after[:, 2].min() > before[:, 2].min() + 1
|
|
||||||
# Reference Depth = 1, Uniform Scale = 1 and Depth Scale = 1.
|
|
||||||
np.testing.assert_allclose(after[:, :2] / (1 - after[:, 2:3]),
|
|
||||||
before[:, :2] / (1 - before[:, 2:3]), atol=1e-6)
|
|
||||||
band = edge_band(original_faces, boundary_neighbors(original_faces))
|
band = edge_band(original_faces, boundary_neighbors(original_faces))
|
||||||
interior = [i for i in range(len(before)) if i not in band]
|
interior = [i for i in range(len(before)) if i not in band]
|
||||||
assert interior
|
assert interior
|
||||||
np.testing.assert_array_equal(after[interior], before[interior])
|
np.testing.assert_array_equal(after[interior], before[interior])
|
||||||
|
assert np.max(np.linalg.norm(after - before, axis=1)) > 1e-4
|
||||||
set_value("Boundary Smooth", 0)
|
set_value("Boundary Smooth", 0)
|
||||||
np.testing.assert_array_equal(evaluated(obj)[0], before)
|
np.testing.assert_array_equal(evaluated(obj)[0], before)
|
||||||
|
|
||||||
|
|||||||
@@ -62,7 +62,7 @@ def test_smoothing_reduces_stairs_without_leaving_two_ring_band(triangles):
|
|||||||
band = edge_band(faces, boundary)
|
band = edge_band(faces, boundary)
|
||||||
movable = np.array([i in band for i in range(len(original))])
|
movable = np.array([i in band for i in range(len(original))])
|
||||||
assert movable.any() and (~movable).any()
|
assert movable.any() and (~movable).any()
|
||||||
set_value("Boundary Smooth", 20)
|
set_value("Boundary Smooth", 16)
|
||||||
smoothed, result_faces = evaluated(obj)
|
smoothed, result_faces = evaluated(obj)
|
||||||
assert result_faces == faces
|
assert result_faces == faces
|
||||||
np.testing.assert_array_equal(smoothed[~movable], original[~movable])
|
np.testing.assert_array_equal(smoothed[~movable], original[~movable])
|
||||||
@@ -85,11 +85,11 @@ def test_smoothing_reduces_stairs_without_leaving_two_ring_band(triangles):
|
|||||||
np.testing.assert_array_equal(evaluated(obj)[0], original)
|
np.testing.assert_array_equal(evaluated(obj)[0], original)
|
||||||
|
|
||||||
|
|
||||||
def test_smoothing_without_split_moves_original_boundary_only_within_band():
|
def test_smoothing_without_split_moves_outline_only_within_boundary_band():
|
||||||
obj, set_value = diagonal_surface()
|
obj, set_value = diagonal_surface()
|
||||||
set_value("Depth Split", 0)
|
set_value("Depth Split", 0)
|
||||||
original, faces = evaluated(obj)
|
original, faces = evaluated(obj)
|
||||||
set_value("Boundary Smooth", 20)
|
set_value("Boundary Smooth", 16)
|
||||||
result, result_faces = evaluated(obj)
|
result, result_faces = evaluated(obj)
|
||||||
boundary = boundary_neighbors(faces)
|
boundary = boundary_neighbors(faces)
|
||||||
band = edge_band(faces, boundary)
|
band = edge_band(faces, boundary)
|
||||||
@@ -97,6 +97,8 @@ def test_smoothing_without_split_moves_original_boundary_only_within_band():
|
|||||||
assert np.max(np.linalg.norm(result[list(boundary)] - original[list(boundary)], axis=1)) > 1e-4
|
assert np.max(np.linalg.norm(result[list(boundary)] - original[list(boundary)], axis=1)) > 1e-4
|
||||||
np.testing.assert_array_equal(result[outside], original[outside])
|
np.testing.assert_array_equal(result[outside], original[outside])
|
||||||
assert result_faces == faces
|
assert result_faces == faces
|
||||||
|
set_value("Boundary Smooth", 0)
|
||||||
|
np.testing.assert_array_equal(evaluated(obj)[0], original)
|
||||||
|
|
||||||
|
|
||||||
@pytest.mark.parametrize("mode", [0, 1])
|
@pytest.mark.parametrize("mode", [0, 1])
|
||||||
@@ -147,7 +149,7 @@ def test_plane_edge_smoothing_preserves_uvs_and_closes_thickness():
|
|||||||
from tests.support.planes import create_surface, evaluated as plane_mesh
|
from tests.support.planes import create_surface, evaluated as plane_mesh
|
||||||
|
|
||||||
obj, set_value, _, inputs, image = create_surface("DEPTH")
|
obj, set_value, _, inputs, image = create_surface("DEPTH")
|
||||||
assert inputs["Boundary Smooth"].default_value == 2
|
assert inputs["Boundary Smooth"].default_value == 4
|
||||||
assert inputs["Boundary Smooth"].parent.name == "Options"
|
assert inputs["Boundary Smooth"].parent.name == "Options"
|
||||||
pixels = np.array(image.pixels[:], np.float32).reshape(8, 16, 4)
|
pixels = np.array(image.pixels[:], np.float32).reshape(8, 16, 4)
|
||||||
yy, xx = np.mgrid[:8, :16]
|
yy, xx = np.mgrid[:8, :16]
|
||||||
|
|||||||
@@ -37,7 +37,7 @@ def test_boundary_triangle_cleanup_is_one_pass():
|
|||||||
assert all(len(f) == 3 for f in faces)
|
assert all(len(f) == 3 for f in faces)
|
||||||
|
|
||||||
|
|
||||||
@pytest.mark.parametrize("iterations", [0, 20])
|
@pytest.mark.parametrize("iterations", [0, 16])
|
||||||
def test_cleaned_sawtooth_smoothing_preserves_face_orientation(iterations):
|
def test_cleaned_sawtooth_smoothing_preserves_face_orientation(iterations):
|
||||||
obj, set_value = diagonal_surface(triangles=True)
|
obj, set_value = diagonal_surface(triangles=True)
|
||||||
original, faces = evaluated(obj)
|
original, faces = evaluated(obj)
|
||||||
|
|||||||
@@ -20,6 +20,36 @@ def store_test_fields(group, geometry, fields):
|
|||||||
return geometry
|
return geometry
|
||||||
|
|
||||||
|
|
||||||
|
@pytest.mark.parametrize("mode", [0, 1])
|
||||||
|
def test_normal_smooth_changes_thickness_direction_without_moving_zero_thickness(mode):
|
||||||
|
from tests.support.depth_surface import surface, evaluated
|
||||||
|
|
||||||
|
obj, set_value = surface(step=1, resolution=10)
|
||||||
|
xy = np.array([v.co[:2] for v in obj.data.vertices])
|
||||||
|
profile = .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)
|
||||||
|
set_value("Mode", mode)
|
||||||
|
set_value("Front Inflation", .2)
|
||||||
|
modifier = obj.modifiers[0]
|
||||||
|
thickness = modifier_input_identifier(modifier.node_group, "Thickness", subtype="DISTANCE" if mode else "NONE")
|
||||||
|
|
||||||
|
def result(amount, smoothing):
|
||||||
|
set_modifier_input(modifier, thickness, amount)
|
||||||
|
set_value("Normal Smooth", smoothing)
|
||||||
|
return evaluated(obj)
|
||||||
|
|
||||||
|
raw, faces = result(0, 16)
|
||||||
|
smooth, smooth_faces = result(0, 512)
|
||||||
|
np.testing.assert_array_equal(raw, smooth)
|
||||||
|
assert faces == smooth_faces
|
||||||
|
raw, faces = result(1, 16)
|
||||||
|
smooth, smooth_faces = result(1, 512)
|
||||||
|
assert faces == smooth_faces
|
||||||
|
assert np.isfinite(smooth).all()
|
||||||
|
assert np.max(np.abs(raw - smooth)) > 1e-5
|
||||||
|
|
||||||
|
|
||||||
def set_test_structure(monkeypatch, value):
|
def set_test_structure(monkeypatch, value):
|
||||||
"""Control structural amplitude without depending on storage nodes."""
|
"""Control structural amplitude without depending on storage nodes."""
|
||||||
from tools.nodes.groups import image_depth_cutout
|
from tools.nodes.groups import image_depth_cutout
|
||||||
|
|||||||
@@ -25,7 +25,7 @@ def test_saved_parameter_descriptions_match_definitions():
|
|||||||
"O Image Cutout": {"Thickness", "Smooth", "Smooth Weight"},
|
"O Image Cutout": {"Thickness", "Smooth", "Smooth Weight"},
|
||||||
"O Image Depth Cutout": {
|
"O Image Depth Cutout": {
|
||||||
"Thickness", "Depth Split", "Smooth", "Smooth Weight",
|
"Thickness", "Depth Split", "Smooth", "Smooth Weight",
|
||||||
"Boundary Smooth", "Outline Depth Fix", "Front Inflation",
|
"Boundary Smooth", "Normal Smooth", "Front Inflation",
|
||||||
},
|
},
|
||||||
"O Image Depth Balloon": {"Thickness", "Smooth", "Smooth Weight"},
|
"O Image Depth Balloon": {"Thickness", "Smooth", "Smooth Weight"},
|
||||||
"O Image Depth Panorama": {
|
"O Image Depth Panorama": {
|
||||||
@@ -48,6 +48,12 @@ def test_saved_parameter_descriptions_match_definitions():
|
|||||||
]
|
]
|
||||||
assert {item.name for item in sockets} == parameters[group.name]
|
assert {item.name for item in sockets} == parameters[group.name]
|
||||||
assert all(item.description for item in sockets), group.name
|
assert all(item.description for item in sockets), group.name
|
||||||
|
if group.name in {"O Image Depth Plane", "O Image Depth Cutout"}:
|
||||||
|
boundary = next(item for item in sockets if item.name == "Boundary Smooth")
|
||||||
|
assert boundary.default_value == (4 if group.name == "O Image Depth Plane" else 0)
|
||||||
|
assert boundary.min_value == 0
|
||||||
|
assert boundary.max_value == 16
|
||||||
|
assert boundary.parent.name == "Options"
|
||||||
if group.name == "O Image Depth Panorama":
|
if group.name == "O Image Depth Panorama":
|
||||||
defaults = {item.name: item.default_value for item in sockets
|
defaults = {item.name: item.default_value for item in sockets
|
||||||
if item.name in {"Subdivide", "Depth Split", "Boundary Smooth"}}
|
if item.name in {"Subdivide", "Depth Split", "Boundary Smooth"}}
|
||||||
@@ -66,8 +72,16 @@ def test_saved_parameter_descriptions_match_definitions():
|
|||||||
thickness = [item.description for item in sockets if item.name == "Thickness"]
|
thickness = [item.description for item in sockets if item.name == "Thickness"]
|
||||||
assert len(thickness) == 2 and len(set(thickness)) == 2
|
assert len(thickness) == 2 and len(set(thickness)) == 2
|
||||||
if group.name == "O Image Depth Cutout":
|
if group.name == "O Image Depth Cutout":
|
||||||
|
assert not {"Outline Depth Fix", "Center Smooth", "Center Subdivide", "Show Center"} & {
|
||||||
|
item.name for item in group.interface.items_tree if item.item_type == "SOCKET"
|
||||||
|
}
|
||||||
inflation = next(item for item in sockets if item.name == "Front Inflation")
|
inflation = next(item for item in sockets if item.name == "Front Inflation")
|
||||||
assert inflation.default_value == 0.0
|
assert inflation.default_value == pytest.approx(0.2)
|
||||||
|
normal_smooth = next(item for item in sockets if item.name == "Normal Smooth")
|
||||||
|
assert normal_smooth.default_value == 64
|
||||||
|
assert normal_smooth.min_value == 16
|
||||||
|
assert normal_smooth.max_value == 512
|
||||||
|
assert normal_smooth.parent.name == "Options"
|
||||||
result[group.name] = [
|
result[group.name] = [
|
||||||
(item.identifier, item.name, item.description) for item in sockets
|
(item.identifier, item.name, item.description) for item in sockets
|
||||||
]
|
]
|
||||||
|
|||||||
@@ -1,104 +0,0 @@
|
|||||||
"""Check adaptive outline batches against a numerical graph reference."""
|
|
||||||
|
|
||||||
from collections import Counter
|
|
||||||
|
|
||||||
import bpy
|
|
||||||
import numpy as np
|
|
||||||
import pytest
|
|
||||||
|
|
||||||
from tests.support.depth_surface import evaluated
|
|
||||||
from tools.nodes.common.outline_smoothing import smooth_outline_camera
|
|
||||||
from tools.nodes.common.depth_surface import CUT_ATTRIBUTE
|
|
||||||
|
|
||||||
|
|
||||||
@pytest.mark.parametrize("steps", [1, 4])
|
|
||||||
def test_outline_batches_update_weights_preserve_split_and_camera_rays(steps):
|
|
||||||
bpy.ops.wm.read_factory_settings(use_empty=True)
|
|
||||||
points = np.array([(x, y, 0) for y in range(9) for x in range(9)], dtype=float)
|
|
||||||
faces = [(y*9+x, y*9+x+1, (y+1)*9+x+1, (y+1)*9+x) for y in range(8) for x in range(8)]
|
|
||||||
deleted = [i % 8 == 4 for i in range(len(faces))]
|
|
||||||
result_faces = [face for face, remove in zip(faces, deleted) if not remove]
|
|
||||||
cut = np.isin(points[:, 0], [4, 5])
|
|
||||||
depth = np.full(81, 2.0)
|
|
||||||
depth[2] = 20
|
|
||||||
depth[80] = 0
|
|
||||||
camera = np.column_stack((points[:, :2]*depth[:, None]/10, depth))
|
|
||||||
distance = np.linalg.norm(camera, axis=1)
|
|
||||||
mesh = bpy.data.meshes.new("Split outline")
|
|
||||||
mesh.from_pydata(points.tolist(), [], faces)
|
|
||||||
for name, kind, domain, values, prop in (
|
|
||||||
("test_camera", "FLOAT_VECTOR", "POINT", camera.ravel(), "vector"),
|
|
||||||
(CUT_ATTRIBUTE, "FLOAT", "POINT", cut, "value"),
|
|
||||||
("test_deleted", "BOOLEAN", "FACE", deleted, "value"),
|
|
||||||
):
|
|
||||||
mesh.attributes.new(name, kind, domain).data.foreach_set(prop, values)
|
|
||||||
obj = bpy.data.objects.new("Outline", mesh)
|
|
||||||
bpy.context.collection.objects.link(obj)
|
|
||||||
group = bpy.data.node_groups.new("Adaptive outline test", "GeometryNodeTree")
|
|
||||||
for direction in ("INPUT", "OUTPUT"):
|
|
||||||
group.interface.new_socket(name="Geometry", in_out=direction, socket_type="NodeSocketGeometry")
|
|
||||||
ids = {}
|
|
||||||
for name in ("Outline Depth Fix", "Boundary Smooth"):
|
|
||||||
ids[name] = group.interface.new_socket(name=name, in_out="INPUT", socket_type="NodeSocketInt").identifier
|
|
||||||
source = group.nodes.new("NodeGroupInput")
|
|
||||||
attributes = {}
|
|
||||||
for name, kind in (("test_camera", "FLOAT_VECTOR"), ("test_deleted", "BOOLEAN")):
|
|
||||||
node = group.nodes.new("GeometryNodeInputNamedAttribute")
|
|
||||||
node.data_type = kind
|
|
||||||
node.inputs["Name"].default_value = name
|
|
||||||
attributes[name] = node.outputs["Attribute"]
|
|
||||||
delete = group.nodes.new("GeometryNodeDeleteGeometry")
|
|
||||||
delete.domain, delete.mode = "FACE", "ONLY_FACE"
|
|
||||||
group.links.new(source.outputs["Geometry"], delete.inputs["Geometry"])
|
|
||||||
group.links.new(attributes["test_deleted"], delete.inputs["Selection"])
|
|
||||||
scale = group.nodes.new("ShaderNodeValue")
|
|
||||||
scale.outputs[0].default_value = 1
|
|
||||||
result = smooth_outline_camera(group, source.outputs["Geometry"], delete.outputs[0], attributes["test_camera"], scale.outputs[0])
|
|
||||||
position = group.nodes.new("GeometryNodeSetPosition")
|
|
||||||
group.links.new(delete.outputs[0], position.inputs["Geometry"])
|
|
||||||
group.links.new(result, position.inputs["Position"])
|
|
||||||
output = group.nodes.new("NodeGroupOutput")
|
|
||||||
group.links.new(position.outputs[0], output.inputs["Geometry"])
|
|
||||||
modifier = obj.modifiers.new("Outline", "NODES")
|
|
||||||
modifier.node_group = group
|
|
||||||
|
|
||||||
# ONLY_FACE retains the separating column's edges in the current topology.
|
|
||||||
edges = np.array([tuple(e.vertices) for e in mesh.edges])
|
|
||||||
adjacency = np.zeros((81, 81))
|
|
||||||
adjacency[edges[:, 0], edges[:, 1]] = 1
|
|
||||||
adjacency += adjacency.T
|
|
||||||
degree = adjacency.sum(axis=1)
|
|
||||||
def blur(value, weight):
|
|
||||||
return (value + weight*(adjacency @ value))/(1 + weight*degree)
|
|
||||||
counts = Counter(tuple(sorted((a, b))) for f in result_faces for a, b in zip(f, (*f[1:], f[0])))
|
|
||||||
boundary = np.zeros(81)
|
|
||||||
for edge, count in counts.items():
|
|
||||||
if count == 1:
|
|
||||||
boundary[list(edge)] = 1
|
|
||||||
protected = boundary.astype(bool) & cut
|
|
||||||
seed = boundary*(~protected)
|
|
||||||
influence = np.maximum(seed, blur(blur(seed, 1), 1))*(~protected)
|
|
||||||
influence *= depth > 1e-8
|
|
||||||
for rounds, batch in ((0, steps), (16, 0), (16, steps)):
|
|
||||||
modifier[ids["Outline Depth Fix"]] = rounds
|
|
||||||
modifier[ids["Boundary Smooth"]] = batch
|
|
||||||
obj.update_tag(refresh={"DATA"})
|
|
||||||
bpy.context.view_layer.update()
|
|
||||||
actual, actual_faces = evaluated(obj)
|
|
||||||
expected = distance.copy()
|
|
||||||
for _ in range(rounds):
|
|
||||||
ratio = abs(expected-blur(expected, .5))
|
|
||||||
t = np.clip((ratio-.25)/.25, 0, 1)
|
|
||||||
weight = t*t*(3-2*t)*influence
|
|
||||||
for _ in range(batch):
|
|
||||||
expected = blur(expected, weight)
|
|
||||||
np.testing.assert_allclose(np.linalg.norm(actual, axis=1), expected, atol=2e-5)
|
|
||||||
assert actual_faces == result_faces
|
|
||||||
np.testing.assert_array_equal(actual[influence == 0], camera[influence == 0].astype(np.float32))
|
|
||||||
if rounds and batch:
|
|
||||||
assert 2 < actual[2, 2] < 15
|
|
||||||
valid = depth > 0
|
|
||||||
np.testing.assert_allclose(actual[valid, :2]/actual[valid, 2:3], camera[valid, :2]/camera[valid, 2:3], atol=1e-6)
|
|
||||||
else:
|
|
||||||
np.testing.assert_array_equal(actual, camera.astype(np.float32))
|
|
||||||
assert not any(n.bl_idname in {"GeometryNodeCaptureAttribute", "GeometryNodeStoreNamedAttribute"} for n in group.nodes)
|
|
||||||
@@ -20,8 +20,8 @@ def remove_boundary_triangles(group, geometry):
|
|||||||
return delete.outputs["Geometry"]
|
return delete.outputs["Geometry"]
|
||||||
|
|
||||||
|
|
||||||
def taper_split_profile(group, original, geometry):
|
def original_boundary_field(group, original):
|
||||||
"""Taper balloon thickness through the shared two-ring influence builder."""
|
"""Sample the unsplit outline in UV space after projection and separation."""
|
||||||
nodes, links = group.nodes, group.links
|
nodes, links = group.nodes, group.links
|
||||||
uv = nodes.new("GeometryNodeInputNamedAttribute")
|
uv = nodes.new("GeometryNodeInputNamedAttribute")
|
||||||
uv.data_type = "FLOAT_VECTOR"
|
uv.data_type = "FLOAT_VECTOR"
|
||||||
@@ -34,8 +34,13 @@ def taper_split_profile(group, original, geometry):
|
|||||||
links.new(original_uv.outputs["Geometry"], nearest.inputs["Geometry"])
|
links.new(original_uv.outputs["Geometry"], nearest.inputs["Geometry"])
|
||||||
links.new(uv.outputs["Attribute"], nearest.inputs["Sample Position"])
|
links.new(uv.outputs["Attribute"], nearest.inputs["Sample Position"])
|
||||||
rim = evaluate_field(group, edge_boundary_field(nodes, links), "FLOAT", "POINT")
|
rim = evaluate_field(group, edge_boundary_field(nodes, links), "FLOAT", "POINT")
|
||||||
protected = sample_field(group, original, rim, "FLOAT", index=nearest.outputs["Index"])
|
return sample_field(group, original, rim, "FLOAT", index=nearest.outputs["Index"])
|
||||||
influence = boundary_influence(group, protected)
|
|
||||||
|
|
||||||
|
def taper_split_profile(group, geometry, original_boundary):
|
||||||
|
"""Taper balloon thickness through the shared two-ring influence builder."""
|
||||||
|
nodes = group.nodes
|
||||||
|
influence = boundary_influence(group, original_boundary)
|
||||||
controls = group_input(nodes, {"Depth Split"})
|
controls = group_input(nodes, {"Depth Split"})
|
||||||
band = _math(group, "MULTIPLY", influence, _math(group, "GREATER_THAN", controls.outputs["Depth Split"], 0))
|
band = _math(group, "MULTIPLY", influence, _math(group, "GREATER_THAN", controls.outputs["Depth Split"], 0))
|
||||||
retained = _math(group, "SQRT", _math(group, "MAXIMUM", _math(group, "SUBTRACT", 1, band), 0))
|
retained = _math(group, "SQRT", _math(group, "MAXIMUM", _math(group, "SUBTRACT", 1, band), 0))
|
||||||
|
|||||||
@@ -1,78 +0,0 @@
|
|||||||
"""Build a shared center on the original Cutout mesh."""
|
|
||||||
|
|
||||||
|
|
||||||
from .nodes import evaluate_field, group_input, sample_field
|
|
||||||
|
|
||||||
|
|
||||||
def vector(group, operation, first, second=None):
|
|
||||||
node = group.nodes.new("ShaderNodeVectorMath")
|
|
||||||
node.operation = operation
|
|
||||||
for value, socket in ((first, node.inputs[0]), (second, node.inputs[3 if operation == "SCALE" else 1])):
|
|
||||||
if value is None:
|
|
||||||
continue
|
|
||||||
if hasattr(value, "node"):
|
|
||||||
group.links.new(value, socket)
|
|
||||||
else:
|
|
||||||
socket.default_value = value
|
|
||||||
return node.outputs[0]
|
|
||||||
|
|
||||||
|
|
||||||
def axes(group, value):
|
|
||||||
node = group.nodes.new("ShaderNodeSeparateXYZ")
|
|
||||||
group.links.new(value, node.inputs[0])
|
|
||||||
return node.outputs
|
|
||||||
|
|
||||||
|
|
||||||
def combine(group, x, y, z=0):
|
|
||||||
node = group.nodes.new("ShaderNodeCombineXYZ")
|
|
||||||
for value, socket in zip((x, y, z), node.inputs):
|
|
||||||
if hasattr(value, "node"):
|
|
||||||
group.links.new(value, socket)
|
|
||||||
else:
|
|
||||||
socket.default_value = value
|
|
||||||
return node.outputs[0]
|
|
||||||
|
|
||||||
|
|
||||||
def set_position(group, geometry, position):
|
|
||||||
node = group.nodes.new("GeometryNodeSetPosition")
|
|
||||||
group.links.new(geometry, node.inputs["Geometry"])
|
|
||||||
group.links.new(position, node.inputs["Position"])
|
|
||||||
return node.outputs[0]
|
|
||||||
|
|
||||||
|
|
||||||
def sample_surface(group, geometry, value, data_type, domain_position, query):
|
|
||||||
"""Keep point indices on points before the face interpolation adapts domains."""
|
|
||||||
field = evaluate_field(group, sample_field(group, geometry, value, data_type), data_type, "POINT")
|
|
||||||
flat = set_position(group, geometry, domain_position)
|
|
||||||
sample = group.nodes.new("GeometryNodeSampleNearestSurface")
|
|
||||||
sample.data_type = data_type
|
|
||||||
group.links.new(flat, sample.inputs["Mesh"])
|
|
||||||
group.links.new(field, sample.inputs["Value"])
|
|
||||||
group.links.new(query, sample.inputs["Sample Position"])
|
|
||||||
return sample.outputs[0]
|
|
||||||
|
|
||||||
|
|
||||||
def build_center_surface(group, unsplit):
|
|
||||||
"""Smooth the unsplit displaced surface without moving its XY coordinates."""
|
|
||||||
nodes, links = group.nodes, group.links
|
|
||||||
controls = group_input(nodes, {"Center Smooth"})
|
|
||||||
position = nodes.new("GeometryNodeInputPosition").outputs[0]
|
|
||||||
components = axes(group, position)
|
|
||||||
uv = nodes.new("GeometryNodeInputNamedAttribute")
|
|
||||||
uv.data_type = "FLOAT_VECTOR"
|
|
||||||
uv.inputs["Name"].default_value = "UVMap"
|
|
||||||
coordinates = uv.outputs["Attribute"]
|
|
||||||
blur = nodes.new("GeometryNodeBlurAttribute")
|
|
||||||
blur.data_type = "FLOAT"
|
|
||||||
links.new(components["Z"], blur.inputs["Value"])
|
|
||||||
links.new(controls.outputs["Center Smooth"], blur.inputs["Iterations"])
|
|
||||||
support = set_position(group, unsplit, combine(group, components["X"], components["Y"], blur.outputs[0]))
|
|
||||||
height = sample_surface(group, support, components["Z"], "FLOAT", coordinates, coordinates)
|
|
||||||
normal = nodes.new("GeometryNodeInputNormal").outputs[0]
|
|
||||||
normal_blur = nodes.new("GeometryNodeBlurAttribute")
|
|
||||||
normal_blur.data_type = "FLOAT_VECTOR"
|
|
||||||
links.new(evaluate_field(group, normal, "FLOAT_VECTOR", "POINT"), normal_blur.inputs["Value"])
|
|
||||||
links.new(controls.outputs["Center Smooth"], normal_blur.inputs["Iterations"])
|
|
||||||
normal = normal_blur.outputs[0]
|
|
||||||
normal = vector(group, "NORMALIZE", sample_surface(group, support, normal, "FLOAT_VECTOR", coordinates, coordinates))
|
|
||||||
return {"geometry": support, "preview": support, "height": height, "normal": normal}
|
|
||||||
@@ -1,98 +0,0 @@
|
|||||||
"""Adapt camera depth along the original outline with batched weight updates."""
|
|
||||||
|
|
||||||
from .boundary_smoothing import boundary_influence, reconstruct_camera_depth
|
|
||||||
from .depth_surface import CUT_ATTRIBUTE, _math, ray_distance
|
|
||||||
from .nodes import evaluate_field, group_input, read_float_attribute, sample_field
|
|
||||||
from .smoothing import edge_boundary_field
|
|
||||||
|
|
||||||
|
|
||||||
def _outline_influence(group, original, geometry):
|
|
||||||
"""Identify original outline points and protect split boundary intersections."""
|
|
||||||
nodes, links = group.nodes, group.links
|
|
||||||
rim = evaluate_field(group, edge_boundary_field(nodes, links), "FLOAT", "POINT")
|
|
||||||
nearest = nodes.new("GeometryNodeSampleNearest")
|
|
||||||
nearest.domain = "POINT"
|
|
||||||
links.new(original, nearest.inputs["Geometry"])
|
|
||||||
position = nodes.new("GeometryNodeInputPosition").outputs["Position"]
|
|
||||||
links.new(position, nearest.inputs["Sample Position"])
|
|
||||||
original_rim = sample_field(group, original, rim, "FLOAT", index=nearest.outputs["Index"])
|
|
||||||
original_rim = _math(group, "GREATER_THAN", original_rim, 0)
|
|
||||||
cut = _math(group, "GREATER_THAN", read_float_attribute(group, CUT_ATTRIBUTE), 0)
|
|
||||||
protected = _math(group, "MULTIPLY", _math(group, "GREATER_THAN", rim, 0),
|
|
||||||
_math(group, "MAXIMUM", cut, _math(group, "SUBTRACT", 1, original_rim)))
|
|
||||||
# Resolve planar correspondence before replacing positions with camera depth.
|
|
||||||
return sample_field(group, geometry, boundary_influence(group, protected), "FLOAT")
|
|
||||||
|
|
||||||
|
|
||||||
def smooth_outline_camera(group, original, geometry, camera, uniform_scale):
|
|
||||||
"""Update outline weights and blur ray distance in fixed-weight batches."""
|
|
||||||
nodes, links = group.nodes, group.links
|
|
||||||
controls = group_input(nodes, {"Outline Depth Fix", "Boundary Smooth"})
|
|
||||||
influence = _outline_influence(group, original, geometry)
|
|
||||||
depth = ray_distance(group, camera)
|
|
||||||
|
|
||||||
endpoints = nodes.new("GeometryNodeInputMeshEdgeVertices")
|
|
||||||
length = nodes.new("ShaderNodeVectorMath")
|
|
||||||
length.operation = "DISTANCE"
|
|
||||||
links.new(endpoints.outputs["Position 1"], length.inputs[0])
|
|
||||||
links.new(endpoints.outputs["Position 2"], length.inputs[1])
|
|
||||||
statistic = nodes.new("GeometryNodeAttributeStatistic")
|
|
||||||
statistic.data_type, statistic.domain = "FLOAT", "EDGE"
|
|
||||||
links.new(geometry, statistic.inputs["Geometry"])
|
|
||||||
links.new(length.outputs["Value"], statistic.inputs["Attribute"])
|
|
||||||
scale = _math(group, "DIVIDE", uniform_scale,
|
|
||||||
_math(group, "MAXIMUM", statistic.outputs["Median"], 1e-8))
|
|
||||||
|
|
||||||
seed = nodes.new("ShaderNodeCombineXYZ")
|
|
||||||
links.new(depth, seed.inputs["Z"])
|
|
||||||
carrier = nodes.new("GeometryNodeSetPosition")
|
|
||||||
links.new(geometry, carrier.inputs["Geometry"])
|
|
||||||
links.new(seed.outputs[0], carrier.inputs["Position"])
|
|
||||||
repeat_output = nodes.new("GeometryNodeRepeatOutput")
|
|
||||||
repeat_input = nodes.new("GeometryNodeRepeatInput")
|
|
||||||
repeat_input.pair_with_output(repeat_output)
|
|
||||||
links.new(carrier.outputs[0], repeat_input.inputs["Geometry"])
|
|
||||||
links.new(controls.outputs["Outline Depth Fix"], repeat_input.inputs["Iterations"])
|
|
||||||
|
|
||||||
position = nodes.new("GeometryNodeInputPosition")
|
|
||||||
current = nodes.new("ShaderNodeSeparateXYZ")
|
|
||||||
links.new(position.outputs[0], current.inputs[0])
|
|
||||||
reference = nodes.new("GeometryNodeBlurAttribute")
|
|
||||||
reference.data_type = "FLOAT"
|
|
||||||
reference.inputs["Iterations"].default_value = 1
|
|
||||||
reference.inputs["Weight"].default_value = 0.5
|
|
||||||
links.new(current.outputs["Z"], reference.inputs["Value"])
|
|
||||||
residual = _math(group, "ABSOLUTE", _math(group, "SUBTRACT", current.outputs["Z"], reference.outputs[0]))
|
|
||||||
ratio = _math(group, "MULTIPLY", residual, scale)
|
|
||||||
weight = nodes.new("ShaderNodeMapRange")
|
|
||||||
weight.interpolation_type, weight.clamp = "SMOOTHSTEP", True
|
|
||||||
weight.inputs["From Min"].default_value = 0.25
|
|
||||||
weight.inputs["From Max"].default_value = 0.5
|
|
||||||
links.new(ratio, weight.inputs["Value"])
|
|
||||||
active = _math(group, "MULTIPLY", influence, _math(group, "GREATER_THAN", depth, 1e-8))
|
|
||||||
active = sample_field(group, geometry, active, "FLOAT")
|
|
||||||
weighted = _math(group, "MULTIPLY", weight.outputs[0], active)
|
|
||||||
# Sampling the round's input prevents the inner Blur from updating its weight.
|
|
||||||
frozen = sample_field(group, repeat_input.outputs["Geometry"], weighted, "FLOAT")
|
|
||||||
blur = nodes.new("GeometryNodeBlurAttribute")
|
|
||||||
blur.data_type = "FLOAT"
|
|
||||||
links.new(current.outputs["Z"], blur.inputs["Value"])
|
|
||||||
links.new(frozen, blur.inputs["Weight"])
|
|
||||||
links.new(controls.outputs["Boundary Smooth"], blur.inputs["Iterations"])
|
|
||||||
updated = nodes.new("ShaderNodeCombineXYZ")
|
|
||||||
links.new(blur.outputs[0], updated.inputs["Z"])
|
|
||||||
move = nodes.new("GeometryNodeSetPosition")
|
|
||||||
links.new(repeat_input.outputs["Geometry"], move.inputs["Geometry"])
|
|
||||||
links.new(updated.outputs[0], move.inputs["Position"])
|
|
||||||
links.new(move.outputs[0], repeat_output.inputs["Geometry"])
|
|
||||||
|
|
||||||
result = sample_field(group, repeat_output.outputs["Geometry"], current.outputs["Z"], "FLOAT")
|
|
||||||
enabled = _math(group, "MULTIPLY", active,
|
|
||||||
_math(group, "MULTIPLY", controls.outputs["Outline Depth Fix"],
|
|
||||||
controls.outputs["Boundary Smooth"]))
|
|
||||||
choose = nodes.new("GeometryNodeSwitch")
|
|
||||||
choose.input_type = "FLOAT"
|
|
||||||
links.new(enabled, choose.inputs["Switch"])
|
|
||||||
links.new(depth, choose.inputs["False"])
|
|
||||||
links.new(result, choose.inputs["True"])
|
|
||||||
return reconstruct_camera_depth(group, camera, depth, choose.outputs[0])
|
|
||||||
@@ -5,11 +5,10 @@ from anyimage.operators.cutout_tool.shape import CUTOUT_Z_TO_X, NORMAL_SCALE_ATT
|
|||||||
from ..common.nodes import interface_socket, group_input, math_node, float_input, menu_switch, read_float_attribute, store_float_attribute, prepare_node_group, sample_field, evaluate_field
|
from ..common.nodes import interface_socket, group_input, math_node, float_input, menu_switch, read_float_attribute, store_float_attribute, prepare_node_group, sample_field, evaluate_field
|
||||||
from ..common.smoothing import expand_smooth
|
from ..common.smoothing import expand_smooth
|
||||||
from ..common.boundary_smoothing import smooth_cut_boundary
|
from ..common.boundary_smoothing import smooth_cut_boundary
|
||||||
from ..common.outline_smoothing import smooth_outline_camera
|
|
||||||
from ..common.cutout import _island_cleanup, _position_field, _remove_zero_thickness, _shade_output
|
from ..common.cutout import _island_cleanup, _position_field, _remove_zero_thickness, _shade_output
|
||||||
from ..common.depth_surface import _math, project_depth_surface, build_surface_camera, split_depth_surface, sample_face_camera, thicken_depth_surface
|
from ..common.depth_surface import _math, project_depth_surface, build_surface_camera, split_depth_surface, sample_face_camera, thicken_depth_surface
|
||||||
from ..common.smoothing import edge_boundary_field
|
from ..common.smoothing import edge_boundary_field
|
||||||
from ..common.cutout_boundary import remove_boundary_triangles, taper_split_profile
|
from ..common.cutout_boundary import original_boundary_field, remove_boundary_triangles, taper_split_profile
|
||||||
|
|
||||||
|
|
||||||
NODE_GROUP_NAME = "O Image Depth Cutout"
|
NODE_GROUP_NAME = "O Image Depth Cutout"
|
||||||
@@ -92,7 +91,7 @@ def build_shell_fields(group, geometry, fields, is_shell, normal_weight=1):
|
|||||||
normal = nodes.new("GeometryNodeInputNormal")
|
normal = nodes.new("GeometryNodeInputNormal")
|
||||||
blur = nodes.new("GeometryNodeBlurAttribute")
|
blur = nodes.new("GeometryNodeBlurAttribute")
|
||||||
blur.data_type = "FLOAT_VECTOR"
|
blur.data_type = "FLOAT_VECTOR"
|
||||||
blur.inputs["Iterations"].default_value = 100
|
links.new(group_input(nodes, {"Normal Smooth"}).outputs["Normal Smooth"], blur.inputs["Iterations"])
|
||||||
links.new(normal.outputs[0], blur.inputs["Value"])
|
links.new(normal.outputs[0], blur.inputs["Value"])
|
||||||
normalized = blend_normal(group, normal.outputs[0], blur.outputs[0], normal_weight)
|
normalized = blend_normal(group, normal.outputs[0], blur.outputs[0], normal_weight)
|
||||||
rear_normal = sample_field(group, surface.outputs[0], normalized, "FLOAT_VECTOR")
|
rear_normal = sample_field(group, surface.outputs[0], normalized, "FLOAT_VECTOR")
|
||||||
@@ -135,7 +134,9 @@ def build_front_fields(group, volume, front_normal, normal_weight=1):
|
|||||||
smooth = group.nodes.new("GeometryNodeBlurAttribute")
|
smooth = group.nodes.new("GeometryNodeBlurAttribute")
|
||||||
smooth.data_type = "FLOAT_VECTOR"
|
smooth.data_type = "FLOAT_VECTOR"
|
||||||
links.new(normal, smooth.inputs["Value"])
|
links.new(normal, smooth.inputs["Value"])
|
||||||
smooth.inputs["Iterations"].default_value = 512
|
iterations = group_input(group.nodes, {"Normal Smooth"}).outputs["Normal Smooth"]
|
||||||
|
# Preserve the existing extra smoothing of the inflation direction at 100.
|
||||||
|
links.new(_math(group, "MULTIPLY", iterations, 5.12), smooth.inputs["Iterations"])
|
||||||
normalized = blend_normal(group, normal, smooth.outputs[0], normal_weight)
|
normalized = blend_normal(group, normal, smooth.outputs[0], normal_weight)
|
||||||
return {"structure": structure, "inflation_normal": normalized}
|
return {"structure": structure, "inflation_normal": normalized}
|
||||||
|
|
||||||
@@ -195,7 +196,7 @@ def build_front_normal(group, normal_weight):
|
|||||||
blur = group.nodes.new("GeometryNodeBlurAttribute")
|
blur = group.nodes.new("GeometryNodeBlurAttribute")
|
||||||
blur.data_type = "FLOAT_VECTOR"
|
blur.data_type = "FLOAT_VECTOR"
|
||||||
links.new(normal.outputs[0], blur.inputs["Value"])
|
links.new(normal.outputs[0], blur.inputs["Value"])
|
||||||
blur.inputs["Iterations"].default_value = 100
|
links.new(group_input(group.nodes, {"Normal Smooth"}).outputs["Normal Smooth"], blur.inputs["Iterations"])
|
||||||
normalized = blend_normal(group, normal.outputs[0], blur.outputs[0], normal_weight)
|
normalized = blend_normal(group, normal.outputs[0], blur.outputs[0], normal_weight)
|
||||||
return evaluate_field(group, normalized, "FLOAT_VECTOR", "POINT")
|
return evaluate_field(group, normalized, "FLOAT_VECTOR", "POINT")
|
||||||
|
|
||||||
@@ -273,7 +274,6 @@ def _build_depth_surface(group, geometry, controls):
|
|||||||
)
|
)
|
||||||
geometry = remove_boundary_triangles(group, geometry)
|
geometry = remove_boundary_triangles(group, geometry)
|
||||||
camera = build_surface_camera(group, _position_field(group, image), corner_camera, cut_vertex, split)
|
camera = build_surface_camera(group, _position_field(group, image), corner_camera, cut_vertex, split)
|
||||||
camera = smooth_outline_camera(group, original, geometry, camera, scale)
|
|
||||||
projection = project_depth_surface(
|
projection = project_depth_surface(
|
||||||
group, geometry, camera, scale, controls.outputs["Reference Depth"], controls.outputs["Depth Scale"], split,
|
group, geometry, camera, scale, controls.outputs["Reference Depth"], controls.outputs["Depth Scale"], split,
|
||||||
)
|
)
|
||||||
@@ -293,7 +293,7 @@ def _build_depth_surface(group, geometry, controls):
|
|||||||
links.new(projection, merge.inputs["Geometry"])
|
links.new(projection, merge.inputs["Geometry"])
|
||||||
links.new(_math(group, "MULTIPLY", typical.outputs["Mean"], 1e-6), merge.inputs["Distance"])
|
links.new(_math(group, "MULTIPLY", typical.outputs["Mean"], 1e-6), merge.inputs["Distance"])
|
||||||
projection = merge.outputs["Geometry"]
|
projection = merge.outputs["Geometry"]
|
||||||
projection = taper_split_profile(group, original, projection)
|
projection = taper_split_profile(group, projection, original_boundary_field(group, original))
|
||||||
# The mode field is shared by the geometry and thickness choices.
|
# The mode field is shared by the geometry and thickness choices.
|
||||||
mode = group.nodes.new("GeometryNodeMenuSwitch")
|
mode = group.nodes.new("GeometryNodeMenuSwitch")
|
||||||
mode.data_type = "BOOLEAN"
|
mode.data_type = "BOOLEAN"
|
||||||
@@ -341,7 +341,6 @@ def build_image_depth_cutout_group():
|
|||||||
group = bpy.data.node_groups.new(name, 'GeometryNodeTree')
|
group = bpy.data.node_groups.new(name, 'GeometryNodeTree')
|
||||||
group.is_modifier, group.use_fake_user = (True, True)
|
group.is_modifier, group.use_fake_user = (True, True)
|
||||||
group.color_tag = 'GEOMETRY'
|
group.color_tag = 'GEOMETRY'
|
||||||
group['anyimage_asset_revision'] = 26
|
|
||||||
interface_socket(group, 'Geometry', 'INPUT', 'NodeSocketGeometry')
|
interface_socket(group, 'Geometry', 'INPUT', 'NodeSocketGeometry')
|
||||||
interface_socket(group, 'Mode', 'INPUT', 'NodeSocketMenu')
|
interface_socket(group, 'Mode', 'INPUT', 'NodeSocketMenu')
|
||||||
float_input(group, 'Thickness', 1.0).description = 'Control how much the surface bulges in Balloon mode. Zero removes the bulge.'
|
float_input(group, 'Thickness', 1.0).description = 'Control how much the surface bulges in Balloon mode. Zero removes the bulge.'
|
||||||
@@ -358,12 +357,12 @@ def build_image_depth_cutout_group():
|
|||||||
float_input(group, 'Reference Depth', 1.0, subtype='DISTANCE', parent=options)
|
float_input(group, 'Reference Depth', 1.0, subtype='DISTANCE', parent=options)
|
||||||
interface_socket(group, 'Depth Axis', 'INPUT', 'NodeSocketMenu', options).menu_expanded = True
|
interface_socket(group, 'Depth Axis', 'INPUT', 'NodeSocketMenu', options).menu_expanded = True
|
||||||
boundary_smooth = interface_socket(group, 'Boundary Smooth', 'INPUT', 'NodeSocketInt', options)
|
boundary_smooth = interface_socket(group, 'Boundary Smooth', 'INPUT', 'NodeSocketInt', options)
|
||||||
boundary_smooth.default_value, boundary_smooth.min_value, boundary_smooth.max_value = 2, 1, 20
|
boundary_smooth.default_value, boundary_smooth.min_value, boundary_smooth.max_value = 0, 0, 16
|
||||||
boundary_smooth.description = 'Control boundary smoothing within each Outline Depth Fix pass. Higher values apply more smoothing per pass.'
|
boundary_smooth.description = 'Smooth geometry near the original outline and Depth Split edges. Zero disables boundary smoothing.'
|
||||||
outline_smooth = interface_socket(group, 'Outline Depth Fix', 'INPUT', 'NodeSocketInt', options)
|
normal_smooth = interface_socket(group, 'Normal Smooth', 'INPUT', 'NodeSocketInt', options)
|
||||||
outline_smooth.default_value, outline_smooth.min_value, outline_smooth.max_value = 16, 0, 200
|
normal_smooth.default_value, normal_smooth.min_value, normal_smooth.max_value = 64, 16, 512
|
||||||
outline_smooth.description = 'Reduce abrupt depth changes near the original Cutout outline. Higher values apply more correction passes. Zero disables correction.'
|
normal_smooth.description = 'Control smoothing of front and rear displacement normals. Higher values produce smoother directions.'
|
||||||
float_input(group, 'Front Inflation', 0.0, maximum=1, subtype='FACTOR', parent=options).description = 'Control how much the front surface bulges in Balloon mode. Zero keeps the original front surface.'
|
float_input(group, 'Front Inflation', 0.2, maximum=1, subtype='FACTOR', parent=options).description = 'Control how much the front surface bulges in Balloon mode. Zero keeps the original front surface.'
|
||||||
data = group.interface.new_panel(name='Data', default_closed=True)
|
data = group.interface.new_panel(name='Data', default_closed=True)
|
||||||
float_input(group, 'Uniform Scale', 1.0, parent=data).hide_in_modifier = True
|
float_input(group, 'Uniform Scale', 1.0, parent=data).hide_in_modifier = True
|
||||||
interface_socket(group, 'Depth Image', 'INPUT', 'NodeSocketImage', data).hide_in_modifier = True
|
interface_socket(group, 'Depth Image', 'INPUT', 'NodeSocketImage', data).hide_in_modifier = True
|
||||||
|
|||||||
@@ -53,7 +53,7 @@ def build_image_depth_plane_group(image_plane_group):
|
|||||||
float_input(group, "Smooth Weight", 1.0, maximum=1, subtype="FACTOR", parent=options).description = "Control the strength of each mesh smoothing pass. Higher values move vertices more."
|
float_input(group, "Smooth Weight", 1.0, maximum=1, subtype="FACTOR", parent=options).description = "Control the strength of each mesh smoothing pass. Higher values move vertices more."
|
||||||
float_input(group, "Reference Depth", 1.0, subtype="DISTANCE", parent=options)
|
float_input(group, "Reference Depth", 1.0, subtype="DISTANCE", parent=options)
|
||||||
boundary_smooth = _interface_socket(group, "Boundary Smooth", "INPUT", "NodeSocketInt", parent=options)
|
boundary_smooth = _interface_socket(group, "Boundary Smooth", "INPUT", "NodeSocketInt", parent=options)
|
||||||
boundary_smooth.default_value, boundary_smooth.min_value, boundary_smooth.max_value = 2, 0, 20
|
boundary_smooth.default_value, boundary_smooth.min_value, boundary_smooth.max_value = 4, 0, 16
|
||||||
boundary_smooth.description = "Smooth depth and geometry near cut edges while preserving the original image border. Zero disables boundary smoothing."
|
boundary_smooth.description = "Smooth depth and geometry near cut edges while preserving the original image border. Zero disables boundary smoothing."
|
||||||
validity_threshold = _interface_socket(group, "Mask Threshold", "INPUT", "NodeSocketFloat", parent=options)
|
validity_threshold = _interface_socket(group, "Mask Threshold", "INPUT", "NodeSocketFloat", parent=options)
|
||||||
validity_threshold.description = "Set the minimum depth mask value treated as valid. Higher values exclude more partially valid boundary areas."
|
validity_threshold.description = "Set the minimum depth mask value treated as valid. Higher values exclude more partially valid boundary areas."
|
||||||
|
|||||||
Reference in New Issue
Block a user