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

@@ -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])