Refactor geometry generation to standardize Blender object coordinates

- Unified the canonical local frame for generated geometries (Plane, Depth Plane, Relief Plane, Cutout, Depth Cutout) to align with Blender's object coordinate system, setting X as horizontal, Z as up, and +Y as depth.
- Removed the `Depth Axis` parameter from `O Image Cutout` and `O Image Depth Cutout`, simplifying the geometry generation process.
- Updated the depth direction defaults to use +Y instead of -Z for `O Image Relief Plane` and Depth Symmetry.
- Adjusted the geometry nodes to ensure thickness, projection, and shell displacements operate along the Y-axis.
- Implemented new capabilities for handling object space normals to align with the new canonical frame.
- Enhanced the Depth Symmetry functionality to mirror across the canonical Y-axis and perform a final rotation to maintain the correct orientation.
- Added tests to validate the new coordinate system and ensure compatibility with existing assets.
This commit is contained in:
2026-09-15 13:33:37 +08:00
parent 591e62085a
commit 4035f5c9e4
47 changed files with 892 additions and 570 deletions

View File

@@ -42,14 +42,14 @@ def check_asset_library(asset_path):
continue
mesh = bpy.data.meshes.new("Asset Check Plane")
mesh.from_pydata(
[(x / 3 - 0.5, y / 3 - 0.5, 0) for y in range(4) for x in range(4)], [],
[(x / 3 - 0.5, 0, y / 3 - 0.5) for y in range(4) for x in range(4)], [],
[(y * 4 + x, y * 4 + x + 1, (y + 1) * 4 + x + 1, (y + 1) * 4 + x)
for y in range(3) for x in range(3)],
)
uv = mesh.uv_layers.new(name="UVMap")
for loop in mesh.loops:
position = mesh.vertices[loop.vertex_index].co
uv.data[loop.index].uv = (position.x + 0.5, position.y + 0.5)
uv.data[loop.index].uv = (position.x + 0.5, position.z + 0.5)
profile = mesh.attributes.new("o_balloon", "FLOAT", "POINT")
for value in profile.data:
value.value = 0.2

View File

