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:
@@ -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
|
||||
|
||||
@@ -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"])
|
||||
|
||||
@@ -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"])
|
||||
|
||||
33
tools/nodes/common/direction.py
Normal file
33
tools/nodes/common/direction.py
Normal 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"]
|
||||
@@ -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'
|
||||
|
||||
@@ -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])
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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"])
|
||||
|
||||
@@ -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],
|
||||
|
||||
Reference in New Issue
Block a user