refactor: update depth cutout and surface handling for improved geometry manipulation and clarity
This commit is contained in:
Binary file not shown.
@@ -1,5 +1,10 @@
|
|||||||
"""Expose the extension and standalone server packages to the test suite."""
|
"""Expose the extension and standalone server packages to the test suite."""
|
||||||
import sys
|
import sys
|
||||||
|
|
||||||
|
# Import the pinned dependencies before bpy prepends Blender's extension
|
||||||
|
# site-packages, where a half-removed install can shadow the project's own scipy.
|
||||||
|
import scipy # noqa: F401
|
||||||
|
|
||||||
from tests.support.paths import PROJECT_ROOT
|
from tests.support.paths import PROJECT_ROOT
|
||||||
|
|
||||||
sys.path[:0] = [str(PROJECT_ROOT / "src"), str(PROJECT_ROOT / "src/anyimage")]
|
sys.path[:0] = [str(PROJECT_ROOT / "src"), str(PROJECT_ROOT / "src/anyimage")]
|
||||||
|
|||||||
@@ -113,7 +113,7 @@ def _surface(node_group):
|
|||||||
image.pixels.foreach_set(pixels.ravel())
|
image.pixels.foreach_set(pixels.ravel())
|
||||||
values = {"Subdivide": 1, "Thickness": 0.2, "Depth Image": image,
|
values = {"Subdivide": 1, "Thickness": 0.2, "Depth Image": image,
|
||||||
"Uniform Scale": 1.0, "Reference Depth": 2.5, "Depth Scale": 0.8,
|
"Uniform Scale": 1.0, "Reference Depth": 2.5, "Depth Scale": 0.8,
|
||||||
"Normal Smooth": 2, "Depth Split": 0.25,
|
"Edge Turn": 2, "Depth Split": 0.25,
|
||||||
"Offset": 0.5}
|
"Offset": 0.5}
|
||||||
mode = None
|
mode = None
|
||||||
for item in node_group.interface.items_tree:
|
for item in node_group.interface.items_tree:
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
"""Verify split profile shaping and thickness-weighted normals."""
|
"""Verify split profile shaping and displacement direction shaping."""
|
||||||
|
|
||||||
import bpy
|
import bpy
|
||||||
import numpy as np
|
import numpy as np
|
||||||
@@ -6,9 +6,9 @@ import pytest
|
|||||||
|
|
||||||
from anyimage.common.object import modifier_input_identifier, set_modifier_input
|
from anyimage.common.object import modifier_input_identifier, set_modifier_input
|
||||||
from tests.support.depth_surface import surface, evaluated
|
from tests.support.depth_surface import surface, evaluated
|
||||||
from tests.tools.nodes.test_boundary_smoothing import diagonal_surface, boundary_neighbors, edge_band
|
from tests.tools.nodes.test_boundary_smoothing import diagonal_surface
|
||||||
from tools.nodes.common.cutout_boundary import remove_boundary_triangles
|
from tools.nodes.common.cutout_boundary import remove_boundary_triangles
|
||||||
from tools.nodes.groups.image_depth_cutout import build_normal_weight, blend_normal
|
from tools.nodes.groups.image_depth_cutout import build_normal_direction, build_normal_weight
|
||||||
|
|
||||||
|
|
||||||
def profile_values(obj):
|
def profile_values(obj):
|
||||||
@@ -77,63 +77,76 @@ def test_split_profile_tapers_within_boundary_band():
|
|||||||
set_value("Depth Scale", 1)
|
set_value("Depth Scale", 1)
|
||||||
np.testing.assert_allclose(profile_values(obj), .3)
|
np.testing.assert_allclose(profile_values(obj), .3)
|
||||||
set_value("Depth Split", .1)
|
set_value("Depth Split", .1)
|
||||||
points, faces = evaluated(obj)
|
|
||||||
boundary = boundary_neighbors(faces)
|
|
||||||
uv = vertex_uv(obj)
|
|
||||||
# The depth step sits at u=0.5, so that column is the split line.
|
|
||||||
cut = [i for i in boundary if abs(uv[i,0] - .5) < 1e-4 and 1e-4 < uv[i,1] < 1 - 1e-4]
|
|
||||||
assert cut
|
|
||||||
band = edge_band(faces, cut)
|
|
||||||
outside = [i for i in range(len(points)) if i not in band]
|
|
||||||
assert outside
|
|
||||||
values = profile_values(obj)
|
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)
|
np.testing.assert_array_equal(values[cut], 0)
|
||||||
np.testing.assert_allclose(values[outside], .3)
|
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 np.isfinite(values).all()
|
||||||
assert values.max() == pytest.approx(.3)
|
assert values.max() == pytest.approx(.3)
|
||||||
set_value("Depth Split", 0)
|
set_value("Depth Split", 0)
|
||||||
np.testing.assert_allclose(profile_values(obj), .3)
|
np.testing.assert_allclose(profile_values(obj), .3)
|
||||||
|
|
||||||
|
|
||||||
@pytest.mark.parametrize("mode", [0, 1])
|
def sampled(obj, group, value, name):
|
||||||
def test_normal_blend_weights_balloon_and_keeps_full_shell_smoothing(mode):
|
"""Store one field on the points and read it back from the evaluated mesh."""
|
||||||
obj, set_value = surface(resolution=2)
|
data_type, reader = {"VECTOR": ("FLOAT_VECTOR", "vector"), "VALUE": ("FLOAT", "value")}[value.type]
|
||||||
group = obj.modifiers[0].node_group
|
|
||||||
# The production interface renames the Shell thickness socket after building.
|
|
||||||
socket = next(s for s in group.interface.items_tree if s.item_type == "SOCKET" and s.in_out == "INPUT"
|
|
||||||
and s.name == "Thickness" and s.subtype == "DISTANCE")
|
|
||||||
socket.name = "Shell Thickness"
|
|
||||||
is_shell = group.nodes.new("FunctionNodeInputBool")
|
|
||||||
is_shell.boolean = bool(mode)
|
|
||||||
weight = build_normal_weight(group, is_shell.outputs[0])
|
|
||||||
raw = group.nodes.new("ShaderNodeCombineXYZ")
|
|
||||||
raw.inputs["Z"].default_value = 1
|
|
||||||
blurred = group.nodes.new("ShaderNodeCombineXYZ")
|
|
||||||
blurred.inputs["X"].default_value = 1
|
|
||||||
result = blend_normal(group, raw.outputs[0], blurred.outputs[0], weight)
|
|
||||||
store = group.nodes.new("GeometryNodeStoreNamedAttribute")
|
store = group.nodes.new("GeometryNodeStoreNamedAttribute")
|
||||||
store.data_type, store.domain = "FLOAT_VECTOR", "POINT"
|
store.data_type, store.domain = data_type, "POINT"
|
||||||
store.inputs["Name"].default_value = "normal_result"
|
store.inputs["Name"].default_value = name
|
||||||
source = group.nodes.new("NodeGroupInput")
|
source = group.nodes.new("NodeGroupInput")
|
||||||
group.links.new(source.outputs["Geometry"], store.inputs["Geometry"])
|
group.links.new(source.outputs["Geometry"], store.inputs["Geometry"])
|
||||||
group.links.new(result, store.inputs["Value"])
|
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"])
|
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]
|
modifier = obj.modifiers[0]
|
||||||
identifier = socket.identifier if mode else modifier_input_identifier(group, "Thickness", subtype="NONE")
|
turn = modifier_input_identifier(group, "Edge Turn")
|
||||||
reference = 2
|
|
||||||
set_value("Reference Depth", reference)
|
def result(weight_value, turn_value):
|
||||||
results = []
|
weight.outputs[0].default_value = weight_value
|
||||||
for amount in (0, .001, .01, 1):
|
set_modifier_input(modifier, turn, turn_value)
|
||||||
set_modifier_input(modifier, identifier, amount * (max(reference, 1e-8) if mode else 1))
|
obj.update_tag(refresh={"DATA"})
|
||||||
obj.update_tag(refresh={"DATA"}); bpy.context.view_layer.update()
|
bpy.context.view_layer.update()
|
||||||
ev = obj.evaluated_get(bpy.context.evaluated_depsgraph_get()); mesh = ev.to_mesh()
|
return sampled(obj, group, direction, "normal_direction")[0]
|
||||||
try:
|
|
||||||
results.append(np.array(mesh.attributes["normal_result"].data[0].vector[:]))
|
np.testing.assert_allclose(result(0.0, 0.0), (0, 0, 1), atol=1e-6)
|
||||||
finally:
|
np.testing.assert_allclose(result(0.0, 1.0), (1, 0, 0), atol=1e-6)
|
||||||
ev.to_mesh_clear()
|
np.testing.assert_allclose(result(0.0, 2.0), (2, 0, -1) / np.sqrt(5), atol=1e-6)
|
||||||
np.testing.assert_array_equal(results[-1], [1,0,0])
|
np.testing.assert_allclose(result(1.0, 2.0), (1, 0, 0), atol=1e-6)
|
||||||
if mode:
|
|
||||||
np.testing.assert_array_equal(results, [[1,0,0]] * len(results))
|
|
||||||
else:
|
|
||||||
np.testing.assert_array_equal(results[0], [0,0,1])
|
|
||||||
assert np.linalg.norm(results[1]-results[0]) < np.linalg.norm(results[2]-results[0]) < .001
|
|
||||||
|
|||||||
@@ -7,7 +7,8 @@ from anyimage.common.object import modifier_input_identifier, set_modifier_input
|
|||||||
|
|
||||||
|
|
||||||
@pytest.mark.parametrize("mode", [0, 1])
|
@pytest.mark.parametrize("mode", [0, 1])
|
||||||
def test_normal_smooth_changes_thickness_direction_without_moving_zero_thickness(mode):
|
def test_edge_turn_reshapes_thickness_without_moving_zero_thickness(mode):
|
||||||
|
"""Edge Turn swings the wall direction and leaves zero thickness untouched."""
|
||||||
from tests.support.depth_surface import surface, evaluated
|
from tests.support.depth_surface import surface, evaluated
|
||||||
|
|
||||||
obj, set_value = surface(step=1, resolution=10)
|
obj, set_value = surface(step=1, resolution=10)
|
||||||
@@ -19,24 +20,28 @@ def test_normal_smooth_changes_thickness_direction_without_moving_zero_thickness
|
|||||||
set_value("Front Inflation", .2)
|
set_value("Front Inflation", .2)
|
||||||
modifier = obj.modifiers[0]
|
modifier = obj.modifiers[0]
|
||||||
thickness = modifier_input_identifier(modifier.node_group, "Thickness", subtype="DISTANCE" if mode else "NONE")
|
thickness = modifier_input_identifier(modifier.node_group, "Thickness", subtype="DISTANCE" if mode else "NONE")
|
||||||
|
turn = modifier_input_identifier(modifier.node_group, "Edge Turn")
|
||||||
|
|
||||||
def result(amount, smoothing):
|
def result(amount, edge_turn):
|
||||||
set_modifier_input(modifier, thickness, amount)
|
set_modifier_input(modifier, thickness, amount)
|
||||||
set_value("Normal Smooth", smoothing)
|
set_modifier_input(modifier, turn, edge_turn)
|
||||||
|
obj.update_tag(refresh={"DATA"})
|
||||||
|
bpy.context.view_layer.update()
|
||||||
return evaluated(obj)
|
return evaluated(obj)
|
||||||
|
|
||||||
raw, faces = result(0, 16)
|
plain, faces = result(0, 0)
|
||||||
smooth, smooth_faces = result(0, 512)
|
turned, turned_faces = result(0, 2)
|
||||||
np.testing.assert_array_equal(raw, smooth)
|
np.testing.assert_array_equal(plain, turned)
|
||||||
assert faces == smooth_faces
|
assert faces == turned_faces
|
||||||
raw, faces = result(1, 16)
|
plain, faces = result(1, 0)
|
||||||
smooth, smooth_faces = result(1, 512)
|
turned, turned_faces = result(1, 2)
|
||||||
assert faces == smooth_faces
|
assert faces == turned_faces
|
||||||
assert np.isfinite(smooth).all()
|
assert np.isfinite(turned).all()
|
||||||
assert np.max(np.abs(raw - smooth)) > 1e-5
|
assert np.max(np.abs(plain - turned)) > 1e-5
|
||||||
|
|
||||||
|
|
||||||
def test_thin_shell_normal_smoothing_is_independent_of_thickness_and_reference():
|
def test_shell_offset_scales_with_thickness_and_ignores_reference():
|
||||||
|
"""A thin Shell moves a distance proportional to its thickness."""
|
||||||
from tests.support.depth_surface import surface, evaluated
|
from tests.support.depth_surface import surface, evaluated
|
||||||
|
|
||||||
obj, set_value = surface(step=0, resolution=12)
|
obj, set_value = surface(step=0, resolution=12)
|
||||||
@@ -49,9 +54,8 @@ def test_thin_shell_normal_smoothing_is_independent_of_thickness_and_reference()
|
|||||||
set_value('Thickness', 0.)
|
set_value('Thickness', 0.)
|
||||||
front, _ = evaluated(obj)
|
front, _ = evaluated(obj)
|
||||||
|
|
||||||
def offsets(thickness, smoothing, reference=10.):
|
def offsets(thickness, reference=10.):
|
||||||
set_value('Thickness', thickness)
|
set_value('Thickness', thickness)
|
||||||
set_value('Normal Smooth', smoothing)
|
|
||||||
set_value('Reference Depth', reference)
|
set_value('Reference Depth', reference)
|
||||||
points, faces = evaluated(obj)
|
points, faces = evaluated(obj)
|
||||||
adjusted_front = front + (0, 10 - reference, 0)
|
adjusted_front = front + (0, 10 - reference, 0)
|
||||||
@@ -63,11 +67,9 @@ def test_thin_shell_normal_smoothing_is_independent_of_thickness_and_reference()
|
|||||||
order = np.argsort(indices[rear])
|
order = np.argsort(indices[rear])
|
||||||
return ((points[rear] - adjusted_front[indices[rear]]) / thickness)[order], faces
|
return ((points[rear] - adjusted_front[indices[rear]]) / thickness)[order], faces
|
||||||
|
|
||||||
raw, _ = offsets(.02, 16)
|
smooth, faces = offsets(.02)
|
||||||
smooth, faces = offsets(.02, 512)
|
thinner, thinner_faces = offsets(.01)
|
||||||
assert np.max(np.linalg.norm(raw - smooth, axis=1)) > .05
|
distant, _ = offsets(.02, 20.)
|
||||||
thinner, thinner_faces = offsets(.01, 512)
|
|
||||||
distant, _ = offsets(.02, 512, 20.)
|
|
||||||
np.testing.assert_allclose(thinner, smooth, atol=2e-4)
|
np.testing.assert_allclose(thinner, smooth, atol=2e-4)
|
||||||
np.testing.assert_allclose(distant, smooth, atol=2e-4)
|
np.testing.assert_allclose(distant, smooth, atol=2e-4)
|
||||||
assert thinner_faces == faces
|
assert thinner_faces == faces
|
||||||
|
|||||||
@@ -174,7 +174,7 @@ def test_temporary_fields_are_removed_and_user_attributes_survive(thickness):
|
|||||||
obj, set_value = surface()
|
obj, set_value = surface()
|
||||||
obj.data.attributes.new("o_depth_probe", "FLOAT", "POINT")
|
obj.data.attributes.new("o_depth_probe", "FLOAT", "POINT")
|
||||||
attribute = obj.data.attributes.new("o_user_weight", "FLOAT", "POINT")
|
attribute = obj.data.attributes.new("o_user_weight", "FLOAT", "POINT")
|
||||||
obj.data.attributes.new(".o_anyimage_user_weight", "FLOAT", "POINT")
|
obj.data.attributes.new(".o_user_weight", "FLOAT", "POINT")
|
||||||
for value in attribute.data:
|
for value in attribute.data:
|
||||||
value.value = 0.75
|
value.value = 0.75
|
||||||
set_value("Depth Split", 0.5)
|
set_value("Depth Split", 0.5)
|
||||||
@@ -184,10 +184,8 @@ def test_temporary_fields_are_removed_and_user_attributes_survive(thickness):
|
|||||||
try:
|
try:
|
||||||
assert {a.name for a in mesh.attributes if a.name.startswith(("o_depth_", "o_balloon_surface_"))} == {"o_depth_probe"}
|
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 "UVMap" in mesh.attributes
|
||||||
assert ".o_anyimage_user_weight" in mesh.attributes
|
assert ".o_user_weight" in mesh.attributes
|
||||||
assert "_o_anyimage_depth_cut" not in mesh.attributes
|
assert not any("o_anyimage" in a.name for a in mesh.attributes)
|
||||||
assert ".o_anyimage_original_boundary" not in mesh.attributes
|
|
||||||
assert ".o_anyimage_depth_cut" not in mesh.attributes
|
|
||||||
assert not any(a.name.startswith("_o_") for a in mesh.attributes)
|
assert not any(a.name.startswith("_o_") for a in mesh.attributes)
|
||||||
assert all(
|
assert all(
|
||||||
v.value == pytest.approx(0.75)
|
v.value == pytest.approx(0.75)
|
||||||
|
|||||||
@@ -19,7 +19,7 @@ def test_saved_parameter_descriptions_match_definitions():
|
|||||||
"O Image Depth Plane": {"Subdivide", "Thickness", "Depth Split", "Depth Mask", "Mask Threshold", "Boundary Smooth"},
|
"O Image Depth Plane": {"Subdivide", "Thickness", "Depth Split", "Depth Mask", "Mask Threshold", "Boundary Smooth"},
|
||||||
"O Image Relief Plane": {"Subdivide", "Thickness", "Depth Direction", "Depth Offset"},
|
"O Image Relief Plane": {"Subdivide", "Thickness", "Depth Direction", "Depth Offset"},
|
||||||
"O Image Cutout": {"Thickness"},
|
"O Image Cutout": {"Thickness"},
|
||||||
"O Image Depth Cutout": {"Thickness", "Depth Split", "Boundary Smooth", "Normal Smooth", "Front Inflation"},
|
"O Image Depth Cutout": {"Thickness", "Depth Split", "Boundary Smooth", "Edge Turn", "Front Inflation"},
|
||||||
"O Image Cutout Symmetry": {"Direction", "Offset", "Scale", "Fill Sides", "Smooth"},
|
"O Image Cutout Symmetry": {"Direction", "Offset", "Scale", "Fill Sides", "Smooth"},
|
||||||
"O Image Depth Panorama": {"Subdivide", "Depth Scale", "Depth Split", "Dome Radius", "Depth Mask", "Mask Threshold", "Boundary Smooth"},
|
"O Image Depth Panorama": {"Subdivide", "Depth Scale", "Depth Split", "Dome Radius", "Depth Mask", "Mask Threshold", "Boundary Smooth"},
|
||||||
"O Image Layer": {"Alpha Fix", "Normal Scale", "Bump Scale", "Object Space"},
|
"O Image Layer": {"Alpha Fix", "Normal Scale", "Bump Scale", "Object Space"},
|
||||||
@@ -82,8 +82,8 @@ def test_geometry_attribute_contract_includes_nested_groups():
|
|||||||
allowed = {
|
allowed = {
|
||||||
"UVMap": ("FLOAT2", "CORNER"),
|
"UVMap": ("FLOAT2", "CORNER"),
|
||||||
"o_balloon": ("FLOAT", "POINT"),
|
"o_balloon": ("FLOAT", "POINT"),
|
||||||
"_o_anyimage_depth_cut": ("BOOLEAN", "CORNER"),
|
"_o_depth_cut": ("BOOLEAN", "CORNER"),
|
||||||
"_o_solid_normal": ("FLOAT_VECTOR", "POINT"),
|
"_o_front_normal": ("FLOAT_VECTOR", "POINT"),
|
||||||
"_o_symmetry_weld": ("BOOLEAN", "FACE"),
|
"_o_symmetry_weld": ("BOOLEAN", "FACE"),
|
||||||
"o_depth_rotation": ("FLOAT_VECTOR", "FACE"),
|
"o_depth_rotation": ("FLOAT_VECTOR", "FACE"),
|
||||||
"o_depth_face": ("FLOAT", "FACE"),
|
"o_depth_face": ("FLOAT", "FACE"),
|
||||||
|
|||||||
@@ -26,10 +26,7 @@ def test_depth_attribute_cleanup_has_no_warnings(kind):
|
|||||||
mesh = evaluated.to_mesh()
|
mesh = evaluated.to_mesh()
|
||||||
try:
|
try:
|
||||||
assert mesh.vertices
|
assert mesh.vertices
|
||||||
assert not {a.name for a in mesh.attributes} & {
|
assert not any("o_anyimage" in a.name for a in mesh.attributes)
|
||||||
".o_anyimage_depth_cut", ".o_anyimage_original_boundary",
|
|
||||||
"_o_anyimage_depth_cut",
|
|
||||||
}
|
|
||||||
assert not any(a.name.startswith("_o_") for a in mesh.attributes)
|
assert not any(a.name.startswith("_o_") for a in mesh.attributes)
|
||||||
assert not list(modifier.node_warnings), [
|
assert not list(modifier.node_warnings), [
|
||||||
warning.message for warning in modifier.node_warnings
|
warning.message for warning in modifier.node_warnings
|
||||||
|
|||||||
@@ -58,8 +58,7 @@ def check_asset_library(asset_path):
|
|||||||
modifier = obj.modifiers.new("Asset Check", "NODES")
|
modifier = obj.modifiers.new("Asset Check", "NODES")
|
||||||
modifier.node_group = node_group
|
modifier.node_group = node_group
|
||||||
values = {"Subdivide": 0, "Thickness": 0.2, "Reference Depth": 2.0,
|
values = {"Subdivide": 0, "Thickness": 0.2, "Reference Depth": 2.0,
|
||||||
"Depth Image": depth_image, "Uniform Scale": 1.0, "Normal Smooth": 0,
|
"Depth Image": depth_image, "Uniform Scale": 1.0, "Depth Split": 0.0}
|
||||||
"Depth Split": 0.0}
|
|
||||||
if node_group.name == "O Image Cutout Symmetry":
|
if node_group.name == "O Image Cutout Symmetry":
|
||||||
values["Offset"] = 0.5
|
values["Offset"] = 0.5
|
||||||
for item in node_group.interface.items_tree:
|
for item in node_group.interface.items_tree:
|
||||||
@@ -75,10 +74,9 @@ def check_asset_library(asset_path):
|
|||||||
assert not list(modifier.node_warnings), (
|
assert not list(modifier.node_warnings), (
|
||||||
node_group.name, [warning.message for warning in modifier.node_warnings],
|
node_group.name, [warning.message for warning in modifier.node_warnings],
|
||||||
)
|
)
|
||||||
assert not {attribute.name for attribute in result.attributes} & {
|
assert not any(
|
||||||
".o_anyimage_depth_cut", ".o_anyimage_original_boundary",
|
"o_anyimage" in attribute.name for attribute in result.attributes
|
||||||
"_o_anyimage_depth_cut",
|
), node_group.name
|
||||||
}, node_group.name
|
|
||||||
assert not any(
|
assert not any(
|
||||||
attribute.name.startswith("_o_") for attribute in result.attributes
|
attribute.name.startswith("_o_") for attribute in result.attributes
|
||||||
), node_group.name
|
), node_group.name
|
||||||
|
|||||||
@@ -7,6 +7,8 @@ from .smoothing import edge_boundary_field, pinned_smooth
|
|||||||
|
|
||||||
def _gated(group, value, blocked, *, default):
|
def _gated(group, value, blocked, *, default):
|
||||||
"""Return the value unless the blocked control is true."""
|
"""Return the value unless the blocked control is true."""
|
||||||
|
if blocked is False:
|
||||||
|
return value
|
||||||
node = group.nodes.new("GeometryNodeSwitch")
|
node = group.nodes.new("GeometryNodeSwitch")
|
||||||
node.input_type = "FLOAT"
|
node.input_type = "FLOAT"
|
||||||
node.inputs["True"].default_value = default
|
node.inputs["True"].default_value = default
|
||||||
|
|||||||
@@ -1,10 +1,7 @@
|
|||||||
"""Clean cutout boundary triangles and taper the split thickness profile."""
|
"""Clean cutout boundary triangles and taper the split thickness profile."""
|
||||||
|
|
||||||
from .depth_surface import _math
|
from .depth_surface import _math
|
||||||
from .nodes import (
|
from .nodes import boolean_node, compare_node, evaluate_field, read_float_attribute, store_float_attribute
|
||||||
boolean_node, compare_node, evaluate_field, group_input,
|
|
||||||
read_float_attribute, sample_field, store_float_attribute,
|
|
||||||
)
|
|
||||||
from .smoothing import edge_boundary_field
|
from .smoothing import edge_boundary_field
|
||||||
from .boundary_smoothing import boundary_influence
|
from .boundary_smoothing import boundary_influence
|
||||||
|
|
||||||
@@ -23,37 +20,10 @@ def remove_boundary_triangles(group, geometry):
|
|||||||
return delete.outputs["Geometry"]
|
return delete.outputs["Geometry"]
|
||||||
|
|
||||||
|
|
||||||
def original_boundary_field(group, original, rim=None):
|
def taper_split_profile(group, geometry, cut_boundary):
|
||||||
"""Sample the unsplit outline in UV space after projection and separation."""
|
"""Taper the balloon profile to zero across the Depth Split cuts."""
|
||||||
nodes, links = group.nodes, group.links
|
|
||||||
uv = nodes.new("GeometryNodeInputNamedAttribute")
|
|
||||||
uv.data_type = "FLOAT_VECTOR"
|
|
||||||
uv.inputs["Name"].default_value = "UVMap"
|
|
||||||
original_uv = nodes.new("GeometryNodeSetPosition")
|
|
||||||
links.new(original, original_uv.inputs["Geometry"])
|
|
||||||
links.new(uv.outputs["Attribute"], original_uv.inputs["Position"])
|
|
||||||
nearest = nodes.new("GeometryNodeSampleNearest")
|
|
||||||
nearest.domain = "POINT"
|
|
||||||
links.new(original_uv.outputs["Geometry"], nearest.inputs["Geometry"])
|
|
||||||
links.new(uv.outputs["Attribute"], nearest.inputs["Sample Position"])
|
|
||||||
if rim is None:
|
|
||||||
rim = evaluate_field(group, edge_boundary_field(nodes, links), "FLOAT", "POINT")
|
|
||||||
return sample_field(group, original, rim, "FLOAT", index=nearest.outputs["Index"])
|
|
||||||
|
|
||||||
|
|
||||||
def taper_split_profile(group, geometry, original_boundary):
|
|
||||||
"""Taper balloon thickness through the shared two-ring influence builder."""
|
|
||||||
nodes = group.nodes
|
nodes = group.nodes
|
||||||
links = group.links
|
links = group.links
|
||||||
influence = boundary_influence(group, original_boundary)
|
retained = _math(group, "SUBTRACT", 1, boundary_influence(group, boundary=cut_boundary))
|
||||||
controls = group_input(nodes, {"Depth Split"})
|
|
||||||
has_split = compare_node(group, "GREATER_THAN", controls.outputs["Depth Split"], 0)
|
|
||||||
band = nodes.new("GeometryNodeSwitch")
|
|
||||||
band.input_type = "FLOAT"
|
|
||||||
band.inputs["False"].default_value = 0.0
|
|
||||||
links.new(has_split, band.inputs["Switch"])
|
|
||||||
links.new(influence, band.inputs["True"])
|
|
||||||
band = band.outputs[0]
|
|
||||||
retained = _math(group, "SQRT", _math(group, "MAXIMUM", _math(group, "SUBTRACT", 1, band), 0))
|
|
||||||
profile = _math(group, "MULTIPLY", read_float_attribute(group, "o_balloon"), retained)
|
profile = _math(group, "MULTIPLY", read_float_attribute(group, "o_balloon"), retained)
|
||||||
return store_float_attribute(group, geometry, "o_balloon", profile)
|
return store_float_attribute(group, geometry, "o_balloon", profile)
|
||||||
|
|||||||
@@ -7,7 +7,7 @@ from .nodes import (
|
|||||||
from .smoothing import edge_boundary_field
|
from .smoothing import edge_boundary_field
|
||||||
|
|
||||||
|
|
||||||
CUT_ATTRIBUTE = "_o_anyimage_depth_cut"
|
CUT_ATTRIBUTE = "_o_depth_cut"
|
||||||
|
|
||||||
|
|
||||||
def _math(group, operation, a, b=None):
|
def _math(group, operation, a, b=None):
|
||||||
|
|||||||
@@ -2,7 +2,7 @@
|
|||||||
import bpy
|
import bpy
|
||||||
from anyimage.operators.cutout_tool.shape import NORMAL_REDUCTION_ATTRIBUTE_NAME
|
from anyimage.operators.cutout_tool.shape import NORMAL_REDUCTION_ATTRIBUTE_NAME
|
||||||
from ..common.nodes import (
|
from ..common.nodes import (
|
||||||
interface_socket, group_input, float_input, read_float_attribute, read_vector_attribute,
|
interface_socket, group_input, float_input, boolean_node, read_float_attribute, read_vector_attribute,
|
||||||
store_float_attribute, store_vector_attribute, remove_attribute_pattern,
|
store_float_attribute, store_vector_attribute, remove_attribute_pattern,
|
||||||
prepare_node_group, evaluate_field, compare_node,
|
prepare_node_group, evaluate_field, compare_node,
|
||||||
)
|
)
|
||||||
@@ -10,62 +10,97 @@ from ..common.boundary_smoothing import smooth_cut_boundary
|
|||||||
from ..common.cutout import _position_field, _shade_output
|
from ..common.cutout import _position_field, _shade_output
|
||||||
from ..common.depth_surface import _math, project_depth_surface, build_surface_camera, split_depth_surface, sample_face_camera
|
from ..common.depth_surface import _math, project_depth_surface, build_surface_camera, split_depth_surface, sample_face_camera
|
||||||
from ..common.smoothing import edge_boundary_field
|
from ..common.smoothing import edge_boundary_field
|
||||||
from ..common.cutout_boundary import original_boundary_field, remove_boundary_triangles, taper_split_profile
|
from ..common.cutout_boundary import remove_boundary_triangles, taper_split_profile
|
||||||
|
|
||||||
|
|
||||||
NODE_GROUP_NAME = "O Image Depth Cutout"
|
NODE_GROUP_NAME = "O Image Depth Cutout"
|
||||||
SOLID_NORMAL_ATTRIBUTE = "_o_solid_normal"
|
FRONT_NORMAL_ATTRIBUTE = "_o_front_normal"
|
||||||
BALLOON_THICKNESS_SCALE = 2.0
|
BALLOON_THICKNESS_BASE = 1.0
|
||||||
FRONT_INFLATION_SCALE = 0.5
|
NORMAL_SMOOTH_ITERATIONS = 256
|
||||||
|
SOLID_WELD_SCALE = 1e-3
|
||||||
|
|
||||||
|
|
||||||
def build_normal_weight(group, is_shell):
|
def build_normal_weight(group, is_shell, profile):
|
||||||
"""Use full shell smoothing and thickness-weighted balloon smoothing."""
|
"""Keep shell smoothing and let the balloon profile drive the blend."""
|
||||||
control = group_input(group.nodes, {"Thickness"})
|
weight = group.nodes.new("GeometryNodeSwitch")
|
||||||
amount = group.nodes.new("GeometryNodeSwitch")
|
weight.input_type = "FLOAT"
|
||||||
amount.input_type = "FLOAT"
|
group.links.new(is_shell, weight.inputs["Switch"])
|
||||||
group.links.new(is_shell, amount.inputs["Switch"])
|
group.links.new(profile, weight.inputs["False"])
|
||||||
group.links.new(control.outputs["Thickness"], amount.inputs["False"])
|
weight.inputs["True"].default_value = 0.0
|
||||||
amount.inputs["True"].default_value = 1
|
return weight.outputs[0]
|
||||||
ramp = group.nodes.new("ShaderNodeMapRange")
|
|
||||||
ramp.interpolation_type = "SMOOTHSTEP"
|
|
||||||
ramp.clamp = True
|
|
||||||
group.links.new(amount.outputs[0], ramp.inputs["Value"])
|
|
||||||
return ramp.outputs["Result"]
|
|
||||||
|
|
||||||
|
|
||||||
def blend_normal(group, original, smoothed, weight):
|
def build_profile_weight(group, geometry):
|
||||||
"""Normalize the thickness-weighted blend on the consumer geometry."""
|
"""Normalize the corrected balloon profile into a zero to one blend weight."""
|
||||||
mix = group.nodes.new("ShaderNodeMix")
|
profile = read_float_attribute(group, "o_balloon")
|
||||||
mix.data_type = "VECTOR"
|
statistics = group.nodes.new("GeometryNodeAttributeStatistic")
|
||||||
if isinstance(weight, (int, float)):
|
statistics.data_type, statistics.domain = "FLOAT", "POINT"
|
||||||
mix.inputs[0].default_value = weight
|
group.links.new(geometry, statistics.inputs["Geometry"])
|
||||||
else:
|
group.links.new(profile, statistics.inputs["Attribute"])
|
||||||
group.links.new(weight, mix.inputs[0])
|
return _math(
|
||||||
group.links.new(original, mix.inputs[4])
|
group, "DIVIDE", profile,
|
||||||
group.links.new(smoothed, mix.inputs[5])
|
_math(group, "MAXIMUM", statistics.outputs["Max"], 1e-6),
|
||||||
normalize = group.nodes.new("ShaderNodeVectorMath")
|
)
|
||||||
|
|
||||||
|
|
||||||
|
def build_island_normal(group, normal):
|
||||||
|
"""Average the smoothed normals of one connected mesh island."""
|
||||||
|
nodes, links = group.nodes, group.links
|
||||||
|
island = nodes.new("GeometryNodeInputMeshIsland")
|
||||||
|
accumulate = nodes.new("GeometryNodeAccumulateField")
|
||||||
|
accumulate.data_type, accumulate.domain = "FLOAT_VECTOR", "POINT"
|
||||||
|
links.new(normal, accumulate.inputs["Value"])
|
||||||
|
links.new(island.outputs["Island Index"], accumulate.inputs["Group Index"])
|
||||||
|
normalize = nodes.new("ShaderNodeVectorMath")
|
||||||
normalize.operation = "NORMALIZE"
|
normalize.operation = "NORMALIZE"
|
||||||
group.links.new(mix.outputs["Result"], normalize.inputs[0])
|
links.new(accumulate.outputs["Total"], normalize.inputs[0])
|
||||||
return normalize.outputs[0]
|
return normalize.outputs["Vector"]
|
||||||
|
|
||||||
|
|
||||||
def build_front_normal(group, normal_weight):
|
def build_smoothed_normals(group):
|
||||||
"""Smooth the projected surface normals that drive the inflation."""
|
"""Return the smoothed normals and the average normal of each mesh island."""
|
||||||
links = group.links
|
nodes, links = group.nodes, group.links
|
||||||
normal = group.nodes.new("GeometryNodeInputNormal")
|
normal = group.nodes.new("GeometryNodeInputNormal")
|
||||||
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"])
|
||||||
links.new(group_input(group.nodes, {"Normal Smooth"}).outputs["Normal Smooth"], blur.inputs["Iterations"])
|
blur.inputs["Iterations"].default_value = NORMAL_SMOOTH_ITERATIONS
|
||||||
return blend_normal(group, normal.outputs[0], blur.outputs[0], normal_weight)
|
smoothed = blur.outputs[0]
|
||||||
|
return smoothed, build_island_normal(group, smoothed)
|
||||||
|
|
||||||
|
|
||||||
|
def build_normal_direction(group, controls, smoothed, average, weight):
|
||||||
|
"""Blend the smoothed normal against the island average normal."""
|
||||||
|
nodes, links = group.nodes, group.links
|
||||||
|
difference = group.nodes.new("ShaderNodeVectorMath")
|
||||||
|
difference.operation = "SUBTRACT"
|
||||||
|
links.new(average, difference.inputs[0])
|
||||||
|
links.new(smoothed, difference.inputs[1])
|
||||||
|
lean = _math(
|
||||||
|
group, "MULTIPLY", _math(group, "SUBTRACT", 1.0, weight),
|
||||||
|
controls.outputs["Edge Turn"],
|
||||||
|
)
|
||||||
|
blend = _math(group, "ADD", weight, lean)
|
||||||
|
offset = group.nodes.new("ShaderNodeVectorMath")
|
||||||
|
offset.operation = "SCALE"
|
||||||
|
links.new(difference.outputs["Vector"], offset.inputs[0])
|
||||||
|
links.new(blend, offset.inputs[3])
|
||||||
|
direction = group.nodes.new("ShaderNodeVectorMath")
|
||||||
|
direction.operation = "ADD"
|
||||||
|
links.new(smoothed, direction.inputs[0])
|
||||||
|
links.new(offset.outputs["Vector"], direction.inputs[1])
|
||||||
|
normalize = group.nodes.new("ShaderNodeVectorMath")
|
||||||
|
normalize.operation = "NORMALIZE"
|
||||||
|
links.new(direction.outputs["Vector"], normalize.inputs[0])
|
||||||
|
return normalize.outputs["Vector"]
|
||||||
|
|
||||||
|
|
||||||
def build_inflation_amount(group, controls, is_shell):
|
def build_inflation_amount(group, controls, is_shell):
|
||||||
"""Balloon scales the profile, Shell moves a fixed distance."""
|
"""Balloon scales the profile by Depth Scale, Shell moves a fixed distance."""
|
||||||
nodes, links = group.nodes, group.links
|
nodes, links = group.nodes, group.links
|
||||||
thickness = _math(
|
thickness = _math(
|
||||||
group, "MULTIPLY", controls.outputs["Thickness"], BALLOON_THICKNESS_SCALE,
|
group, "MULTIPLY", controls.outputs["Thickness"],
|
||||||
|
_math(group, "ADD", BALLOON_THICKNESS_BASE, controls.outputs["Depth Scale"]),
|
||||||
)
|
)
|
||||||
balloon = _math(
|
balloon = _math(
|
||||||
group, "MULTIPLY", read_float_attribute(group, "o_balloon"), thickness,
|
group, "MULTIPLY", read_float_attribute(group, "o_balloon"), thickness,
|
||||||
@@ -89,9 +124,15 @@ def build_solid(group, surface, amount, normal, is_shell, controls, weld_distanc
|
|||||||
links.new(distance, result.inputs[3])
|
links.new(distance, result.inputs[3])
|
||||||
return result.outputs[0]
|
return result.outputs[0]
|
||||||
|
|
||||||
|
# The front leaf moves by Thickness x o_balloon x Front Inflation, independent of
|
||||||
|
# the Depth Scale that scales the rear recession.
|
||||||
inflation = _math(
|
inflation = _math(
|
||||||
group, "MULTIPLY", amount,
|
group, "MULTIPLY",
|
||||||
_math(group, "MULTIPLY", controls.outputs["Front Inflation"], FRONT_INFLATION_SCALE),
|
_math(
|
||||||
|
group, "MULTIPLY", read_float_attribute(group, "o_balloon"),
|
||||||
|
controls.outputs["Thickness"],
|
||||||
|
),
|
||||||
|
controls.outputs["Front Inflation"],
|
||||||
)
|
)
|
||||||
front_amount = nodes.new("GeometryNodeSwitch")
|
front_amount = nodes.new("GeometryNodeSwitch")
|
||||||
front_amount.input_type = "FLOAT"
|
front_amount.input_type = "FLOAT"
|
||||||
@@ -103,17 +144,47 @@ def build_solid(group, surface, amount, normal, is_shell, controls, weld_distanc
|
|||||||
links.new(surface, front.inputs["Geometry"])
|
links.new(surface, front.inputs["Geometry"])
|
||||||
links.new(offset(front_amount.outputs[0]), front.inputs["Offset"])
|
links.new(offset(front_amount.outputs[0]), front.inputs["Offset"])
|
||||||
|
|
||||||
span = _math(group, "ADD", amount, front_amount.outputs[0])
|
# The rear leaf inflates along the negated front direction, straight from the
|
||||||
span = _math(group, "MULTIPLY", span, -1.0)
|
# untouched surface so the front offset cannot drag it along.
|
||||||
|
span = _math(group, "MULTIPLY", amount, -1.0)
|
||||||
extrude = nodes.new("GeometryNodeExtrudeMesh")
|
extrude = nodes.new("GeometryNodeExtrudeMesh")
|
||||||
extrude.mode = "FACES"
|
extrude.mode = "FACES"
|
||||||
extrude.inputs["Individual"].default_value = False
|
extrude.inputs["Individual"].default_value = False
|
||||||
links.new(front.outputs["Geometry"], extrude.inputs["Mesh"])
|
links.new(surface, extrude.inputs["Mesh"])
|
||||||
links.new(offset(span), extrude.inputs["Offset"])
|
links.new(offset(span), extrude.inputs["Offset"])
|
||||||
|
|
||||||
|
# The rear surface smooths in proportion to its thickness and the balloon profile,
|
||||||
|
# so thin shells and cut boundaries keep their exact positions.
|
||||||
|
thickness = nodes.new("GeometryNodeSwitch")
|
||||||
|
thickness.input_type = "FLOAT"
|
||||||
|
links.new(is_shell, thickness.inputs["Switch"])
|
||||||
|
links.new(controls.outputs["Thickness"], thickness.inputs["False"])
|
||||||
|
links.new(controls.outputs["Shell Thickness"], thickness.inputs["True"])
|
||||||
|
weight = _math(
|
||||||
|
group, "MULTIPLY", _math(group, "MINIMUM", thickness.outputs[0], 1.0),
|
||||||
|
read_float_attribute(group, "o_balloon"),
|
||||||
|
)
|
||||||
|
position = nodes.new("GeometryNodeInputPosition")
|
||||||
|
blur = nodes.new("GeometryNodeBlurAttribute")
|
||||||
|
blur.data_type = "FLOAT_VECTOR"
|
||||||
|
blur.inputs["Weight"].default_value = 1.0
|
||||||
|
links.new(position.outputs["Position"], blur.inputs["Value"])
|
||||||
|
links.new(group_input(nodes, {"Back Smooth"}).outputs["Back Smooth"], blur.inputs["Iterations"])
|
||||||
|
difference = nodes.new("ShaderNodeVectorMath")
|
||||||
|
difference.operation = "SUBTRACT"
|
||||||
|
links.new(blur.outputs["Value"], difference.inputs[0])
|
||||||
|
links.new(position.outputs["Position"], difference.inputs[1])
|
||||||
|
smooth_offset = nodes.new("ShaderNodeVectorMath")
|
||||||
|
smooth_offset.operation = "SCALE"
|
||||||
|
links.new(difference.outputs["Vector"], smooth_offset.inputs[0])
|
||||||
|
links.new(weight, smooth_offset.inputs[3])
|
||||||
|
smoothed = nodes.new("GeometryNodeSetPosition")
|
||||||
|
links.new(extrude.outputs["Mesh"], smoothed.inputs["Geometry"])
|
||||||
|
links.new(smooth_offset.outputs["Vector"], smoothed.inputs["Offset"])
|
||||||
|
|
||||||
# Extruding against the normal leaves the rear leaf and wall facing inward.
|
# Extruding against the normal leaves the rear leaf and wall facing inward.
|
||||||
flipped = nodes.new("GeometryNodeFlipFaces")
|
flipped = nodes.new("GeometryNodeFlipFaces")
|
||||||
links.new(extrude.outputs["Mesh"], flipped.inputs["Mesh"])
|
links.new(smoothed.outputs["Geometry"], flipped.inputs["Mesh"])
|
||||||
rear = store_float_attribute(
|
rear = store_float_attribute(
|
||||||
group, flipped.outputs["Mesh"], NORMAL_REDUCTION_ATTRIBUTE_NAME, 1.0,
|
group, flipped.outputs["Mesh"], NORMAL_REDUCTION_ATTRIBUTE_NAME, 1.0,
|
||||||
)
|
)
|
||||||
@@ -133,7 +204,6 @@ def _build_depth_surface(group, geometry, controls):
|
|||||||
split = controls.outputs["Depth Split"]
|
split = controls.outputs["Depth Split"]
|
||||||
image = controls.outputs["Depth Image"]
|
image = controls.outputs["Depth Image"]
|
||||||
scale = controls.outputs["Uniform Scale"]
|
scale = controls.outputs["Uniform Scale"]
|
||||||
original = geometry
|
|
||||||
geometry, corner_camera, cut_vertex = split_depth_surface(
|
geometry, corner_camera, cut_vertex = split_depth_surface(
|
||||||
group, geometry, sample_face_camera(group, image), split, controls.outputs["Reference Depth"],
|
group, geometry, sample_face_camera(group, image), split, controls.outputs["Reference Depth"],
|
||||||
controls.outputs["Depth Scale"], triangles=True,
|
controls.outputs["Depth Scale"], triangles=True,
|
||||||
@@ -141,6 +211,11 @@ def _build_depth_surface(group, geometry, controls):
|
|||||||
geometry = remove_boundary_triangles(group, geometry)
|
geometry = remove_boundary_triangles(group, geometry)
|
||||||
boundary = edge_boundary_field(group.nodes, group.links)
|
boundary = edge_boundary_field(group.nodes, group.links)
|
||||||
rim = evaluate_field(group, boundary, "FLOAT", "POINT")
|
rim = evaluate_field(group, boundary, "FLOAT", "POINT")
|
||||||
|
cut = evaluate_field(
|
||||||
|
group, boolean_node(group, "AND", boundary, cut_vertex), "FLOAT", "POINT",
|
||||||
|
)
|
||||||
|
# Only the Depth Split cuts taper the profile; the outline keeps its thickness.
|
||||||
|
geometry = taper_split_profile(group, geometry, cut)
|
||||||
camera = build_surface_camera(
|
camera = build_surface_camera(
|
||||||
group, _position_field(group, image), corner_camera, cut_vertex, split,
|
group, _position_field(group, image), corner_camera, cut_vertex, split,
|
||||||
)
|
)
|
||||||
@@ -163,8 +238,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, projection, original_boundary_field(group, original, rim))
|
# The mode field is shared by the thickness and direction 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"
|
||||||
for item, name in zip(mode.enum_items, ("Balloon", "Shell")):
|
for item, name in zip(mode.enum_items, ("Balloon", "Shell")):
|
||||||
@@ -179,23 +253,45 @@ def _build_depth_surface(group, geometry, controls):
|
|||||||
links.new(projection, statistics.inputs["Geometry"])
|
links.new(projection, statistics.inputs["Geometry"])
|
||||||
links.new(amount, statistics.inputs["Attribute"])
|
links.new(amount, statistics.inputs["Attribute"])
|
||||||
has_thickness = compare_node(group, "GREATER_THAN", statistics.outputs["Max"], 1e-6)
|
has_thickness = compare_node(group, "GREATER_THAN", statistics.outputs["Max"], 1e-6)
|
||||||
zero_shell = group.nodes.new("GeometryNodeSwitch")
|
profile = build_profile_weight(group, projection)
|
||||||
zero_shell.input_type = "GEOMETRY"
|
smoothed, average = build_smoothed_normals(group)
|
||||||
links.new(has_thickness, zero_shell.inputs["Switch"])
|
weight = build_normal_weight(group, is_shell, profile)
|
||||||
links.new(projection, zero_shell.inputs["False"])
|
# Material normal strength: the balloon profile gates it, Depth Scale attenuates it.
|
||||||
normal = build_front_normal(group, build_normal_weight(group, is_shell))
|
inflated = group.nodes.new("GeometryNodeSwitch")
|
||||||
# Both leaves must inflate along one normal, so it is carried across the
|
inflated.input_type = "FLOAT"
|
||||||
# extrusion instead of being evaluated again on the moved surface.
|
links.new(is_shell, inflated.inputs["Switch"])
|
||||||
|
links.new(profile, inflated.inputs["False"])
|
||||||
|
inflated.inputs["True"].default_value = 1.0
|
||||||
|
normal_strength = group.nodes.new("GeometryNodeSwitch")
|
||||||
|
normal_strength.input_type = "FLOAT"
|
||||||
|
links.new(has_thickness, normal_strength.inputs["Switch"])
|
||||||
|
normal_strength.inputs["False"].default_value = 1.0
|
||||||
|
links.new(inflated.outputs[0], normal_strength.inputs["True"])
|
||||||
|
reduction = _math(
|
||||||
|
group, "SUBTRACT", 1.0,
|
||||||
|
_math(group, "MULTIPLY", controls.outputs["Depth Scale"], normal_strength.outputs[0]),
|
||||||
|
)
|
||||||
|
projection = store_float_attribute(
|
||||||
|
group, projection, NORMAL_REDUCTION_ATTRIBUTE_NAME, reduction,
|
||||||
|
)
|
||||||
|
# The rear leaf inflates along the negated front direction, so one direction is
|
||||||
|
# carried across the extrusion instead of being re-evaluated on the moved surface.
|
||||||
projection = store_vector_attribute(
|
projection = store_vector_attribute(
|
||||||
group, projection, SOLID_NORMAL_ATTRIBUTE, normal,
|
group, projection, FRONT_NORMAL_ATTRIBUTE,
|
||||||
|
build_normal_direction(group, controls, smoothed, average, weight),
|
||||||
)
|
)
|
||||||
solid = build_solid(
|
solid = build_solid(
|
||||||
group, projection, amount, read_vector_attribute(group, SOLID_NORMAL_ATTRIBUTE),
|
group, projection, amount,
|
||||||
|
read_vector_attribute(group, FRONT_NORMAL_ATTRIBUTE),
|
||||||
is_shell, controls,
|
is_shell, controls,
|
||||||
_math(group, "MULTIPLY", typical.outputs["Mean"], 1e-3),
|
_math(group, "MULTIPLY", typical.outputs["Mean"], SOLID_WELD_SCALE),
|
||||||
)
|
)
|
||||||
links.new(solid, zero_shell.inputs["True"])
|
shaped = group.nodes.new("GeometryNodeSwitch")
|
||||||
return remove_attribute_pattern(group, zero_shell.outputs[0], "_o_*")
|
shaped.input_type = "GEOMETRY"
|
||||||
|
links.new(has_thickness, shaped.inputs["Switch"])
|
||||||
|
links.new(projection, shaped.inputs["False"])
|
||||||
|
links.new(solid, shaped.inputs["True"])
|
||||||
|
return remove_attribute_pattern(group, shaped.outputs[0], "_o_*")
|
||||||
|
|
||||||
|
|
||||||
def build_image_depth_cutout_group():
|
def build_image_depth_cutout_group():
|
||||||
@@ -216,18 +312,18 @@ def build_image_depth_cutout_group():
|
|||||||
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 = 4, 0, 16
|
boundary_smooth.default_value, boundary_smooth.min_value, boundary_smooth.max_value = 4, 0, 16
|
||||||
boundary_smooth.description = 'Smooth geometry near the original outline and Depth Split edges. Zero disables boundary smoothing.'
|
boundary_smooth.description = 'Smooth geometry near the original outline and Depth Split edges. Zero disables boundary smoothing.'
|
||||||
normal_smooth = interface_socket(group, 'Normal Smooth', 'INPUT', 'NodeSocketInt', options)
|
back_smooth = interface_socket(group, 'Back Smooth', 'INPUT', 'NodeSocketInt', options)
|
||||||
normal_smooth.default_value, normal_smooth.min_value, normal_smooth.max_value = 512, 16, 512
|
back_smooth.default_value, back_smooth.min_value, back_smooth.max_value = 8, 0, 16
|
||||||
normal_smooth.description = 'Control smoothing of front and rear displacement normals. Higher values produce smoother directions.'
|
back_smooth.description = 'Soften the rear surface where the balloon is thick. The effect follows Thickness and the balloon profile, so thin geometry and cut boundaries keep their positions.'
|
||||||
float_input(group, 'Front Inflation', 0.2, maximum=1, subtype='FACTOR', parent=options).description = 'Control how much the front surface bulges in Balloon mode, reaching half of the balloon thickness. Zero keeps the original front surface.'
|
float_input(group, 'Edge Turn', 2.0, minimum=-3.0, maximum=3.0, parent=options).description = 'Turn the outline direction: zero follows the smoothed normal, one reaches the island average normal, two mirrors the smoothed normal across it, and higher or negative values keep turning either way.'
|
||||||
|
float_input(group, 'Front Inflation', 0.2, maximum=1, subtype='FACTOR', parent=options).description = 'Control how much the front surface bulges in Balloon mode, as a share of the balloon thickness. Zero keeps the original front surface.'
|
||||||
float_input(group, 'Reference Depth', 1.0, subtype='DISTANCE', parent=options)
|
float_input(group, 'Reference Depth', 1.0, subtype='DISTANCE', parent=options)
|
||||||
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
|
||||||
interface_socket(group, 'Geometry', 'OUTPUT', 'NodeSocketGeometry')
|
interface_socket(group, 'Geometry', 'OUTPUT', 'NodeSocketGeometry')
|
||||||
controls = group_input(group.nodes, set())
|
controls = group_input(group.nodes, set())
|
||||||
geometry = store_float_attribute(group, controls.outputs['Geometry'], NORMAL_REDUCTION_ATTRIBUTE_NAME, 0.0)
|
geometry = _build_depth_surface(group, controls.outputs['Geometry'], controls)
|
||||||
geometry = _build_depth_surface(group, geometry, controls)
|
|
||||||
_shade_output(group, geometry)
|
_shade_output(group, geometry)
|
||||||
prepare_node_group(group)
|
prepare_node_group(group)
|
||||||
for item in group.interface.items_tree:
|
for item in group.interface.items_tree:
|
||||||
|
|||||||
Reference in New Issue
Block a user