@@ -57,39 +57,39 @@ def _extrude_layers(group, geometry):
node = group.nodes.new("GeometryNodeExtrudeMesh")
node.mode = "FACES"
node.inputs["Individual"].default_value = False
node.inputs["Offset"].default_value = (0.0, 0.0, 1.0)
# Keep distinct front and back vertices before overriding their Z positions.
node.inputs["Offset"].default_value = (0.0, 1.0, 0.0)
# Keep distinct front and back vertices before overriding their Y positions.
node.inputs["Offset Scale"].default_value = 1.0
group.links.new(geometry, node.inputs["Mesh"])
return node
def _offset_geometry_z(group, geometry, z_value):
def _offset_geometry_y(group, geometry, y_value):
nodes = group.nodes
links = group.links
offset = nodes.new("ShaderNodeCombineXYZ")
links.new(z_value, offset.inputs["Z"])
links.new(y_value, offset.inputs["Y"])
set_position = nodes.new("GeometryNodeSetPosition")
links.new(geometry, set_position.inputs["Geometry"])
links.new(offset.outputs["Vector"], set_position.inputs["Offset"])
return set_position.outputs["Geometry"]
def _set_layer_z(group, geometry, extrude, front_z, back_z):
def _set_layer_y(group, geometry, extrude, front_y, back_y):
nodes = group.nodes
links = group.links
choose_z = nodes.new("GeometryNodeSwitch")
choose_z.input_type = "FLOAT"
links.new(extrude.outputs["Top"], choose_z.inputs["Switch"])
links.new(front_z, choose_z.inputs["False"])
links.new(back_z, choose_z.inputs["True"])
choose_y = nodes.new("GeometryNodeSwitch")
choose_y.input_type = "FLOAT"
links.new(extrude.outputs["Top"], choose_y.inputs["Switch"])
links.new(front_y, choose_y.inputs["False"])
links.new(back_y, choose_y.inputs["True"])
position = nodes.new("GeometryNodeInputPosition")
separate = nodes.new("ShaderNodeSeparateXYZ")
links.new(position.outputs["Position"], separate.inputs["Vector"])
combine = nodes.new("ShaderNodeCombineXYZ")
links.new(separate.outputs["X"], combine.inputs["X"])
links.new(separate.outputs["Y"], combine.inputs["Y"])
links.new(choose_z.outputs["Output"], combine.inputs["Z"])
links.new(choose_y.outputs["Output"], combine.inputs["Y"])
links.new(separate.outputs["Z"], combine.inputs["Z"])
set_position = nodes.new("GeometryNodeSetPosition")
links.new(extrude.outputs["Mesh"], set_position.inputs["Geometry"])
links.new(combine.outputs["Vector"], set_position.inputs["Position"])
@@ -100,11 +100,11 @@ def _set_layer_z(group, geometry, extrude, front_z, back_z):
links.new(extrude.outputs["Side"], side.inputs["Selection"])
flip_side = nodes.new("GeometryNodeFlipFaces")
links.new(side.outputs["Selection"], flip_side.inputs["Mesh"])
front = _offset_geometry_z(
group, geometry, front_z
front = _offset_geometry_y(
group, geometry, front_y
)
back = _offset_geometry_z(
group, geometry, back_z
back = _offset_geometry_y(
group, geometry, back_y
)
flip_back = nodes.new("GeometryNodeFlipFaces")
links.new(back, flip_back.inputs["Mesh"])

View File

@@ -290,7 +290,7 @@ def thicken_depth_surface(group, geometry, thickness, normal_smooth=None, *,
row = nodes.new("FunctionNodeIntegerMath")
row.operation = "MULTIPLY"
row.inputs[1].default_value = 4096
links.new(layer_axes.outputs["Y"], row.inputs[0])
links.new(layer_axes.outputs["Z"], row.inputs[0])
vertex = nodes.new("FunctionNodeIntegerMath")
vertex.operation = "ADD"
links.new(layer_axes.outputs["X"], vertex.inputs[0])
@@ -303,7 +303,7 @@ def thicken_depth_surface(group, geometry, thickness, normal_smooth=None, *,
extrude.mode = "FACES"
extrude.inputs["Individual"].default_value = False
extrusion_offset = nodes.new("ShaderNodeCombineXYZ")
extrusion_offset.inputs["Z"].default_value = -1.0
extrusion_offset.inputs["Y"].default_value = -1.0
links.new(extrusion_offset.outputs[0], extrude.inputs["Offset"])
links.new(saved, extrude.inputs["Mesh"])
sides = nodes.new("GeometryNodeSeparateGeometry")
@@ -311,7 +311,7 @@ def thicken_depth_surface(group, geometry, thickness, normal_smooth=None, *,
links.new(extrude.outputs["Mesh"], sides.inputs["Geometry"])
links.new(extrude.outputs["Side"], sides.inputs["Selection"])
offset = nodes.new("ShaderNodeCombineXYZ")
offset.inputs["Z"].default_value = -1.0
offset.inputs["Y"].default_value = -1.0
back = nodes.new("GeometryNodeSetPosition")
links.new(saved, back.inputs["Geometry"])
links.new(offset.outputs[0], back.inputs["Offset"])
@@ -337,9 +337,9 @@ def thicken_depth_surface(group, geometry, thickness, normal_smooth=None, *,
if weld_zero_thickness:
positive = _math(group, "GREATER_THAN", source_thickness, 1e-12)
flattened = nodes.new("ShaderNodeCombineXYZ")
for axis in ("X", "Y"):
for axis in ("X", "Z"):
links.new(layer_axes.outputs[axis], flattened.inputs[axis])
links.new(_math(group, "MULTIPLY", positive, layer_axes.outputs["Z"]), flattened.inputs["Z"])
links.new(_math(group, "MULTIPLY", positive, layer_axes.outputs["Y"]), flattened.inputs["Y"])
collapse = nodes.new("GeometryNodeSetPosition")
links.new(construction, collapse.inputs["Geometry"])
links.new(flattened.outputs[0], collapse.inputs["Position"])
@@ -349,8 +349,8 @@ def thicken_depth_surface(group, geometry, thickness, normal_smooth=None, *,
merge.inputs["Distance"].default_value = 0.1
links.new(construction, merge.inputs["Geometry"])
displacement = nodes.new("ShaderNodeCombineXYZ")
distance = _math(group, "MULTIPLY", layer_axes.outputs["Z"], source_thickness)
links.new(distance, displacement.inputs["Z"])
distance = _math(group, "MULTIPLY", layer_axes.outputs["Y"], source_thickness)
links.new(distance, displacement.inputs["Y"])
restored = nodes.new("ShaderNodeVectorMath")
restored.operation = "ADD"
links.new(source_position, restored.inputs[0])
@@ -401,18 +401,19 @@ def project_depth_surface(
separate = nodes.new("ShaderNodeSeparateXYZ")
links.new(scaled.outputs["Vector"], separate.inputs["Vector"])
relative_depth = _projection_math(nodes, "SUBTRACT")
links.new(reference_depth, relative_depth.inputs[0])
links.new(separate.outputs["Z"], relative_depth.inputs[1])
links.new(separate.outputs["Z"], relative_depth.inputs[0])
links.new(reference_depth, relative_depth.inputs[1])
front = _projection_math(nodes, "MULTIPLY")
links.new(relative_depth.outputs[0], front.inputs[0])
links.new(depth_scale, front.inputs[1])
negative_y = _projection_math(nodes, "MULTIPLY")
negative_y.inputs[1].default_value = -1.0
links.new(separate.outputs["Y"], negative_y.inputs[0])
negative_z = _projection_math(nodes, "MULTIPLY")
negative_z.inputs[1].default_value = -1.0
links.new(separate.outputs["Y"], negative_z.inputs[0])
projected = nodes.new("ShaderNodeCombineXYZ")
links.new(separate.outputs["X"], projected.inputs["X"])
links.new(negative_y.outputs[0], projected.inputs["Y"])
links.new(front.outputs[0], projected.inputs["Y"])
links.new(negative_z.outputs[0], projected.inputs["Z"])
current_position = nodes.new("GeometryNodeInputPosition")
blend = nodes.new("ShaderNodeMix")
blend.data_type = "VECTOR"
@@ -424,8 +425,8 @@ def project_depth_surface(
links.new(blend.outputs["Result"], blended_axes.inputs["Vector"])
surface_position = nodes.new("ShaderNodeCombineXYZ")
links.new(blended_axes.outputs["X"], surface_position.inputs["X"])
links.new(blended_axes.outputs["Y"], surface_position.inputs["Y"])
links.new(front.outputs[0], surface_position.inputs["Z"])
links.new(front.outputs[0], surface_position.inputs["Y"])
links.new(blended_axes.outputs["Z"], surface_position.inputs["Z"])
project_geometry = nodes.new("GeometryNodeSetPosition")
links.new(geometry, project_geometry.inputs["Geometry"])
links.new(surface_position.outputs["Vector"], project_geometry.inputs["Position"])

View File

@@ -0,0 +1,33 @@
"""Build nodes that decode legacy Direction parameters into canonical space."""
def legacy_direction_to_canonical(group, legacy):
nodes = group.nodes
links = group.links
axes = nodes.new("ShaderNodeSeparateXYZ")
links.new(legacy, axes.inputs[0])
negative_x = nodes.new("ShaderNodeMath")
negative_x.operation = "MULTIPLY"
negative_x.inputs[1].default_value = -1.0
links.new(axes.outputs["X"], negative_x.inputs[0])
negative_y = nodes.new("ShaderNodeMath")
negative_y.operation = "MULTIPLY"
negative_y.inputs[1].default_value = -1.0
links.new(axes.outputs["Y"], negative_y.inputs[0])
direction = nodes.new("ShaderNodeCombineXYZ")
links.new(negative_x.outputs[0], direction.inputs["X"])
links.new(axes.outputs["Z"], direction.inputs["Y"])
links.new(negative_y.outputs[0], direction.inputs["Z"])
return direction.outputs["Vector"]
def symmetry_legacy_direction_to_canonical(group, legacy):
nodes = group.nodes
links = group.links
axes = nodes.new("ShaderNodeSeparateXYZ")
links.new(legacy, axes.inputs[0])
direction = nodes.new("ShaderNodeCombineXYZ")
links.new(axes.outputs["X"], direction.inputs["X"])
links.new(axes.outputs["Z"], direction.inputs["Y"])
links.new(axes.outputs["Y"], direction.inputs["Z"])
return direction.outputs["Vector"]

View File

@@ -6,7 +6,7 @@ These FACE attributes stay on the output mesh for the material consumer.
"""
from mathutils import Matrix
from anyimage.operators.cutout_tool.shape import CUTOUT_Z_TO_X
from anyimage.common.object import blender_frame_rotation
ROTATION_ATTRIBUTE = 'o_depth_rotation'
FACE_ATTRIBUTE = 'o_depth_face'
@@ -38,28 +38,32 @@ def transform_object_normal_color(group, color, strength):
decode.inputs[1].default_value = (2., 2., 2.)
decode.inputs[2].default_value = (-1., -1., -1.)
links.new(color, decode.inputs[0])
base = nodes.new('ShaderNodeVectorRotate')
base.rotation_type = 'EULER_XYZ'
base.inputs['Rotation'].default_value = Matrix(blender_frame_rotation()).to_euler()
links.new(decode.outputs[0], base.inputs['Vector'])
rotate = nodes.new('ShaderNodeVectorRotate')
rotate.rotation_type = 'EULER_XYZ'
rotate.invert = True
links.new(decode.outputs[0], rotate.inputs['Vector'])
links.new(base.outputs[0], rotate.inputs['Vector'])
links.new(rotation.outputs['Vector'], rotate.inputs['Rotation'])
separate = nodes.new('ShaderNodeSeparateXYZ')
links.new(rotate.outputs[0], separate.inputs[0])
back = math('COMPARE', face.outputs['Fac'], 2.)
# Reflect the normal's Z component on the back.
difference = math('MULTIPLY', separate.outputs['Z'], -2.)
# Reflect the normal's Y component on the back.
difference = math('MULTIPLY', separate.outputs['Y'], -2.)
correction = nodes.new('ShaderNodeMath')
correction.operation = 'MULTIPLY_ADD'
links.new(back, correction.inputs[0])
links.new(difference, correction.inputs[1])
links.new(separate.outputs['Z'], correction.inputs[2])
links.new(separate.outputs['Y'], correction.inputs[2])
combine = nodes.new('ShaderNodeCombineXYZ')
links.new(separate.outputs['X'], combine.inputs['X'])
links.new(separate.outputs['Y'], combine.inputs['Y'])
links.new(correction.outputs[0], combine.inputs['Z'])
links.new(correction.outputs[0], combine.inputs['Y'])
links.new(separate.outputs['Z'], combine.inputs['Z'])
axis_rotation = nodes.new('ShaderNodeVectorMath')
axis_rotation.operation = 'SCALE'
axis_rotation.inputs[0].default_value = Matrix(CUTOUT_Z_TO_X).to_euler()
axis_rotation.inputs[0].default_value = Matrix.Rotation(1.5707963267948966, 4, 'Z').to_euler()
links.new(axis.outputs['Fac'], axis_rotation.inputs['Scale'])
orient = nodes.new('ShaderNodeVectorRotate')
orient.rotation_type = 'EULER_XYZ'

View File

@@ -1,5 +1,6 @@
"""Build the direction-adjusted reference depth field."""
from .nodes import math_node
from .direction import legacy_direction_to_canonical
def reference_plane_depth(group, direction, reference_depth):
@@ -8,23 +9,24 @@ def reference_plane_depth(group, direction, reference_depth):
normal = nodes.new("ShaderNodeVectorMath")
normal.operation = "NORMALIZE"
links.new(direction, normal.inputs[0])
remapped = legacy_direction_to_canonical(group, normal.outputs["Vector"])
normal_components = nodes.new("ShaderNodeSeparateXYZ")
links.new(normal.outputs["Vector"], normal_components.inputs["Vector"])
links.new(remapped, normal_components.inputs["Vector"])
position = nodes.new("GeometryNodeInputPosition")
position_components = nodes.new("ShaderNodeSeparateXYZ")
links.new(position.outputs["Position"], position_components.inputs["Vector"])
x_term = math_node(nodes, "MULTIPLY")
links.new(position_components.outputs["X"], x_term.inputs[0])
links.new(normal_components.outputs["X"], x_term.inputs[1])
y_term = math_node(nodes, "MULTIPLY")
links.new(position_components.outputs["Y"], y_term.inputs[0])
links.new(normal_components.outputs["Y"], y_term.inputs[1])
z_term = math_node(nodes, "MULTIPLY")
links.new(position_components.outputs["Z"], z_term.inputs[0])
links.new(normal_components.outputs["Z"], z_term.inputs[1])
point_dot = math_node(nodes, "ADD")
links.new(x_term.outputs[0], point_dot.inputs[0])
links.new(y_term.outputs[0], point_dot.inputs[1])
links.new(z_term.outputs[0], point_dot.inputs[1])
divide = math_node(nodes, "DIVIDE")
links.new(point_dot.outputs[0], divide.inputs[0])
links.new(normal_components.outputs["Z"], divide.inputs[1])
links.new(normal_components.outputs["Y"], divide.inputs[1])
plane = math_node(nodes, "ADD")
links.new(reference_depth, plane.inputs[0])
links.new(divide.outputs[0], plane.inputs[1])

View File

@@ -1,15 +1,14 @@
"""Build O Image Cutout."""
import bpy
from mathutils import Matrix
from anyimage.operators.cutout_tool.shape import CUTOUT_Z_TO_X, NORMAL_REDUCTION_ATTRIBUTE_NAME
from anyimage.operators.cutout_tool.shape import NORMAL_REDUCTION_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.cutout import _extrude_layers, _island_cleanup, _offset_geometry_z, _remove_zero_thickness, _set_layer_z, _shade_output
from ..common.cutout import _extrude_layers, _island_cleanup, _offset_geometry_y, _remove_zero_thickness, _set_layer_y, _shade_output
NODE_GROUP_NAME = "O Image Cutout"
def _set_segmented_layer_z(
def _set_segmented_layer_y(
group,
geometry,
front_z,
@@ -56,7 +55,7 @@ def _set_segmented_layer_z(
links.new(thickness, step.inputs[0])
links.new(segment_limit.outputs[0], step.inputs[1])
front = _offset_geometry_z(group, geometry, front_z)
front = _offset_geometry_y(group, geometry, front_z)
repeat_output = nodes.new("GeometryNodeRepeatOutput")
repeat_input = nodes.new("GeometryNodeRepeatInput")
repeat_input.pair_with_output(repeat_output)
@@ -84,7 +83,7 @@ def _set_segmented_layer_z(
links.new(split_extrude.outputs["Inverted"], repeat_output.inputs["Geometry"])
links.new(join_sides.outputs["Geometry"], repeat_output.inputs["Sides"])
positioned_side = _offset_geometry_z(
positioned_side = _offset_geometry_y(
group,
repeat_output.outputs["Sides"],
front_z,
@@ -92,7 +91,7 @@ def _set_segmented_layer_z(
flip_side = nodes.new("GeometryNodeFlipFaces")
links.new(positioned_side, flip_side.inputs["Mesh"])
back = _offset_geometry_z(group, geometry, back_z)
back = _offset_geometry_y(group, geometry, back_z)
flip_back = nodes.new("GeometryNodeFlipFaces")
links.new(back, flip_back.inputs["Mesh"])
join = nodes.new("GeometryNodeJoinGeometry")
@@ -113,9 +112,9 @@ def _build_shell(group, geometry, thickness):
front.inputs[1].default_value = 0.0
links.new(thickness, front.inputs[0])
back = math_node(nodes, "MULTIPLY")
back.inputs[1].default_value = -1.0
back.inputs[1].default_value = 1.0
links.new(thickness, back.inputs[0])
shaped = _set_segmented_layer_z(
shaped = _set_segmented_layer_y(
group,
geometry,
front.outputs[0],
@@ -132,16 +131,16 @@ def _build_balloon(group, geometry, thickness):
front_scale = math_node(nodes, "MULTIPLY")
links.new(profile, front_scale.inputs[0])
links.new(thickness, front_scale.inputs[1])
negative_back = math_node(nodes, "MULTIPLY")
negative_back.inputs[1].default_value = -1.0
links.new(front_scale.outputs[0], negative_back.inputs[0])
negative_front = math_node(nodes, "MULTIPLY")
negative_front.inputs[1].default_value = -1.0
links.new(front_scale.outputs[0], negative_front.inputs[0])
extrude = _extrude_layers(group, geometry)
shaped = _set_layer_z(
shaped = _set_layer_y(
group,
geometry,
extrude,
negative_front.outputs[0],
front_scale.outputs[0],
negative_back.outputs[0],
)
return _remove_zero_thickness(group, geometry, shaped, thickness)
@@ -160,7 +159,6 @@ def build_image_cutout_group():
float_input(group, 'Shell Thickness', 0.0, maximum=1.0, subtype='DISTANCE').description = 'Add thickness behind the original surface while keeping the front surface in place.'
options = group.interface.new_panel(name='Options', default_closed=True)
float_input(group, 'Cleanup Threshold', 0.0, parent=options)
interface_socket(group, 'Depth Axis', 'INPUT', 'NodeSocketMenu', options).menu_expanded = True
interface_socket(group, 'Geometry', 'OUTPUT', 'NodeSocketGeometry')
controls = group_input(group.nodes, set())
balloon = _build_balloon(group, controls.outputs['Geometry'], controls.outputs['Thickness'])
@@ -195,21 +193,13 @@ def build_image_cutout_group():
reduction.inputs[0].default_value = 1.0
group.links.new(scale, reduction.inputs[1])
geometry = store_float_attribute(group, geometry, NORMAL_REDUCTION_ATTRIBUTE_NAME, reduction.outputs[0])
orient = group.nodes.new('GeometryNodeTransform')
orient.inputs['Rotation'].default_value = Matrix(CUTOUT_Z_TO_X).to_euler()
group.links.new(geometry, orient.inputs['Geometry'])
axis = menu_switch(group, 'Depth Axis', ('-Z', '+X'), 'GEOMETRY')
group.links.new(geometry, axis.inputs['-Z'])
group.links.new(orient.outputs[0], axis.inputs['+X'])
_shade_output(group, axis.outputs[0])
_shade_output(group, geometry)
prepare_node_group(group)
for item in group.interface.items_tree:
if item.item_type != 'SOCKET' or item.in_out != 'INPUT':
continue
if item.name == 'Mode':
item.default_value = 'Balloon'
elif item.name == 'Depth Axis':
item.default_value = '-Z'
elif item.name == 'Shell Thickness':
item.name = 'Thickness'
return group

View File

@@ -1,5 +1,6 @@
"""Mirror completed depth geometry and fill matching boundaries."""
import bpy
from mathutils import Matrix
from ..common.nodes import (
interface_socket, group_input, float_input, bool_input, vector_input,
@@ -7,6 +8,7 @@ from ..common.nodes import (
)
from ..common.depth_surface import _math
from ..common.normal_map import ROTATION_ATTRIBUTE, FACE_ATTRIBUTE, AXIS_ATTRIBUTE
from ..common.direction import symmetry_legacy_direction_to_canonical
NODE_GROUP_NAME = "O Image Cutout Symmetry"
@@ -14,9 +16,10 @@ NODE_GROUP_NAME = "O Image Cutout Symmetry"
def _align_front(group, geometry, controls):
"""Align existing geometry with the fixed XY symmetry plane."""
nodes, links = group.nodes, group.links
direction = symmetry_legacy_direction_to_canonical(group, controls['Direction'])
align = nodes.new('FunctionNodeAlignEulerToVector')
align.axis = 'Z'
links.new(controls['Direction'], align.inputs['Vector'])
align.axis = 'Y'
links.new(direction, align.inputs['Vector'])
position = nodes.new('GeometryNodeInputPosition')
rotate = nodes.new('ShaderNodeVectorRotate')
rotate.rotation_type = 'EULER_XYZ'
@@ -24,15 +27,19 @@ def _align_front(group, geometry, controls):
links.new(position.outputs[0], rotate.inputs['Vector'])
links.new(align.outputs['Rotation'], rotate.inputs['Rotation'])
offset = nodes.new('ShaderNodeCombineXYZ')
links.new(controls['Offset'], offset.inputs['Z'])
offset_value = nodes.new('ShaderNodeMath')
offset_value.operation = 'MULTIPLY'
offset_value.inputs[1].default_value = -1.0
links.new(controls['Offset'], offset_value.inputs[0])
links.new(offset_value.outputs[0], offset.inputs['Y'])
offset_position = nodes.new('ShaderNodeVectorMath')
offset_position.operation = 'ADD'
links.new(rotate.outputs[0], offset_position.inputs[0])
links.new(offset.outputs[0], offset_position.inputs[1])
scale_vector = nodes.new('ShaderNodeCombineXYZ')
scale_vector.inputs['X'].default_value = 1
scale_vector.inputs['Y'].default_value = 1
links.new(controls['Scale'], scale_vector.inputs['Z'])
scale_vector.inputs['Z'].default_value = 1
links.new(controls['Scale'], scale_vector.inputs['Y'])
scale = nodes.new('ShaderNodeVectorMath')
scale.operation = 'MULTIPLY'
links.new(offset_position.outputs[0], scale.inputs[0])
@@ -48,32 +55,32 @@ def _align_front(group, geometry, controls):
return store.outputs['Geometry']
def _delete_negative_faces(group, front, tolerance):
"""Discard faces touching negative Z without leaving wires or isolated vertices."""
def _delete_positive_faces(group, front, tolerance):
"""Discard faces touching positive Y without leaving wires or isolated vertices."""
position = group.nodes.new('GeometryNodeInputPosition')
components = group.nodes.new('ShaderNodeSeparateXYZ')
group.links.new(position.outputs[0], components.inputs[0])
negative = _math(group, 'LESS_THAN', components.outputs['Z'], tolerance)
point_negative = evaluate_field(group, negative, 'FLOAT', 'POINT')
face_negative = evaluate_field(group, point_negative, 'FLOAT', 'FACE')
positive = _math(group, 'GREATER_THAN', components.outputs['Y'], tolerance)
point_positive = evaluate_field(group, positive, 'FLOAT', 'POINT')
face_positive = evaluate_field(group, point_positive, 'FLOAT', 'FACE')
delete = group.nodes.new('GeometryNodeDeleteGeometry')
delete.domain = 'FACE'
delete.mode = 'ALL'
group.links.new(front, delete.inputs['Geometry'])
group.links.new(_math(group, 'GREATER_THAN', face_negative, 0), delete.inputs['Selection'])
group.links.new(_math(group, 'GREATER_THAN', face_positive, 0), delete.inputs['Selection'])
return delete.outputs[0]
def _mirror(group, geometry):
mirror = group.nodes.new('GeometryNodeTransform')
mirror.inputs['Scale'].default_value = (1, 1, -1)
mirror.inputs['Scale'].default_value = (1, -1, 1)
group.links.new(geometry, mirror.inputs['Geometry'])
flip = group.nodes.new('GeometryNodeFlipFaces')
group.links.new(mirror.outputs[0], flip.inputs['Mesh'])
return flip.outputs[0]
def _extrude_boundary(group, geometry, selection, z_factor):
def _extrude_boundary(group, geometry, selection, y_factor):
"""Extrude selected edges to a fraction of their current height."""
nodes, links = group.nodes, group.links
extrude = nodes.new('GeometryNodeExtrudeMesh')
@@ -84,7 +91,7 @@ def _extrude_boundary(group, geometry, selection, z_factor):
position = nodes.new('GeometryNodeInputPosition')
scale = nodes.new('ShaderNodeVectorMath')
scale.operation = 'MULTIPLY'
scale.inputs[1].default_value = (1, 1, z_factor)
scale.inputs[1].default_value = (1, y_factor, 1)
links.new(position.outputs[0], scale.inputs[0])
place = nodes.new('GeometryNodeSetPosition')
links.new(extrude.outputs['Mesh'], place.inputs['Geometry'])
@@ -128,7 +135,7 @@ def _walls(group, front, boundary, smooth, tolerance, *, curved):
links.new(position.outputs[0], difference.inputs[1])
mask = nodes.new('ShaderNodeVectorMath')
mask.operation = 'MULTIPLY'
mask.inputs[1].default_value = (.5, .5, 0)
mask.inputs[1].default_value = (.5, 0, .5)
links.new(difference.outputs[0], mask.inputs[0])
place = nodes.new('GeometryNodeSetPosition')
links.new(geometry, place.inputs['Geometry'])
@@ -171,7 +178,7 @@ def build_image_cutout_symmetry_group():
links.new(bounds.outputs['Min'], size.inputs[0])
links.new(bounds.outputs['Max'], size.inputs[1])
tolerance = _math(group, 'MULTIPLY', _math(group, 'MAXIMUM', size.outputs['Value'], 1e-8), 1e-7)
front = _delete_negative_faces(group, _align_front(group, inputs['Geometry'], inputs), tolerance)
front = _delete_positive_faces(group, _align_front(group, inputs['Geometry'], inputs), tolerance)
shade = nodes.new('GeometryNodeSetShadeSmooth')
shade.domain = 'FACE'
links.new(front, shade.inputs['Geometry'])
@@ -194,7 +201,10 @@ def build_image_cutout_symmetry_group():
merge = nodes.new('GeometryNodeMergeByDistance')
links.new(tolerance, merge.inputs['Distance'])
links.new(join.outputs[0], merge.inputs['Geometry'])
geometry = store_float_attribute(group, merge.outputs[0], AXIS_ATTRIBUTE, 0., domain='FACE')
links.new(geometry, nodes.new('NodeGroupOutput').inputs['Geometry'])
geometry = store_float_attribute(group, merge.outputs[0], AXIS_ATTRIBUTE, 1., domain='FACE')
orient = nodes.new('GeometryNodeTransform')
orient.inputs['Rotation'].default_value = Matrix.Rotation(1.5707963267948966, 4, 'Z').to_euler()
links.new(geometry, orient.inputs['Geometry'])
links.new(orient.outputs[0], nodes.new('NodeGroupOutput').inputs['Geometry'])
prepare_node_group(group)
return group

View File

@@ -1,8 +1,7 @@
"""Build O Image Depth Cutout."""
import bpy
from mathutils import Matrix
from anyimage.operators.cutout_tool.shape import CUTOUT_Z_TO_X, NORMAL_REDUCTION_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, sample_field, evaluate_field
from anyimage.operators.cutout_tool.shape import NORMAL_REDUCTION_ATTRIBUTE_NAME
from ..common.nodes import interface_socket, group_input, math_node, float_input, read_float_attribute, store_float_attribute, prepare_node_group, sample_field, evaluate_field
from ..common.boundary_smoothing import smooth_cut_boundary
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
@@ -52,7 +51,7 @@ def build_volume_fields(group):
position = group.nodes.new("GeometryNodeInputPosition")
axes = group.nodes.new("ShaderNodeSeparateXYZ")
links.new(position.outputs[0], axes.inputs[0])
seed = _math(group, "SUBTRACT", axes.outputs["Z"], profile)
seed = _math(group, "SUBTRACT", axes.outputs["Y"], profile)
interior = _math(group, "SUBTRACT", 1, rim)
blur = group.nodes.new("GeometryNodeBlurAttribute")
blur.data_type = "FLOAT"
@@ -60,14 +59,14 @@ def build_volume_fields(group):
links.new(seed, blur.inputs["Value"])
links.new(interior, blur.inputs["Weight"])
center = blur.outputs[0]
residual = _math(group, "SUBTRACT", axes.outputs["Z"], center)
residual = _math(group, "SUBTRACT", axes.outputs["Y"], center)
full = _math(group, "ADD", residual, profile)
minimum = _math(group, "MULTIPLY", profile, 0.05)
positive = _math(group, "MAXIMUM", full, minimum)
control = group_input(group.nodes, {"Thickness"})
thickness = _math(group, "MULTIPLY", positive, control.outputs["Thickness"])
return {
"profile": profile, "depth": axes.outputs["Z"], "interior": interior,
"profile": profile, "depth": axes.outputs["Y"], "interior": interior,
"center": center, "thickness": thickness,
}
@@ -79,9 +78,9 @@ def build_shell_fields(group, geometry, fields, is_shell, normal_weight=1):
axes = nodes.new("ShaderNodeSeparateXYZ")
links.new(position.outputs[0], axes.inputs[0])
center_position = nodes.new("ShaderNodeCombineXYZ")
for axis in ("X", "Y"):
for axis in ("X", "Z"):
links.new(axes.outputs[axis], center_position.inputs[axis])
links.new(fields["center"], center_position.inputs["Z"])
links.new(fields["center"], center_position.inputs["Y"])
surface = nodes.new("GeometryNodeSetPosition")
links.new(geometry, surface.inputs["Geometry"])
links.new(center_position.outputs[0], surface.inputs["Position"])
@@ -214,15 +213,15 @@ def build_shell_position(group, position, front, is_shell, fields):
connect(b, result.inputs[3 if operation == "SCALE" else 1])
return result.outputs[0]
def z(value):
def y(value):
result = group.nodes.new("ShaderNodeSeparateXYZ")
links.new(value, result.inputs[0])
return result.outputs["Z"]
return result.outputs["Y"]
# Shell and the thin end of Balloon share the actual front normal.
front_z = z(front)
raw_z = z(position)
distance = _math(group, "SUBTRACT", raw_z, front_z)
front_y = y(front)
raw_y = y(position)
distance = _math(group, "SUBTRACT", raw_y, front_y)
normal = fields["front_normal"]
thin_offset = vector("SCALE", normal, distance)
shell_position = vector("ADD", front, thin_offset)
@@ -231,13 +230,13 @@ def build_shell_position(group, position, front, is_shell, fields):
clamp = group.nodes.new("ShaderNodeClamp")
links.new(control.outputs["Thickness"], clamp.inputs["Value"])
t = clamp.outputs[0]
radius = _math(group, "MULTIPLY", _math(group, "SUBTRACT", front_z, fields["center"]), t)
radius = _math(group, "MULTIPLY", _math(group, "SUBTRACT", front_y, fields["center"]), t)
rear = read_float_attribute(group, NORMAL_REDUCTION_ATTRIBUTE_NAME)
rear_radius = _math(group, "MULTIPLY", rear, _math(group, "ADD", radius, distance))
direction = vector("SUBTRACT", fields["normal"], (0, 0, 1))
direction = vector("SUBTRACT", fields["normal"], (0, 1, 0))
rear_shift = vector("SCALE", direction, rear_radius)
axis_offset = group.nodes.new("ShaderNodeCombineXYZ")
links.new(distance, axis_offset.inputs["Z"])
links.new(distance, axis_offset.inputs["Y"])
full_offset = vector("ADD", axis_offset.outputs[0], rear_shift)
# Smoothly reach the existing full-volume shape at Thickness >= 1.
@@ -348,7 +347,6 @@ def build_image_depth_cutout_group():
float_input(group, 'Cleanup Threshold', 0.0, parent=options)
float_input(group, 'Smooth Weight', 1.0, maximum=1, subtype='FACTOR', parent=options).description = 'Control the strength of boundary smoothing. Higher values move boundary vertices more.'
float_input(group, 'Reference Depth', 1.0, subtype='DISTANCE', parent=options)
interface_socket(group, 'Depth Axis', 'INPUT', 'NodeSocketMenu', options).menu_expanded = True
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.description = 'Smooth geometry near the original outline and Depth Split edges. Zero disables boundary smoothing.'
@@ -368,21 +366,13 @@ def build_image_depth_cutout_group():
cleaned = store_float_attribute(group, cleaned, NORMAL_REDUCTION_ATTRIBUTE_NAME, 0.0, domain='FACE')
controls = group_input(group.nodes, set())
geometry = _build_depth_surface(group, cleaned, controls)
orient = group.nodes.new('GeometryNodeTransform')
orient.inputs['Rotation'].default_value = Matrix(CUTOUT_Z_TO_X).to_euler()
group.links.new(geometry, orient.inputs['Geometry'])
axis = menu_switch(group, 'Depth Axis', ('-Z', '+X'), 'GEOMETRY')
group.links.new(geometry, axis.inputs['-Z'])
group.links.new(orient.outputs[0], axis.inputs['+X'])
_shade_output(group, axis.outputs[0])
_shade_output(group, geometry)
prepare_node_group(group)
for item in group.interface.items_tree:
if item.item_type != 'SOCKET' or item.in_out != 'INPUT':
continue
if item.name == 'Mode':
item.default_value = 'Balloon'
elif item.name == 'Depth Axis':
item.default_value = '-Z'
elif item.name == 'Shell Thickness':
item.name = 'Thickness'
return group

View File

@@ -1,6 +1,7 @@
"""Build the Image Plane Geometry Nodes group."""
import bpy
from mathutils import Matrix
from ..common.nodes import prepare_node_group
from ..common.material_slots import inherit_material_slots
@@ -94,7 +95,7 @@ def build_image_plane_group():
links.new(subdivide_input.outputs["Subdivide"], short_segments.inputs[1])
vertex_counts = []
for axis, other_axis in (("X", "Y"), ("Y", "X")):
for axis, other_axis in (("X", "Z"), ("Z", "X")):
ratio = nodes.new("ShaderNodeMath")
ratio.operation = "DIVIDE"
links.new(separate_dimensions.outputs[axis], ratio.inputs[0])
@@ -118,18 +119,18 @@ def build_image_plane_group():
grid = nodes.new("GeometryNodeMeshGrid")
links.new(separate_dimensions.outputs["X"], grid.inputs["Size X"])
links.new(separate_dimensions.outputs["Y"], grid.inputs["Size Y"])
links.new(separate_dimensions.outputs["Z"], grid.inputs["Size Y"])
links.new(vertex_counts[0], grid.inputs["Vertices X"])
links.new(vertex_counts[1], grid.inputs["Vertices Y"])
position = nodes.new("GeometryNodeInputPosition")
separate_position = nodes.new("ShaderNodeSeparateXYZ")
links.new(position.outputs["Position"], separate_position.inputs[0])
uv_components = []
for axis in ("X", "Y"):
for position_axis, dimension_axis in (("X", "X"), ("Y", "Z")):
normalize = nodes.new("ShaderNodeMath")
normalize.operation = "DIVIDE"
links.new(separate_position.outputs[axis], normalize.inputs[0])
links.new(separate_dimensions.outputs[axis], normalize.inputs[1])
links.new(separate_position.outputs[position_axis], normalize.inputs[0])
links.new(separate_dimensions.outputs[dimension_axis], normalize.inputs[1])
offset_uv = nodes.new("ShaderNodeMath")
offset_uv.operation = "ADD"
offset_uv.inputs[1].default_value = 0.5
@@ -139,6 +140,21 @@ def build_image_plane_group():
links.new(uv_components[0], uv_coordinates.inputs["X"])
links.new(uv_components[1], uv_coordinates.inputs["Y"])
cube_uv_components = []
for position_axis, dimension_axis in (("X", "X"), ("Z", "Z")):
normalize = nodes.new("ShaderNodeMath")
normalize.operation = "DIVIDE"
links.new(separate_position.outputs[position_axis], normalize.inputs[0])
links.new(separate_dimensions.outputs[dimension_axis], normalize.inputs[1])
offset_uv = nodes.new("ShaderNodeMath")
offset_uv.operation = "ADD"
offset_uv.inputs[1].default_value = 0.5
links.new(normalize.outputs[0], offset_uv.inputs[0])
cube_uv_components.append(offset_uv.outputs[0])
cube_uv_coordinates = nodes.new("ShaderNodeCombineXYZ")
links.new(cube_uv_components[0], cube_uv_coordinates.inputs["X"])
links.new(cube_uv_components[1], cube_uv_coordinates.inputs["Y"])
store_grid_uv = nodes.new("GeometryNodeStoreNamedAttribute")
store_grid_uv.data_type = "FLOAT2"
store_grid_uv.domain = "CORNER"
@@ -146,6 +162,10 @@ def build_image_plane_group():
links.new(grid.outputs["Mesh"], store_grid_uv.inputs["Geometry"])
links.new(uv_coordinates.outputs["Vector"], store_grid_uv.inputs["Value"])
rotate_grid = nodes.new("GeometryNodeTransform")
rotate_grid.inputs["Rotation"].default_value = Matrix.Rotation(1.5707963267948966, 4, "X").to_euler()
links.new(store_grid_uv.outputs["Geometry"], rotate_grid.inputs["Geometry"])
center_sum = nodes.new("ShaderNodeVectorMath")
center_sum.operation = "ADD"
links.new(bounds.outputs["Min"], center_sum.inputs[0])
@@ -155,11 +175,11 @@ def build_image_plane_group():
center.inputs[3].default_value = 0.5
links.new(center_sum.outputs["Vector"], center.inputs[0])
place_grid = nodes.new("GeometryNodeTransform")
links.new(store_grid_uv.outputs["Geometry"], place_grid.inputs["Geometry"])
links.new(rotate_grid.outputs["Geometry"], place_grid.inputs["Geometry"])
links.new(center.outputs["Vector"], place_grid.inputs["Translation"])
cell_sizes = []
for index, axis in enumerate(("X", "Y")):
for index, axis in enumerate(("X", "Z")):
segments = nodes.new("ShaderNodeMath")
segments.operation = "SUBTRACT"
segments.inputs[1].default_value = 1.0
@@ -194,19 +214,19 @@ def build_image_plane_group():
cube_size = nodes.new("ShaderNodeCombineXYZ")
links.new(separate_dimensions.outputs["X"], cube_size.inputs["X"])
links.new(separate_dimensions.outputs["Y"], cube_size.inputs["Y"])
links.new(thickness_input.outputs["Thickness"], cube_size.inputs["Z"])
links.new(thickness_input.outputs["Thickness"], cube_size.inputs["Y"])
links.new(separate_dimensions.outputs["Z"], cube_size.inputs["Z"])
cube = nodes.new("GeometryNodeMeshCube")
links.new(cube_size.outputs["Vector"], cube.inputs["Size"])
links.new(vertex_counts[0], cube.inputs["Vertices X"])
links.new(vertex_counts[1], cube.inputs["Vertices Y"])
links.new(depth_vertices.outputs[0], cube.inputs["Vertices Z"])
links.new(depth_vertices.outputs[0], cube.inputs["Vertices Y"])
links.new(vertex_counts[1], cube.inputs["Vertices Z"])
store_cube_uv = nodes.new("GeometryNodeStoreNamedAttribute")
store_cube_uv.data_type = "FLOAT2"
store_cube_uv.domain = "CORNER"
store_cube_uv.inputs["Name"].default_value = "UVMap"
links.new(cube.outputs["Mesh"], store_cube_uv.inputs["Geometry"])
links.new(uv_coordinates.outputs["Vector"], store_cube_uv.inputs["Value"])
links.new(cube_uv_coordinates.outputs["Vector"], store_cube_uv.inputs["Value"])
place_cube = nodes.new("GeometryNodeTransform")
links.new(store_cube_uv.outputs["Geometry"], place_cube.inputs["Geometry"])
links.new(center.outputs["Vector"], place_cube.inputs["Translation"])

View File

@@ -7,6 +7,7 @@ from ..common.nodes import group_input as _group_input, interface_socket as _int
from ..common.nodes import store_float_attribute
from ..common.nodes import prepare_node_group, float_input, vector_input
from ..common.reference_plane import reference_plane_depth
from ..common.boundary_smoothing import boundary_influence
RELIEF_PLANE_NODE_GROUP_NAME = "O Image Relief Plane"
@@ -35,7 +36,7 @@ def build_image_relief_plane_group(image_plane_group):
subdivide.min_value = 0
subdivide.max_value = 10
thickness = _interface_socket(group, "Thickness", "INPUT", "NodeSocketFloat")
thickness.default_value = 0.5
thickness.default_value = 0.0
thickness.min_value = 0.0
thickness.subtype = "DISTANCE"
thickness.description = "Set the thickness of the flat base beneath the relief."
@@ -50,6 +51,9 @@ def build_image_relief_plane_group(image_plane_group):
depth_scale.default_value = 0.5
depth_scale.min_value = 0.0
depth_scale.max_value = 2.0
valid_only = _interface_socket(group, "Depth Mask", "INPUT", "NodeSocketBool")
valid_only.description = "Treat depth-mask-invalid areas as zero depth instead of deleting them."
valid_only.default_value = True
options = group.interface.new_panel(name="Options", default_closed=True)
base_plane_depth = _interface_socket(
group,
@@ -61,6 +65,11 @@ def build_image_relief_plane_group(image_plane_group):
base_plane_depth.default_value = 1.0
base_plane_depth.min_value = 0.0
base_plane_depth.subtype = "DISTANCE"
validity_threshold = _interface_socket(group, "Mask Threshold", "INPUT", "NodeSocketFloat", options)
validity_threshold.description = "Set the minimum depth mask value treated as valid."
validity_threshold.default_value = 0.9
validity_threshold.min_value, validity_threshold.max_value = 0.0, 1.0
validity_threshold.subtype = "FACTOR"
data = group.interface.new_panel(name="Data", default_closed=True)
uniform_scale = _interface_socket(
group,
@@ -94,7 +103,7 @@ def build_image_relief_plane_group(image_plane_group):
)
image_input = _group_input(
nodes,
{"Depth Image", "Uniform Scale"},
{"Depth Image", "Uniform Scale", "Depth Mask", "Mask Threshold"},
)
group_output = nodes.new("NodeGroupOutput")
image_plane = nodes.new("GeometryNodeGroup")
@@ -107,7 +116,7 @@ def build_image_relief_plane_group(image_plane_group):
positive_half.inputs[1].default_value = 0.5
links.new(shape_input.outputs["Thickness"], positive_half.inputs[0])
translation = nodes.new("ShaderNodeCombineXYZ")
links.new(positive_half.outputs[0], translation.inputs["Z"])
links.new(positive_half.outputs[0], translation.inputs["Y"])
place_block = nodes.new("GeometryNodeTransform")
links.new(image_plane.outputs["Geometry"], place_block.inputs["Geometry"])
links.new(translation.outputs["Vector"], place_block.inputs["Translation"])
@@ -122,6 +131,20 @@ def build_image_relief_plane_group(image_plane_group):
links.new(uv.outputs["Attribute"], texture.inputs["Vector"])
depth_components = nodes.new("ShaderNodeSeparateXYZ")
links.new(texture.outputs["Color"], depth_components.inputs["Vector"])
invalid = nodes.new("FunctionNodeCompare")
invalid.data_type, invalid.operation = "FLOAT", "LESS_THAN"
links.new(texture.outputs["Alpha"], invalid.inputs["A"])
links.new(image_input.outputs["Mask Threshold"], invalid.inputs["B"])
mask_enabled = image_input.outputs["Depth Mask"]
valid_mask = nodes.new("ShaderNodeMath")
valid_mask.operation = "SUBTRACT"
valid_mask.inputs[0].default_value = 1.0
links.new(invalid.outputs["Result"], valid_mask.inputs[1])
mask_switch = nodes.new("GeometryNodeSwitch")
mask_switch.input_type = "FLOAT"
mask_switch.inputs["False"].default_value = 1.0
links.new(mask_enabled, mask_switch.inputs["Switch"])
links.new(valid_mask.outputs[0], mask_switch.inputs["True"])
metric_depth = _math(nodes, "MULTIPLY")
links.new(depth_components.outputs["Z"], metric_depth.inputs[0])
links.new(image_input.outputs["Uniform Scale"], metric_depth.inputs[1])
@@ -133,24 +156,38 @@ def build_image_relief_plane_group(image_plane_group):
plane_depth = reference_plane_depth(
group, shape_input.outputs["Depth Direction"], corrected_reference.outputs[0],
)
links.new(plane_depth, relative_depth.inputs[0])
links.new(metric_depth.outputs[0], relative_depth.inputs[1])
links.new(metric_depth.outputs[0], relative_depth.inputs[0])
links.new(plane_depth, relative_depth.inputs[1])
full_offset = _math(nodes, "MULTIPLY")
links.new(relative_depth.outputs[0], full_offset.inputs[0])
links.new(shape_input.outputs["Depth Scale"], full_offset.inputs[1])
masked_full_offset = _math(nodes, "MULTIPLY")
links.new(full_offset.outputs[0], masked_full_offset.inputs[0])
links.new(mask_switch.outputs[0], masked_full_offset.inputs[1])
has_thickness = _math(nodes, "GREATER_THAN")
has_thickness.inputs[1].default_value = 1e-6
links.new(shape_input.outputs["Thickness"], has_thickness.inputs[0])
safe_offset = _math(nodes, "MAXIMUM")
safe_offset = _math(nodes, "MINIMUM")
safe_offset.inputs[1].default_value = 0.0
links.new(full_offset.outputs[0], safe_offset.inputs[0])
links.new(masked_full_offset.outputs[0], safe_offset.inputs[0])
edge_influence = boundary_influence(group, protected=False)
edge_clamped = nodes.new("ShaderNodeClamp")
edge_clamped.inputs["Min"].default_value = 0.0
edge_clamped.inputs["Max"].default_value = 1.0
links.new(edge_influence, edge_clamped.inputs["Value"])
geometry_weight = _math(nodes, "SUBTRACT")
geometry_weight.inputs[0].default_value = 1.0
links.new(edge_clamped.outputs[0], geometry_weight.inputs[1])
smooth_safe_offset = _math(nodes, "MULTIPLY")
links.new(safe_offset.outputs[0], smooth_safe_offset.inputs[0])
links.new(geometry_weight.outputs[0], smooth_safe_offset.inputs[1])
position = nodes.new("GeometryNodeInputPosition")
position_components = nodes.new("ShaderNodeSeparateXYZ")
links.new(position.outputs["Position"], position_components.inputs["Vector"])
layer_ratio = _math(nodes, "DIVIDE")
links.new(position_components.outputs["Z"], layer_ratio.inputs[0])
links.new(position_components.outputs["Y"], layer_ratio.inputs[0])
links.new(shape_input.outputs["Thickness"], layer_ratio.inputs[1])
nonnegative_ratio = _math(nodes, "MAXIMUM")
nonnegative_ratio.inputs[1].default_value = 0.0
@@ -158,17 +195,20 @@ def build_image_relief_plane_group(image_plane_group):
clamped_ratio = _math(nodes, "MINIMUM")
clamped_ratio.inputs[1].default_value = 1.0
links.new(nonnegative_ratio.outputs[0], clamped_ratio.inputs[0])
one_minus_ratio = _math(nodes, "SUBTRACT")
one_minus_ratio.inputs[0].default_value = 1.0
links.new(clamped_ratio.outputs[0], one_minus_ratio.inputs[1])
layer_weight = nodes.new("GeometryNodeSwitch")
layer_weight.input_type = "FLOAT"
layer_weight.inputs["False"].default_value = 1.0
links.new(has_thickness.outputs[0], layer_weight.inputs["Switch"])
links.new(clamped_ratio.outputs[0], layer_weight.inputs["True"])
links.new(one_minus_ratio.outputs[0], layer_weight.inputs["True"])
weighted_offset = _math(nodes, "MULTIPLY")
links.new(safe_offset.outputs[0], weighted_offset.inputs[0])
links.new(smooth_safe_offset.outputs[0], weighted_offset.inputs[0])
links.new(layer_weight.outputs["Output"], weighted_offset.inputs[1])
displacement = nodes.new("ShaderNodeCombineXYZ")
links.new(weighted_offset.outputs[0], displacement.inputs["Z"])
links.new(weighted_offset.outputs[0], displacement.inputs["Y"])
set_position = nodes.new("GeometryNodeSetPosition")
links.new(place_block.outputs["Geometry"], set_position.inputs["Geometry"])
links.new(displacement.outputs["Vector"], set_position.inputs["Offset"])
@@ -178,7 +218,10 @@ def build_image_relief_plane_group(image_plane_group):
links.new(set_position.outputs["Geometry"], shade_smooth.inputs["Geometry"])
normal_reduction = _math(nodes, "SUBTRACT")
normal_reduction.inputs[0].default_value = 1.0
links.new(shape_input.outputs["Depth Scale"], normal_reduction.inputs[1])
scaled_depth = _math(nodes, "MULTIPLY")
links.new(shape_input.outputs["Depth Scale"], scaled_depth.inputs[0])
links.new(geometry_weight.outputs[0], scaled_depth.inputs[1])
links.new(scaled_depth.outputs[0], normal_reduction.inputs[1])
output_geometry = store_float_attribute(
group, shade_smooth.outputs["Geometry"], NORMAL_REDUCTION_ATTRIBUTE_NAME,
normal_reduction.outputs[0],