- 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.
163 lines
6.5 KiB
Python
163 lines
6.5 KiB
Python
"""Build geometry calculations shared by Cutout assets."""
|
|
from .nodes import math_node
|
|
from anyimage.operators.cutout_tool.shape import BALLOON_ATTRIBUTE_NAME
|
|
|
|
|
|
def _island_cleanup(group, geometry, cleanup):
|
|
nodes = group.nodes
|
|
links = group.links
|
|
mesh_island = nodes.new("GeometryNodeInputMeshIsland")
|
|
face_area = nodes.new("GeometryNodeInputMeshFaceArea")
|
|
island_area = nodes.new("GeometryNodeAccumulateField")
|
|
island_area.data_type = "FLOAT"
|
|
island_area.domain = "FACE"
|
|
links.new(face_area.outputs["Area"], island_area.inputs["Value"])
|
|
links.new(mesh_island.outputs["Island Index"], island_area.inputs["Group ID"])
|
|
|
|
tiny = math_node(nodes, "LESS_THAN")
|
|
links.new(island_area.outputs["Total"], tiny.inputs[0])
|
|
links.new(cleanup, tiny.inputs[1])
|
|
delete = nodes.new("GeometryNodeDeleteGeometry")
|
|
delete.domain = "FACE"
|
|
links.new(geometry, delete.inputs["Geometry"])
|
|
links.new(tiny.outputs[0], delete.inputs["Selection"])
|
|
return delete.outputs["Geometry"]
|
|
|
|
|
|
def _profile_field(group, geometry):
|
|
nodes = group.nodes
|
|
links = group.links
|
|
profile = nodes.new("GeometryNodeInputNamedAttribute")
|
|
profile.data_type = "FLOAT"
|
|
profile.inputs["Name"].default_value = BALLOON_ATTRIBUTE_NAME
|
|
profile_max = nodes.new("GeometryNodeAttributeStatistic")
|
|
profile_max.data_type = "FLOAT"
|
|
profile_max.domain = "POINT"
|
|
links.new(geometry, profile_max.inputs["Geometry"])
|
|
links.new(profile.outputs["Attribute"], profile_max.inputs["Attribute"])
|
|
safe_max = math_node(nodes, "MAXIMUM")
|
|
safe_max.inputs[1].default_value = 1e-6
|
|
links.new(profile_max.outputs["Max"], safe_max.inputs[0])
|
|
normalized = math_node(nodes, "DIVIDE")
|
|
links.new(profile.outputs["Attribute"], normalized.inputs[0])
|
|
links.new(safe_max.outputs[0], normalized.inputs[1])
|
|
smooth = nodes.new("ShaderNodeMapRange")
|
|
smooth.data_type = "FLOAT"
|
|
smooth.interpolation_type = "SMOOTHERSTEP"
|
|
smooth.clamp = True
|
|
smooth.inputs["From Min"].default_value = 0.0
|
|
smooth.inputs["From Max"].default_value = 1.0
|
|
smooth.inputs["To Min"].default_value = 0.0
|
|
smooth.inputs["To Max"].default_value = 1.0
|
|
links.new(normalized.outputs[0], smooth.inputs["Value"])
|
|
return profile.outputs["Attribute"], smooth.outputs["Result"]
|
|
|
|
|
|
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, 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_y(group, geometry, y_value):
|
|
nodes = group.nodes
|
|
links = group.links
|
|
offset = nodes.new("ShaderNodeCombineXYZ")
|
|
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_y(group, geometry, extrude, front_y, back_y):
|
|
nodes = group.nodes
|
|
links = group.links
|
|
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(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"])
|
|
|
|
side = nodes.new("GeometryNodeSeparateGeometry")
|
|
side.domain = "FACE"
|
|
links.new(set_position.outputs["Geometry"], side.inputs["Geometry"])
|
|
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_y(
|
|
group, geometry, front_y
|
|
)
|
|
back = _offset_geometry_y(
|
|
group, geometry, back_y
|
|
)
|
|
flip_back = nodes.new("GeometryNodeFlipFaces")
|
|
links.new(back, flip_back.inputs["Mesh"])
|
|
|
|
join = nodes.new("GeometryNodeJoinGeometry")
|
|
links.new(front, join.inputs["Geometry"])
|
|
links.new(flip_back.outputs["Mesh"], join.inputs["Geometry"])
|
|
links.new(flip_side.outputs["Mesh"], join.inputs["Geometry"])
|
|
merge = nodes.new("GeometryNodeMergeByDistance")
|
|
merge.mode = "ALL"
|
|
merge.inputs["Distance"].default_value = 1e-6
|
|
links.new(join.outputs["Geometry"], merge.inputs["Geometry"])
|
|
return merge.outputs["Geometry"]
|
|
|
|
|
|
def _remove_zero_thickness(
|
|
group,
|
|
zero_geometry,
|
|
shaped_geometry,
|
|
thickness,
|
|
):
|
|
nodes = group.nodes
|
|
links = group.links
|
|
zero = math_node(nodes, "LESS_THAN")
|
|
zero.inputs[1].default_value = 1e-6
|
|
links.new(thickness, zero.inputs[0])
|
|
choose = nodes.new("GeometryNodeSwitch")
|
|
choose.input_type = "GEOMETRY"
|
|
links.new(zero.outputs[0], choose.inputs["Switch"])
|
|
links.new(shaped_geometry, choose.inputs["False"])
|
|
links.new(zero_geometry, choose.inputs["True"])
|
|
return choose.outputs["Output"]
|
|
|
|
|
|
def _position_field(group, depth_image):
|
|
nodes = group.nodes
|
|
links = group.links
|
|
uv = nodes.new("GeometryNodeInputNamedAttribute")
|
|
uv.data_type = "FLOAT_VECTOR"
|
|
uv.inputs["Name"].default_value = "UVMap"
|
|
position_texture = nodes.new("GeometryNodeImageTexture")
|
|
position_texture.interpolation = "Linear"
|
|
position_texture.extension = "EXTEND"
|
|
links.new(depth_image, position_texture.inputs["Image"])
|
|
links.new(uv.outputs["Attribute"], position_texture.inputs["Vector"])
|
|
return position_texture.outputs["Color"]
|
|
|
|
|
|
def _shade_output(group, geometry):
|
|
shade = group.nodes.new("GeometryNodeSetShadeSmooth")
|
|
shade.domain = "FACE"
|
|
shade.inputs["Shade Smooth"].default_value = True
|
|
output = group.nodes.new("NodeGroupOutput")
|
|
group.links.new(geometry, shade.inputs["Geometry"])
|
|
group.links.new(shade.outputs["Geometry"], output.inputs["Geometry"])
|