Refactor Cutout Tool to Use World Space Edge Length
- Replaced the Mesh Detail parameter with a new Edge Length property to define cutout sampling in physical units. - Introduced Minimum Relative Edge Length preference to control effective spacing based on the bounding box of the cutout content. - Updated tests to validate the new edge length functionality and ensure consistent behavior across different resolutions and canvas sizes. - Removed deprecated parameters and adjusted related properties to align with the new edge length implementation. - Enhanced geometry sampling logic to utilize effective world spacing for both standard and fine outline modes.
This commit is contained in:
@@ -4,7 +4,7 @@ from mathutils import Matrix
|
||||
from anyimage.operators.cutout_tool.shape import CUTOUT_Z_TO_X, NORMAL_SCALE_ATTRIBUTE_NAME
|
||||
from ..common.nodes import interface_socket, group_input, math_node, float_input, menu_switch, read_float_attribute, store_float_attribute, prepare_node_group
|
||||
from ..common.smoothing import expand_smooth
|
||||
from ..common.cutout import _extrude_layers, _island_cleanup, _offset_geometry_z, _profile_field, _remove_zero_thickness, _set_layer_z, _shade_output
|
||||
from ..common.cutout import _extrude_layers, _island_cleanup, _offset_geometry_z, _remove_zero_thickness, _set_layer_z, _shade_output
|
||||
|
||||
|
||||
NODE_GROUP_NAME = "O Image Cutout"
|
||||
@@ -129,16 +129,13 @@ def _build_shell(group, geometry, thickness):
|
||||
def _build_balloon(group, geometry, thickness):
|
||||
nodes = group.nodes
|
||||
links = group.links
|
||||
profile, _profile_factor = _profile_field(group, geometry)
|
||||
profile = read_float_attribute(group, 'o_balloon')
|
||||
front_scale = math_node(nodes, "MULTIPLY")
|
||||
links.new(profile, front_scale.inputs[0])
|
||||
links.new(thickness, front_scale.inputs[1])
|
||||
back = math_node(nodes, "MULTIPLY")
|
||||
links.new(profile, back.inputs[0])
|
||||
links.new(thickness, back.inputs[1])
|
||||
negative_back = math_node(nodes, "MULTIPLY")
|
||||
negative_back.inputs[1].default_value = -1.0
|
||||
links.new(back.outputs[0], negative_back.inputs[0])
|
||||
links.new(front_scale.outputs[0], negative_back.inputs[0])
|
||||
extrude = _extrude_layers(group, geometry)
|
||||
shaped = _set_layer_z(
|
||||
group,
|
||||
|
||||
@@ -68,12 +68,9 @@ def _build_depth_balloon(
|
||||
base_front = math_node(nodes, "MULTIPLY")
|
||||
links.new(profile, base_front.inputs[0])
|
||||
links.new(thickness, base_front.inputs[1])
|
||||
base_back = math_node(nodes, "MULTIPLY")
|
||||
links.new(profile, base_back.inputs[0])
|
||||
links.new(thickness, base_back.inputs[1])
|
||||
negate_back = math_node(nodes, "MULTIPLY")
|
||||
negate_back.inputs[1].default_value = -1.0
|
||||
links.new(base_back.outputs[0], negate_back.inputs[0])
|
||||
links.new(base_front.outputs[0], negate_back.inputs[0])
|
||||
front_mix = nodes.new("ShaderNodeMix")
|
||||
front_mix.data_type = "FLOAT"
|
||||
front_mix.clamp_factor = True
|
||||
|
||||
@@ -70,7 +70,10 @@ def build_volume_fields(group):
|
||||
positive = _math(group, "MAXIMUM", full, minimum)
|
||||
control = group_input(group.nodes, {"Thickness"})
|
||||
thickness = _math(group, "MULTIPLY", positive, control.outputs["Thickness"])
|
||||
return {"rim": rim, "center": center, "thickness": thickness}
|
||||
return {
|
||||
"profile": profile, "depth": axes.outputs["Z"], "interior": interior,
|
||||
"center": center, "thickness": thickness,
|
||||
}
|
||||
|
||||
|
||||
def build_shell_fields(group, geometry, fields, is_shell, normal_weight=1):
|
||||
@@ -102,7 +105,7 @@ def build_shell_fields(group, geometry, fields, is_shell, normal_weight=1):
|
||||
return {"thickness": thickness.outputs[0], "normal": rear_normal}
|
||||
|
||||
|
||||
def build_front_fields(group, rim, front_normal, normal_weight=1):
|
||||
def build_front_fields(group, volume, front_normal, normal_weight=1):
|
||||
"""Fit structural amplitude over neighbouring points before creating the shell."""
|
||||
links = group.links
|
||||
|
||||
@@ -115,15 +118,7 @@ def build_front_fields(group, rim, front_normal, normal_weight=1):
|
||||
links.new(weight, result.inputs["Weight"])
|
||||
return result.outputs[0]
|
||||
|
||||
profile_node = group.nodes.new("GeometryNodeInputNamedAttribute")
|
||||
profile_node.data_type = "FLOAT"
|
||||
profile_node.inputs["Name"].default_value = "o_balloon"
|
||||
profile = profile_node.outputs["Attribute"]
|
||||
interior = _math(group, "SUBTRACT", 1, rim)
|
||||
position = group.nodes.new("GeometryNodeInputPosition")
|
||||
axes = group.nodes.new("ShaderNodeSeparateXYZ")
|
||||
links.new(position.outputs[0], axes.inputs[0])
|
||||
depth = axes.outputs["Z"]
|
||||
profile, depth, interior = volume["profile"], volume["depth"], volume["interior"]
|
||||
depth_slow = blur(depth, interior)
|
||||
profile_slow = blur(profile, interior)
|
||||
depth_bump = _math(group, "SUBTRACT", depth, depth_slow)
|
||||
@@ -285,18 +280,21 @@ def _build_depth_surface(group, geometry, controls):
|
||||
projection = smooth_cut_boundary(group, projection)
|
||||
projection = taper_split_profile(group, original, projection)
|
||||
# The mode field is shared by the geometry and thickness choices.
|
||||
mode = menu_switch(group, "Mode", ("Balloon", "Shell"), "BOOLEAN")
|
||||
mode = group.nodes.new("GeometryNodeMenuSwitch")
|
||||
mode.data_type = "BOOLEAN"
|
||||
for item, name in zip(mode.enum_items, ("Balloon", "Shell")):
|
||||
item.name = name
|
||||
links.new(controls.outputs["Mode"], mode.inputs["Menu"])
|
||||
mode.inputs["Balloon"].default_value = False
|
||||
mode.inputs["Shell"].default_value = True
|
||||
is_shell = mode.outputs[0]
|
||||
volume = build_volume_fields(group)
|
||||
normal_weight = build_normal_weight(group, is_shell)
|
||||
normal = build_front_normal(group, normal_weight)
|
||||
front = build_front_fields(group, volume["rim"], normal, normal_weight)
|
||||
front = build_front_fields(group, volume, normal, normal_weight)
|
||||
shell_fields = build_shell_fields(group, projection, volume, is_shell, normal_weight)
|
||||
thickness = sample_field(group, projection, shell_fields["thickness"], "FLOAT")
|
||||
shell = thicken_depth_surface(
|
||||
group, projection, thickness, back_mask=NORMAL_SCALE_ATTRIBUTE_NAME, weld_zero_thickness=True,
|
||||
group, projection, shell_fields["thickness"], back_mask=NORMAL_SCALE_ATTRIBUTE_NAME, weld_zero_thickness=True,
|
||||
)
|
||||
fields = {
|
||||
name: sample_field(group, projection, value, data_type, index=shell["source_index"])
|
||||
@@ -350,7 +348,7 @@ def build_image_depth_cutout_group():
|
||||
outline_smooth = interface_socket(group, 'Outline Depth Fix', 'INPUT', 'NodeSocketInt', options)
|
||||
outline_smooth.default_value, outline_smooth.min_value, outline_smooth.max_value = 16, 0, 200
|
||||
outline_smooth.description = 'Reduce abrupt depth changes near the original Cutout outline. Higher values apply more correction passes. Zero disables correction.'
|
||||
float_input(group, 'Front Inflation', 0.5, 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.0, 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)
|
||||
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
|
||||
|
||||
Reference in New Issue
Block a user