Add geometry nodes asset and validation script
- Implemented a new geometry nodes asset in `build_geometry_nodes_asset.py` to handle mesh refinement and topology fixing. - Created a validation script in `validate_geometry_nodes_asset.py` to ensure the integrity and functionality of the geometry node group. - Added input handling for various parameters including resolution, iterations, and smoothing options. - Included checks for node group structure and attributes to validate the geometry processing pipeline.
This commit is contained in:
633
tools/build_geometry_nodes_asset.py
Normal file
633
tools/build_geometry_nodes_asset.py
Normal file
@@ -0,0 +1,633 @@
|
||||
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()
|
||||
188
tools/validate_geometry_nodes_asset.py
Normal file
188
tools/validate_geometry_nodes_asset.py
Normal file
@@ -0,0 +1,188 @@
|
||||
from pathlib import Path
|
||||
import sys
|
||||
|
||||
import bpy
|
||||
import numpy as np
|
||||
|
||||
|
||||
sys.path.insert(0, str(Path(__file__).parents[1] / "src"))
|
||||
|
||||
from snapform.image_mesh import create_matching_camera, load_geometry_node_group
|
||||
|
||||
|
||||
def set_input(modifier, group, name, value):
|
||||
item = next(
|
||||
item
|
||||
for item in group.interface.items_tree
|
||||
if item.item_type == "SOCKET"
|
||||
and item.in_out == "INPUT"
|
||||
and item.name == name
|
||||
)
|
||||
modifier[item.identifier] = value
|
||||
|
||||
|
||||
def add_face_attributes(mesh, count, background_values=None):
|
||||
for name in ("o_face_confidence", "o_edge_stretch"):
|
||||
attribute = mesh.attributes.new(name, "FLOAT", "FACE")
|
||||
attribute.data.foreach_set("value", [1.0] * count)
|
||||
background = mesh.attributes.new("o_background_face", "BOOLEAN", "FACE")
|
||||
background.data.foreach_set(
|
||||
"value",
|
||||
background_values or [False] * count,
|
||||
)
|
||||
|
||||
|
||||
def main():
|
||||
bpy.ops.wm.read_factory_settings(use_empty=True)
|
||||
group = load_geometry_node_group()
|
||||
assert load_geometry_node_group() == group
|
||||
assert group.is_modifier
|
||||
assert group.asset_data is None
|
||||
assert not any(node.bl_idname == "GeometryNodeGroup" for node in group.nodes)
|
||||
assert not any(node.bl_idname == "GeometryNodeTriangulate" for node in group.nodes)
|
||||
group_inputs = [
|
||||
node for node in group.nodes if node.bl_idname == "NodeGroupInput"
|
||||
]
|
||||
assert len(group_inputs) >= 8
|
||||
for node in group_inputs:
|
||||
visible_outputs = [
|
||||
output for output in node.outputs if output.name and not output.hide
|
||||
]
|
||||
assert 1 <= len(visible_outputs) <= 3
|
||||
uv_stores = [
|
||||
node
|
||||
for node in group.nodes
|
||||
if node.bl_idname == "GeometryNodeStoreNamedAttribute"
|
||||
and node.inputs["Name"].default_value == "UVMap"
|
||||
]
|
||||
assert len(uv_stores) == 1
|
||||
assert uv_stores[0].data_type == "FLOAT2"
|
||||
panels = {
|
||||
item.name: item
|
||||
for item in group.interface.items_tree
|
||||
if item.item_type == "PANEL"
|
||||
}
|
||||
assert not panels["Culling"].default_closed
|
||||
assert panels["Options"].default_closed
|
||||
assert [item.name for item in panels["Culling"].interface_items] == [
|
||||
"Confidence",
|
||||
"Stretch",
|
||||
"Background",
|
||||
]
|
||||
assert [item.name for item in panels["Options"].interface_items] == [
|
||||
"Fix Topology",
|
||||
]
|
||||
if bpy.app.version >= (5, 0, 0):
|
||||
assert group.library is not None
|
||||
|
||||
mesh = bpy.data.meshes.new("Validation Mesh")
|
||||
mesh.from_pydata(
|
||||
[(0, 0, 0), (1, 0, 0), (0, 1, 0), (1, 1, 0)],
|
||||
[],
|
||||
[(0, 2, 1), (1, 2, 3)],
|
||||
)
|
||||
uv = mesh.uv_layers.new(name="UVMap")
|
||||
uv.data.foreach_set("uv", [0, 0, 0, 1, 1, 0, 1, 0, 0, 1, 1, 1])
|
||||
add_face_attributes(mesh, 2, [False, True])
|
||||
|
||||
obj = bpy.data.objects.new("Validation Object", mesh)
|
||||
bpy.context.scene.collection.objects.link(obj)
|
||||
modifier = obj.modifiers.new(name=group.name, type="NODES")
|
||||
modifier.node_group = group
|
||||
modifier.show_group_selector = False
|
||||
assert not modifier.show_group_selector
|
||||
set_input(modifier, group, "Resolution", 0.000001)
|
||||
set_input(modifier, group, "Relax Iterations", 1)
|
||||
set_input(modifier, group, "Fix Topology", 0)
|
||||
set_input(modifier, group, "Smooth", 0.0)
|
||||
evaluated = obj.evaluated_get(bpy.context.evaluated_depsgraph_get())
|
||||
result = evaluated.to_mesh()
|
||||
try:
|
||||
assert len(result.polygons) == 1
|
||||
assert "UVMap" in result.attributes
|
||||
assert result.attributes["UVMap"].data_type == "FLOAT2"
|
||||
finally:
|
||||
evaluated.to_mesh_clear()
|
||||
|
||||
set_input(modifier, group, "Stretch", 1.0)
|
||||
stretch_item = next(
|
||||
item
|
||||
for item in group.interface.items_tree
|
||||
if item.item_type == "SOCKET" and item.name == "Stretch"
|
||||
)
|
||||
assert modifier[stretch_item.identifier] == 1.0
|
||||
obj.update_tag(refresh={"DATA"})
|
||||
bpy.context.view_layer.update()
|
||||
culled = obj.evaluated_get(bpy.context.evaluated_depsgraph_get())
|
||||
culled_result = culled.to_mesh()
|
||||
try:
|
||||
assert len(culled_result.polygons) == 0
|
||||
assert len(culled_result.vertices) == 0
|
||||
finally:
|
||||
culled.to_mesh_clear()
|
||||
|
||||
size = 10
|
||||
dense_vertices = [
|
||||
(x * 0.1, y * 0.1, 0.0)
|
||||
for y in range(size)
|
||||
for x in range(size)
|
||||
]
|
||||
dense_faces = []
|
||||
for y in range(size - 1):
|
||||
for x in range(size - 1):
|
||||
a = y * size + x
|
||||
b = a + 1
|
||||
c = a + size
|
||||
d = c + 1
|
||||
dense_faces.extend(((a, c, b), (b, c, d)))
|
||||
dense_mesh = bpy.data.meshes.new("Dense Validation Mesh")
|
||||
dense_mesh.from_pydata(dense_vertices, [], dense_faces)
|
||||
dense_uv = dense_mesh.uv_layers.new(name="UVMap")
|
||||
dense_uv.data.foreach_set("uv", [0.0] * (len(dense_mesh.loops) * 2))
|
||||
add_face_attributes(dense_mesh, len(dense_faces))
|
||||
dense_object = bpy.data.objects.new("Dense Validation Object", dense_mesh)
|
||||
bpy.context.scene.collection.objects.link(dense_object)
|
||||
dense_modifier = dense_object.modifiers.new(name=group.name, type="NODES")
|
||||
dense_modifier.node_group = group
|
||||
set_input(dense_modifier, group, "Resolution", 0.22)
|
||||
dense_evaluated = dense_object.evaluated_get(
|
||||
bpy.context.evaluated_depsgraph_get()
|
||||
)
|
||||
dense_result = dense_evaluated.to_mesh()
|
||||
try:
|
||||
assert 4 < len(dense_result.polygons) < len(dense_faces)
|
||||
assert all(polygon.normal.z < -0.01 for polygon in dense_result.polygons)
|
||||
finally:
|
||||
dense_evaluated.to_mesh_clear()
|
||||
|
||||
source = bpy.data.objects.new("Validation Source", None)
|
||||
bpy.context.scene.collection.objects.link(source)
|
||||
source.location = (2.0, 3.0, 4.0)
|
||||
bpy.context.view_layer.update()
|
||||
obj.matrix_world = source.matrix_world.copy()
|
||||
bpy.context.view_layer.update()
|
||||
camera = create_matching_camera(
|
||||
bpy.context,
|
||||
source,
|
||||
obj,
|
||||
"Validation",
|
||||
np.asarray(
|
||||
((100.0, 0.0, 50.0), (0.0, 100.0, 50.0), (0.0, 0.0, 1.0))
|
||||
),
|
||||
(101, 101),
|
||||
{"scale_x": 1.0, "scale_z": 1.0, "location": (0.0, 0.0, 2.0)},
|
||||
)
|
||||
assert camera.parent == obj
|
||||
camera_location = tuple(
|
||||
round(value, 5) for value in camera.matrix_world.translation
|
||||
)
|
||||
assert camera_location == (
|
||||
2.0,
|
||||
3.0,
|
||||
6.0,
|
||||
)
|
||||
print(f"Validated Blender {bpy.app.version_string}: {group.name}")
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
main()
|
||||
Reference in New Issue
Block a user