"""Build O Image Depth Symmetry.""" 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, bool_input, vector_input, menu_switch, evaluate_field, read_float_attribute, store_float_attribute, prepare_node_group, ) from ..common.smoothing import expand_smooth from ..common.boundary_smoothing import smooth_cut_boundary from ..common.depth_surface import _math, project_depth_surface from ..common.reference_plane import reference_plane_depth from ..common.normal_map import ROTATION_ATTRIBUTE, FACE_ATTRIBUTE, AXIS_ATTRIBUTE from ..common.cutout import _extrude_layers, _island_cleanup, _position_field, _profile_field, _remove_zero_thickness, _set_layer_z NODE_GROUP_NAME = "O Image Depth Symmetry" def _set_geometry_z(group, geometry, z_value): nodes = group.nodes links = group.links 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(z_value, combine.inputs["Z"]) set_position = nodes.new("GeometryNodeSetPosition") links.new(geometry, set_position.inputs["Geometry"]) links.new(combine.outputs["Vector"], set_position.inputs["Position"]) return set_position.outputs["Geometry"] def _build_balloon( group, geometry, thickness, depth_scale, double_sided, uniform_scale, depth_direction, depth_image, depth_reference, depth_offset, ): nodes = group.nodes links = group.links profile, profile_factor = _profile_field(group, geometry) camera_position = _position_field(group, depth_image) separate_camera = nodes.new("ShaderNodeSeparateXYZ") links.new(camera_position, separate_camera.inputs["Vector"]) metric_depth = math_node(nodes, "MULTIPLY") links.new(separate_camera.outputs["Z"], metric_depth.inputs[0]) links.new(uniform_scale, metric_depth.inputs[1]) plane_depth = reference_plane_depth( group, depth_direction, depth_reference, ) corrected_depth = math_node(nodes, "SUBTRACT") links.new(plane_depth, corrected_depth.inputs[0]) links.new(metric_depth.outputs[0], corrected_depth.inputs[1]) nonnegative_depth = math_node(nodes, "MAXIMUM") nonnegative_depth.inputs[1].default_value = 0.0 links.new(corrected_depth.outputs[0], nonnegative_depth.inputs[0]) scaled_depth = math_node(nodes, "MULTIPLY") links.new(nonnegative_depth.outputs[0], scaled_depth.inputs[0]) links.new(depth_scale, scaled_depth.inputs[1]) front_depth = _math(group, 'MAXIMUM', _math(group, 'ADD', scaled_depth.outputs[0], depth_offset), 0.0) base_front = math_node(nodes, "MULTIPLY") links.new(profile, base_front.inputs[0]) links.new(thickness, base_front.inputs[1]) negate_back = math_node(nodes, "MULTIPLY") negate_back.inputs[1].default_value = -1.0 links.new(base_front.outputs[0], negate_back.inputs[0]) front_mix = nodes.new("ShaderNodeMix") front_mix.data_type = "FLOAT" front_mix.clamp_factor = True links.new(profile_factor, front_mix.inputs["Factor"]) links.new(base_front.outputs[0], front_mix.inputs["A"]) links.new(front_depth, front_mix.inputs["B"]) mirrored_front = math_node(nodes, "MULTIPLY") mirrored_front.inputs[1].default_value = -1.0 links.new(front_mix.outputs["Result"], mirrored_front.inputs[0]) back_choice = nodes.new("GeometryNodeSwitch") back_choice.input_type = "FLOAT" links.new(double_sided, back_choice.inputs["Switch"]) links.new(negate_back.outputs[0], back_choice.inputs["False"]) links.new(mirrored_front.outputs[0], back_choice.inputs["True"]) extrude = _extrude_layers(group, geometry) shaped = _set_layer_z( group, geometry, extrude, front_mix.outputs["Result"], back_choice.outputs["Output"], ) front_only = _set_geometry_z( group, geometry, front_mix.outputs["Result"] ) return _remove_zero_thickness(group, front_only, shaped, thickness) def _build_projected_front(group, geometry, controls): """Project camera depth and align its direction with the fixed XY plane.""" nodes, links = group.nodes, group.links camera = _position_field(group, controls['Depth Image']) projected = project_depth_surface( group, geometry, camera, controls['Uniform Scale'], controls['Reference Depth'], controls['Depth Scale'], 0.0, ) align = nodes.new('FunctionNodeAlignEulerToVector') align.axis = 'Z' links.new(controls['Depth Direction'], align.inputs['Vector']) position = nodes.new('GeometryNodeInputPosition') rotate = nodes.new('ShaderNodeVectorRotate') rotate.rotation_type = 'EULER_XYZ' rotate.invert = True links.new(position.outputs[0], rotate.inputs['Vector']) links.new(align.outputs['Rotation'], rotate.inputs['Rotation']) place = nodes.new('GeometryNodeSetPosition') links.new(projected, place.inputs['Geometry']) links.new(rotate.outputs[0], place.inputs['Position']) offset = nodes.new('ShaderNodeCombineXYZ') links.new(controls['Depth Offset'], offset.inputs['Z']) links.new(offset.outputs[0], place.inputs['Offset']) store = nodes.new('GeometryNodeStoreNamedAttribute') store.data_type, store.domain = 'FLOAT_VECTOR', 'FACE' store.inputs['Name'].default_value = ROTATION_ATTRIBUTE links.new(place.outputs[0], store.inputs['Geometry']) links.new(align.outputs['Rotation'], store.inputs['Value']) return store.outputs['Geometry'] def _delete_negative_faces(group, front, tolerance): """Discard faces touching negative Z 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') 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']) return delete.outputs[0] def _build_straight_volume(group, front, double_sided, fill_sides, tolerance): """Mirror the positive surface and join matching boundary vertices with straight walls.""" nodes, links = group.nodes, group.links shade = nodes.new('GeometryNodeSetShadeSmooth') shade.domain = 'FACE' links.new(front, shade.inputs['Geometry']) front = shade.outputs[0] front = store_float_attribute(group, front, FACE_ATTRIBUTE, 1., domain='FACE') count = nodes.new('GeometryNodeInputMeshEdgeNeighbors') boundary = _math(group, 'COMPARE', count.outputs['Face Count'], 1) extrude = nodes.new('GeometryNodeExtrudeMesh') extrude.mode = 'EDGES' extrude.inputs['Offset'].default_value = (0, 0, 0) extrude.inputs['Offset Scale'].default_value = 0 links.new(front, extrude.inputs['Mesh']) links.new(boundary, extrude.inputs['Selection']) position = nodes.new('GeometryNodeInputPosition') components = nodes.new('ShaderNodeSeparateXYZ') links.new(position.outputs[0], components.inputs[0]) mirror_point = nodes.new('ShaderNodeCombineXYZ') links.new(components.outputs['X'], mirror_point.inputs['X']) links.new(components.outputs['Y'], mirror_point.inputs['Y']) links.new(_math(group, 'MULTIPLY', components.outputs['Z'], -1), mirror_point.inputs['Z']) place = nodes.new('GeometryNodeSetPosition') links.new(extrude.outputs['Mesh'], place.inputs['Geometry']) links.new(extrude.outputs['Top'], place.inputs['Selection']) links.new(mirror_point.outputs[0], place.inputs['Position']) sides = nodes.new('GeometryNodeSeparateGeometry') sides.domain = 'FACE' links.new(place.outputs[0], sides.inputs['Geometry']) links.new(extrude.outputs['Side'], sides.inputs['Selection']) flat = nodes.new('GeometryNodeSetShadeSmooth') flat.domain = 'FACE' flat.inputs['Shade Smooth'].default_value = False links.new(sides.outputs['Selection'], flat.inputs['Geometry']) mirror = nodes.new('GeometryNodeTransform') mirror.inputs['Scale'].default_value = (1, 1, -1) links.new(front, mirror.inputs['Geometry']) flip = nodes.new('GeometryNodeFlipFaces') links.new(mirror.outputs[0], flip.inputs['Mesh']) join = nodes.new('GeometryNodeJoinGeometry') back = store_float_attribute(group, flip.outputs[0], FACE_ATTRIBUTE, 2., domain='FACE') walls = store_float_attribute(group, flat.outputs[0], FACE_ATTRIBUTE, 3., domain='FACE') wall_switch = nodes.new('GeometryNodeSwitch') wall_switch.input_type = 'GEOMETRY' links.new(fill_sides, wall_switch.inputs['Switch']) links.new(walls, wall_switch.inputs['True']) for geometry in (front, back, wall_switch.outputs[0]): links.new(geometry, join.inputs[0]) merge = nodes.new('GeometryNodeMergeByDistance') links.new(tolerance, merge.inputs['Distance']) links.new(join.outputs[0], merge.inputs[0]) switch = nodes.new('GeometryNodeSwitch') switch.input_type = 'GEOMETRY' links.new(double_sided, switch.inputs['Switch']) links.new(front, switch.inputs['False']) links.new(merge.outputs[0], switch.inputs['True']) return switch.outputs[0] def build_image_depth_symmetry_group(): name = NODE_GROUP_NAME existing = bpy.data.node_groups.get(name) if existing is not None: return existing group = bpy.data.node_groups.new(name, 'GeometryNodeTree') group.is_modifier, group.use_fake_user = (True, True) group.color_tag = 'GEOMETRY' interface_socket(group, 'Geometry', 'INPUT', 'NodeSocketGeometry') interface_socket(group, 'Mode', 'INPUT', 'NodeSocketMenu') float_input(group, 'Thickness', 1.0).description = 'Control the bulge in Balloon mode. Zero keeps only the front surface.' float_input(group, 'Depth Scale', 1.0) vector_input(group, 'Depth Direction', (0, 0, 1), subtype='DIRECTION') float_input(group, 'Depth Offset', 0.0, minimum=-1e10, subtype='DISTANCE').description = 'Offset the front depth along the symmetry axis after direction adjustment. Positive values move it away from the symmetry plane.' iterations = interface_socket(group, 'Smooth', 'INPUT', 'NodeSocketInt') iterations.description = 'Set the number of mesh smoothing passes. Higher values smooth the surface more but may soften details. Zero disables smoothing.' iterations.default_value = 0 iterations.min_value, iterations.max_value = (0, 20) options = group.interface.new_panel(name='Options', default_closed=True) bool_input(group, 'Double Sided', True, parent=options) bool_input(group, 'Fill Sides', True, parent=options).description = 'Close the gap between the front and mirrored back with straight walls in Projected mode.' 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 each mesh smoothing pass. Higher values move vertices more.' float_input(group, 'Reference Depth', 1.0, subtype='DISTANCE', parent=options).description = 'Calibrate the source depth before direction adjustment.' interface_socket(group, 'Depth Axis', 'INPUT', 'NodeSocketMenu', options).menu_expanded = True boundary = interface_socket(group, 'Boundary Smooth', 'INPUT', 'NodeSocketInt', options) boundary.default_value, boundary.min_value, boundary.max_value = 1, 0, 16 boundary.description = 'Smooth the projected outline before clipping and closing the surface.' data = group.interface.new_panel(name='Data', default_closed=True) float_input(group, 'Uniform Scale', 1.0, parent=data).hide_in_modifier = True interface_socket(group, 'Depth Image', 'INPUT', 'NodeSocketImage', data).hide_in_modifier = True interface_socket(group, 'Geometry', 'OUTPUT', 'NodeSocketGeometry') controls = group_input(group.nodes, set()) inputs = controls.outputs cleaned = _island_cleanup( group, inputs['Geometry'], _math(group, 'MULTIPLY', inputs['Cleanup Threshold'], 0.01), ) balloon = _build_balloon( group, cleaned, inputs['Thickness'], inputs['Depth Scale'], inputs['Double Sided'], inputs['Uniform Scale'], inputs['Depth Direction'], inputs['Depth Image'], inputs['Reference Depth'], inputs['Depth Offset'], ) balloon = expand_smooth(group, balloon, iterations_name='Smooth', weight_name='Smooth Weight') shade = group.nodes.new('GeometryNodeSetShadeSmooth') shade.domain = 'FACE' group.links.new(balloon, shade.inputs['Geometry']) tolerance = _math(group, 'MULTIPLY', _math(group, 'MAXIMUM', inputs['Uniform Scale'], 1e-8), 1e-7) front = smooth_cut_boundary(group, _build_projected_front(group, cleaned, inputs)) front = _delete_negative_faces(group, front, tolerance) front = expand_smooth(group, front, iterations_name='Smooth', weight_name='Smooth Weight') projected = _build_straight_volume(group, front, inputs['Double Sided'], inputs['Fill Sides'], tolerance) modes = menu_switch(group, 'Mode', ('Balloon', 'Projected'), 'GEOMETRY') group.links.new(shade.outputs[0], modes.inputs['Balloon']) group.links.new(projected, modes.inputs['Projected']) geometry = modes.outputs[0] profile = read_float_attribute(group, 'o_balloon') reduction = _math(group, 'SUBTRACT', 1.0, profile) geometry = store_float_attribute(group, geometry, NORMAL_REDUCTION_ATTRIBUTE_NAME, reduction) geometry = store_float_attribute(group, geometry, AXIS_ATTRIBUTE, 0., domain='FACE') 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']) oriented = store_float_attribute(group, orient.outputs[0], AXIS_ATTRIBUTE, 1., domain='FACE') group.links.new(oriented, axis.inputs['+X']) output = group.nodes.new('NodeGroupOutput') group.links.new(axis.outputs[0], output.inputs['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 = 'Projected' elif item.name == 'Depth Axis': item.default_value = '-Z' return group