from pathlib import Path import bpy GROUP_NAME = "O Refine DA3 Mesh" OUTPUT_PATH = ( Path(__file__).parents[1] / "src" / "snapform" / "assets" / "O_SnapForm.blend" ) def input_socket(node, name, fallback): value = node.inputs.get(name) return value if value is not None else node.inputs[fallback] def new_socket(interface, name, in_out, socket_type, parent=None): return interface.new_socket( name=name, in_out=in_out, socket_type=socket_type, parent=parent, ) def new_compare(nodes, data_type, operation, location, value): node = nodes.new("FunctionNodeCompare") node.data_type = data_type node.operation = operation node.location = location input_socket(node, "B", 1).default_value = value return node def new_group_input(nodes, location, visible_names): node = nodes.new("NodeGroupInput") node.location = location visible = set(visible_names) for output in node.outputs: output.hide = output.name not in visible return node def add_edge_boundary_field(nodes, links, location): x, y = location vertex_neighbors = nodes.new("GeometryNodeInputMeshVertexNeighbors") vertex_neighbors.location = (x, y + 100.0) loose_vertex = new_compare( nodes, "INT", "EQUAL", (x + 180.0, y + 100.0), 1, ) links.new( vertex_neighbors.outputs["Vertex Count"], input_socket(loose_vertex, "A", 0), ) edge_neighbors = nodes.new("GeometryNodeInputMeshEdgeNeighbors") edge_neighbors.location = (x, y - 100.0) boundary_edge = new_compare( nodes, "INT", "EQUAL", (x + 180.0, y - 100.0), 1, ) links.new( edge_neighbors.outputs["Face Count"], input_socket(boundary_edge, "A", 0), ) edge_to_point = nodes.new("GeometryNodeFieldOnDomain") edge_to_point.data_type = "BOOLEAN" edge_to_point.domain = "EDGE" edge_to_point.location = (x + 370.0, y - 100.0) links.new(boundary_edge.outputs["Result"], edge_to_point.inputs["Value"]) combine = nodes.new("FunctionNodeBooleanMath") combine.operation = "OR" combine.location = (x + 560.0, y) links.new(loose_vertex.outputs["Result"], combine.inputs[0]) links.new(edge_to_point.outputs["Value"], combine.inputs[1]) return combine.outputs[0] def add_topology_fix_zone(group, geometry, iterations, location): nodes = group.nodes links = group.links x, y = location repeat_output = nodes.new("GeometryNodeRepeatOutput") repeat_output.location = (x + 1450.0, y) repeat_input = nodes.new("GeometryNodeRepeatInput") repeat_input.pair_with_output(repeat_output) repeat_input.location = (x, y) links.new(geometry, repeat_input.inputs["Geometry"]) links.new(iterations, repeat_input.inputs["Iterations"]) edge_neighbors = nodes.new("GeometryNodeInputMeshEdgeNeighbors") edge_neighbors.location = (x + 100.0, y + 420.0) non_manifold = new_compare( nodes, "INT", "GREATER_THAN", (x + 280.0, y + 500.0), 2, ) boundary = new_compare( nodes, "INT", "LESS_THAN", (x + 280.0, y + 320.0), 2, ) links.new( edge_neighbors.outputs["Face Count"], input_socket(non_manifold, "A", 0), ) links.new( edge_neighbors.outputs["Face Count"], input_socket(boundary, "A", 0), ) non_manifold_face = nodes.new("GeometryNodeFieldOnDomain") non_manifold_face.data_type = "BOOLEAN" non_manifold_face.domain = "FACE" non_manifold_face.location = (x + 480.0, y + 500.0) boundary_face = nodes.new("GeometryNodeFieldOnDomain") boundary_face.data_type = "BOOLEAN" boundary_face.domain = "FACE" boundary_face.location = (x + 480.0, y + 320.0) links.new(non_manifold.outputs["Result"], non_manifold_face.inputs["Value"]) links.new(boundary.outputs["Result"], boundary_face.inputs["Value"]) broken_face = nodes.new("FunctionNodeBooleanMath") broken_face.operation = "AND" broken_face.location = (x + 670.0, y + 420.0) links.new(non_manifold_face.outputs["Value"], broken_face.inputs[0]) links.new(boundary_face.outputs["Value"], broken_face.inputs[1]) remove_broken = nodes.new("GeometryNodeSeparateGeometry") remove_broken.domain = "FACE" remove_broken.location = (x + 670.0, y) links.new(repeat_input.outputs["Geometry"], remove_broken.inputs["Geometry"]) links.new(broken_face.outputs[0], remove_broken.inputs["Selection"]) remaining_neighbors = nodes.new("GeometryNodeInputMeshEdgeNeighbors") remaining_neighbors.location = (x + 650.0, y - 420.0) remaining_boundary = new_compare( nodes, "INT", "LESS_THAN", (x + 830.0, y - 420.0), 2, ) links.new( remaining_neighbors.outputs["Face Count"], input_socket(remaining_boundary, "A", 0), ) boundary_face_2 = nodes.new("GeometryNodeFieldOnDomain") boundary_face_2.data_type = "BOOLEAN" boundary_face_2.domain = "FACE" boundary_face_2.location = (x + 1010.0, y - 420.0) boundary_edge_2 = nodes.new("GeometryNodeFieldOnDomain") boundary_edge_2.data_type = "BOOLEAN" boundary_edge_2.domain = "EDGE" boundary_edge_2.location = (x + 1190.0, y - 420.0) links.new(remaining_boundary.outputs["Result"], boundary_face_2.inputs["Value"]) links.new(boundary_face_2.outputs["Value"], boundary_edge_2.inputs["Value"]) face_groups = nodes.new("GeometryNodeEdgesToFaceGroups") face_groups.location = (x + 1010.0, y - 220.0) links.new( boundary_edge_2.outputs["Value"], face_groups.inputs["Boundary Edges"], ) accumulate = nodes.new("GeometryNodeAccumulateField") accumulate.data_type = "INT" accumulate.domain = "FACE" accumulate.location = (x + 1190.0, y - 160.0) links.new(remaining_neighbors.outputs["Face Count"], accumulate.inputs["Value"]) links.new(face_groups.outputs["Face Group ID"], accumulate.inputs["Group ID"]) single_face = new_compare( nodes, "INT", "LESS_THAN", (x + 1370.0, y - 160.0), 2, ) links.new(accumulate.outputs["Total"], input_socket(single_face, "A", 0)) remove_single = nodes.new("GeometryNodeSeparateGeometry") remove_single.domain = "FACE" remove_single.location = (x + 1250.0, y + 80.0) links.new(remove_broken.outputs["Inverted"], remove_single.inputs["Geometry"]) links.new(single_face.outputs["Result"], remove_single.inputs["Selection"]) links.new(remove_single.outputs["Inverted"], repeat_output.inputs["Geometry"]) return repeat_output.outputs["Geometry"] def add_smooth_step(group, geometry, amount, location): nodes = group.nodes links = group.links x, y = location position = nodes.new("GeometryNodeInputPosition") position.location = (x + 100.0, y + 500.0) smooth_all = nodes.new("GeometryNodeBlurAttribute") smooth_all.data_type = "FLOAT_VECTOR" smooth_all.location = (x + 300.0, y + 500.0) smooth_all.inputs["Iterations"].default_value = 1 smooth_all.inputs["Weight"].default_value = 1.0 links.new(position.outputs["Position"], smooth_all.inputs["Value"]) boundary_field = add_edge_boundary_field( nodes, links, (x + 100.0, y - 350.0), ) boundary_on_points = nodes.new("GeometryNodeFieldOnDomain") boundary_on_points.data_type = "BOOLEAN" boundary_on_points.domain = "POINT" boundary_on_points.location = (x + 700.0, y - 350.0) links.new(boundary_field, boundary_on_points.inputs["Value"]) boundary_geometry = nodes.new("GeometryNodeSeparateGeometry") boundary_geometry.domain = "POINT" boundary_geometry.location = (x + 880.0, y - 220.0) links.new(geometry, boundary_geometry.inputs["Geometry"]) links.new(boundary_on_points.outputs["Value"], boundary_geometry.inputs["Selection"]) smooth_boundary = nodes.new("GeometryNodeBlurAttribute") smooth_boundary.data_type = "FLOAT_VECTOR" smooth_boundary.location = (x + 500.0, y + 300.0) smooth_boundary.inputs["Iterations"].default_value = 1 smooth_boundary.inputs["Weight"].default_value = 0.5 links.new(position.outputs["Position"], smooth_boundary.inputs["Value"]) nearest_boundary = nodes.new("GeometryNodeSampleNearest") nearest_boundary.domain = "POINT" nearest_boundary.location = (x + 1050.0, y - 120.0) links.new(boundary_geometry.outputs["Selection"], nearest_boundary.inputs["Geometry"]) sample_boundary = nodes.new("GeometryNodeSampleIndex") sample_boundary.data_type = "FLOAT_VECTOR" sample_boundary.domain = "POINT" sample_boundary.clamp = False sample_boundary.location = (x + 1220.0, y + 20.0) links.new(boundary_geometry.outputs["Selection"], sample_boundary.inputs["Geometry"]) links.new(nearest_boundary.outputs["Index"], sample_boundary.inputs["Index"]) links.new(smooth_boundary.outputs["Value"], sample_boundary.inputs["Value"]) boundary_switch = nodes.new("GeometryNodeSwitch") boundary_switch.input_type = "VECTOR" boundary_switch.location = (x + 1050.0, y + 350.0) links.new(boundary_on_points.outputs["Value"], boundary_switch.inputs["Switch"]) links.new(smooth_all.outputs["Value"], boundary_switch.inputs["False"]) links.new(sample_boundary.outputs["Value"], boundary_switch.inputs["True"]) normal = nodes.new("GeometryNodeInputNormal") normal.location = (x + 300.0, y + 100.0) blur_normal = nodes.new("GeometryNodeBlurAttribute") blur_normal.data_type = "FLOAT_VECTOR" blur_normal.location = (x + 500.0, y + 100.0) blur_normal.inputs["Iterations"].default_value = 10 links.new(normal.outputs["Normal"], blur_normal.inputs["Value"]) normal_length = nodes.new("ShaderNodeVectorMath") normal_length.operation = "LENGTH" normal_length.location = (x + 700.0, y + 100.0) links.new(blur_normal.outputs["Value"], normal_length.inputs[0]) weight = nodes.new("ShaderNodeMath") weight.operation = "MULTIPLY" weight.location = (x + 1050.0, y + 150.0) links.new(normal_length.outputs["Value"], weight.inputs[0]) links.new(amount, weight.inputs[1]) mix = nodes.new("ShaderNodeMix") mix.data_type = "VECTOR" mix.location = (x + 1270.0, y + 300.0) links.new(weight.outputs[0], input_socket(mix, "Factor", 0)) links.new(position.outputs["Position"], input_socket(mix, "A", 4)) links.new(boundary_switch.outputs["Output"], input_socket(mix, "B", 5)) set_position = nodes.new("GeometryNodeSetPosition") set_position.location = (x + 1450.0, y) links.new(geometry, set_position.inputs["Geometry"]) links.new(mix.outputs["Result"], set_position.inputs["Position"]) return set_position.outputs["Geometry"] def add_remesh_zone( group, geometry, resolution, iterations, fix_steps, smooth_amount, location, ): nodes = group.nodes links = group.links x, y = location repeat_output = nodes.new("GeometryNodeRepeatOutput") repeat_output.location = (x + 3300.0, y) repeat_input = nodes.new("GeometryNodeRepeatInput") repeat_input.pair_with_output(repeat_output) repeat_input.location = (x, y) links.new(geometry, repeat_input.inputs["Geometry"]) links.new(iterations, repeat_input.inputs["Iterations"]) half_resolution = nodes.new("ShaderNodeMath") half_resolution.operation = "MULTIPLY" half_resolution.inputs[1].default_value = 0.5 half_resolution.location = (x + 100.0, y - 250.0) links.new(resolution, half_resolution.inputs[0]) merge = nodes.new("GeometryNodeMergeByDistance") merge.mode = "CONNECTED" merge.location = (x + 300.0, y) links.new(repeat_input.outputs["Geometry"], merge.inputs["Geometry"]) links.new(half_resolution.outputs[0], merge.inputs["Distance"]) fixed = add_topology_fix_zone( group, merge.outputs["Geometry"], fix_steps, (x + 550.0, y), ) smoothed = add_smooth_step( group, fixed, smooth_amount, (x + 2100.0, y), ) links.new(smoothed, repeat_output.inputs["Geometry"]) return repeat_output.outputs["Geometry"] def add_uv_reprojection(group, source, geometry, location): nodes = group.nodes links = group.links x, y = location position = nodes.new("GeometryNodeInputPosition") position.location = (x, y - 200.0) source_uv = nodes.new("GeometryNodeInputNamedAttribute") source_uv.data_type = "FLOAT_VECTOR" source_uv.inputs["Name"].default_value = "UVMap" source_uv.location = (x, y) sample_uv = nodes.new("GeometryNodeSampleNearestSurface") sample_uv.data_type = "FLOAT_VECTOR" sample_uv.location = (x + 430.0, y) links.new(source, sample_uv.inputs["Mesh"]) links.new(source_uv.outputs["Attribute"], sample_uv.inputs["Value"]) links.new(position.outputs["Position"], sample_uv.inputs["Sample Position"]) store_uv = nodes.new("GeometryNodeStoreNamedAttribute") store_uv.data_type = "FLOAT2" store_uv.domain = "CORNER" store_uv.location = (x + 1050.0, y + 250.0) store_uv.inputs["Name"].default_value = "UVMap" links.new(geometry, store_uv.inputs["Geometry"]) links.new(sample_uv.outputs["Value"], store_uv.inputs["Value"]) return store_uv.outputs["Geometry"] def build_group(): group = bpy.data.node_groups.new(GROUP_NAME, "GeometryNodeTree") group.is_modifier = True interface = group.interface new_socket(interface, "Geometry", "INPUT", "NodeSocketGeometry") resolution = new_socket( interface, "Resolution", "INPUT", "NodeSocketFloat", ) resolution.default_value = 0.01 resolution.min_value = 0.000001 resolution.subtype = "DISTANCE" remesh_iterations = new_socket( interface, "Relax Iterations", "INPUT", "NodeSocketInt", ) remesh_iterations.default_value = 2 remesh_iterations.min_value = 1 remesh_iterations.max_value = 8 smooth_amount = new_socket( interface, "Smooth", "INPUT", "NodeSocketFloat", ) smooth_amount.default_value = 1.0 smooth_amount.min_value = 0.0 smooth_amount.max_value = 1.0 smooth_amount.subtype = "FACTOR" culling_panel = interface.new_panel( name="Culling", default_closed=False, ) confidence = new_socket( interface, "Confidence", "INPUT", "NodeSocketFloat", parent=culling_panel, ) confidence.default_value = 0.0 confidence.min_value = 0.0 confidence.max_value = 1.0 confidence.subtype = "PERCENTAGE" edge = new_socket( interface, "Stretch", "INPUT", "NodeSocketFloat", parent=culling_panel, ) edge.default_value = 0.1 edge.min_value = 0.0 edge.max_value = 1.0 edge.subtype = "PERCENTAGE" background = new_socket( interface, "Background", "INPUT", "NodeSocketBool", parent=culling_panel, ) background.default_value = True options_panel = interface.new_panel( name="Options", default_closed=True, ) fix_steps = new_socket( interface, "Fix Topology", "INPUT", "NodeSocketInt", parent=options_panel, ) fix_steps.default_value = 3 fix_steps.min_value = 1 fix_steps.max_value = 8 new_socket(interface, "Geometry", "OUTPUT", "NodeSocketGeometry") nodes = group.nodes links = group.links geometry_input = new_group_input(nodes, (-650.0, 50.0), {"Geometry"}) confidence_input = new_group_input(nodes, (-1800.0, 800.0), {"Confidence"}) stretch_input = new_group_input(nodes, (-1800.0, 50.0), {"Stretch"}) background_input = new_group_input(nodes, (-1800.0, -350.0), {"Background"}) remesh_input = new_group_input( nodes, (-100.0, -500.0), {"Resolution", "Relax Iterations", "Smooth"}, ) topology_input = new_group_input(nodes, (350.0, -700.0), {"Fix Topology"}) island_input = new_group_input(nodes, (3400.0, 100.0), {"Resolution"}) uv_source_input = new_group_input(nodes, (4200.0, -300.0), {"Geometry"}) group_output = nodes.new("NodeGroupOutput") group_output.location = (5650.0, 100.0) confidence_attribute = nodes.new("GeometryNodeInputNamedAttribute") confidence_attribute.data_type = "FLOAT" confidence_attribute.inputs["Name"].default_value = "o_face_confidence" confidence_attribute.location = (-1800.0, 600.0) confidence_compare = new_compare( nodes, "FLOAT", "LESS_THAN", (-1550.0, 600.0), 0.0, ) links.new( confidence_attribute.outputs["Attribute"], input_socket(confidence_compare, "A", 0), ) links.new( confidence_input.outputs["Confidence"], input_socket(confidence_compare, "B", 1), ) edge_attribute = nodes.new("GeometryNodeInputNamedAttribute") edge_attribute.data_type = "FLOAT" edge_attribute.inputs["Name"].default_value = "o_edge_stretch" edge_attribute.location = (-1800.0, 350.0) edge_maximum = nodes.new("ShaderNodeMath") edge_maximum.operation = "MAXIMUM" edge_maximum.inputs[1].default_value = 0.0001 edge_maximum.location = (-1600.0, 100.0) edge_reciprocal = nodes.new("ShaderNodeMath") edge_reciprocal.operation = "DIVIDE" edge_reciprocal.inputs[0].default_value = 1.0 edge_reciprocal.location = (-1400.0, 100.0) edge_compare = new_compare( nodes, "FLOAT", "GREATER_EQUAL", (-1200.0, 350.0), 1.0, ) links.new(stretch_input.outputs["Stretch"], edge_maximum.inputs[0]) links.new(edge_maximum.outputs[0], edge_reciprocal.inputs[1]) links.new(edge_attribute.outputs["Attribute"], input_socket(edge_compare, "A", 0)) links.new(edge_reciprocal.outputs[0], input_socket(edge_compare, "B", 1)) background_attribute = nodes.new("GeometryNodeInputNamedAttribute") background_attribute.data_type = "BOOLEAN" background_attribute.inputs["Name"].default_value = "o_background_face" background_attribute.location = (-1800.0, -200.0) background_and = nodes.new("FunctionNodeBooleanMath") background_and.operation = "AND" background_and.location = (-1450.0, -200.0) links.new(background_attribute.outputs["Attribute"], background_and.inputs[0]) links.new(background_input.outputs["Background"], background_and.inputs[1]) first_or = nodes.new("FunctionNodeBooleanMath") first_or.operation = "OR" first_or.location = (-950.0, 450.0) links.new(confidence_compare.outputs["Result"], first_or.inputs[0]) links.new(edge_compare.outputs["Result"], first_or.inputs[1]) final_or = nodes.new("FunctionNodeBooleanMath") final_or.operation = "OR" final_or.location = (-750.0, 300.0) links.new(first_or.outputs[0], final_or.inputs[0]) links.new(background_and.outputs[0], final_or.inputs[1]) full_stretch = new_compare( nodes, "FLOAT", "GREATER_EQUAL", (-950.0, -20.0), 0.999999, ) links.new( stretch_input.outputs["Stretch"], input_socket(full_stretch, "A", 0), ) culling_selection = nodes.new("FunctionNodeBooleanMath") culling_selection.operation = "OR" culling_selection.location = (-550.0, 300.0) links.new(final_or.outputs[0], culling_selection.inputs[0]) links.new(full_stretch.outputs["Result"], culling_selection.inputs[1]) delete_geometry = nodes.new("GeometryNodeDeleteGeometry") delete_geometry.domain = "FACE" delete_geometry.location = (-350.0, 100.0) links.new(geometry_input.outputs["Geometry"], delete_geometry.inputs["Geometry"]) links.new(culling_selection.outputs[0], delete_geometry.inputs["Selection"]) vertex_neighbors = nodes.new("GeometryNodeInputMeshVertexNeighbors") vertex_neighbors.location = (-550.0, -320.0) orphan_vertex = new_compare( nodes, "INT", "EQUAL", (-350.0, -320.0), 0, ) links.new( vertex_neighbors.outputs["Face Count"], input_socket(orphan_vertex, "A", 0), ) delete_orphans = nodes.new("GeometryNodeDeleteGeometry") delete_orphans.domain = "POINT" delete_orphans.location = (-300.0, 100.0) links.new(delete_geometry.outputs["Geometry"], delete_orphans.inputs["Geometry"]) links.new(orphan_vertex.outputs["Result"], delete_orphans.inputs["Selection"]) remeshed = add_remesh_zone( group, delete_orphans.outputs["Geometry"], remesh_input.outputs["Resolution"], remesh_input.outputs["Relax Iterations"], topology_input.outputs["Fix Topology"], remesh_input.outputs["Smooth"], (-300.0, 100.0), ) mesh_island = nodes.new("GeometryNodeInputMeshIsland") mesh_island.location = (3400.0, 700.0) face_area = nodes.new("GeometryNodeInputMeshFaceArea") face_area.location = (3400.0, 550.0) island_area = nodes.new("GeometryNodeAccumulateField") island_area.data_type = "FLOAT" island_area.domain = "FACE" island_area.location = (3600.0, 630.0) links.new(face_area.outputs["Area"], island_area.inputs["Value"]) links.new(mesh_island.outputs["Island Index"], island_area.inputs["Group ID"]) resolution_squared = nodes.new("ShaderNodeMath") resolution_squared.operation = "POWER" resolution_squared.inputs[1].default_value = 2.0 resolution_squared.location = (3600.0, 250.0) links.new(island_input.outputs["Resolution"], resolution_squared.inputs[0]) tiny_island = new_compare( nodes, "FLOAT", "LESS_THAN", (3800.0, 630.0), 0.0, ) links.new(island_area.outputs["Total"], input_socket(tiny_island, "A", 0)) links.new(resolution_squared.outputs[0], input_socket(tiny_island, "B", 1)) remove_tiny = nodes.new("GeometryNodeDeleteGeometry") remove_tiny.domain = "FACE" remove_tiny.location = (4000.0, 100.0) links.new(remeshed, remove_tiny.inputs["Geometry"]) links.new(tiny_island.outputs["Result"], remove_tiny.inputs["Selection"]) reprojected = add_uv_reprojection( group, uv_source_input.outputs["Geometry"], remove_tiny.outputs["Geometry"], (4100.0, 100.0), ) shade_smooth = nodes.new("GeometryNodeSetShadeSmooth") shade_smooth.domain = "FACE" shade_smooth.location = (5400.0, 100.0) shade_smooth.inputs["Shade Smooth"].default_value = True links.new(reprojected, shade_smooth.inputs["Geometry"]) links.new(shade_smooth.outputs["Geometry"], group_output.inputs["Geometry"]) group.color_tag = "GEOMETRY" group.description = ( "Cull DA3/BEN2 faces, repair merged topology, preserve boundaries, " "and reproject UVs." ) group.use_fake_user = True return group def main(): bpy.ops.wm.read_factory_settings(use_empty=True) bpy.context.preferences.filepaths.save_version = 0 build_group() OUTPUT_PATH.parent.mkdir(parents=True, exist_ok=True) bpy.ops.wm.save_as_mainfile(filepath=str(OUTPUT_PATH), check_existing=False) if __name__ == "__main__": main()