Refactor image mesh processing and background removal functionality

- Updated NODE_GROUP_NAME from "O Refine DA3 Surface" to "O DA3 Surface".
- Introduced `remove_surface_background` function to filter out background faces and compact geometry.
- Enhanced `build_image_mesh` to conditionally remove background faces based on a new property.
- Added `snapform_remove_background` and `snapform_keep_original` properties to operators for better control over background handling.
- Updated UI to reflect new properties and improved resolution scaling logic.
- Refactored tests to cover new background removal functionality and ensure proper geometry handling.
- Adjusted geometry nodes and validation scripts to remove references to background handling.
This commit is contained in:
2026-08-12 16:15:14 +08:00
parent 7cd5681a8b
commit c3a151ad2c
13 changed files with 477 additions and 271 deletions

View File

@@ -3,9 +3,8 @@ from pathlib import Path
import bpy
GROUP_NAME = "O Refine DA3 Surface"
GROUP_NAME = "O DA3 Surface"
DEPTH_PLANE_GROUP_NAME = "O DA3 Depth Plane"
REMOVE_BACKGROUND_ATTRIBUTE = "o_remove_background"
MATERIAL_DISPLACEMENT_ATTRIBUTE = "o_material_displacement"
BACKGROUND_DEPTH_ATTRIBUTE = "o_background_depth"
DEPTH_SCALE_ATTRIBUTE = "o_depth_scale"
@@ -424,14 +423,6 @@ def build_group():
edge.min_value = 0.0
edge.max_value = 100.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",
@@ -454,7 +445,6 @@ def build_group():
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),
@@ -523,25 +513,11 @@ def build_group():
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",
@@ -556,7 +532,7 @@ def build_group():
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(first_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"
@@ -638,7 +614,7 @@ def build_group():
group.color_tag = "GEOMETRY"
group.description = (
"Cull DA3/BEN2 faces, repair merged topology, preserve boundaries, "
"Cull DA3 faces, repair merged topology, preserve boundaries, "
"and reproject UVs."
)
group.use_fake_user = True
@@ -707,6 +683,15 @@ def build_depth_plane_group():
"INPUT",
"NodeSocketMenu",
)
subdivision = new_socket(
interface,
"Subdivision",
"INPUT",
"NodeSocketInt",
)
subdivision.default_value = 5
subdivision.min_value = 0
subdivision.max_value = 10
displacement_strength = new_socket(
interface,
"Depth Scale",
@@ -717,39 +702,17 @@ def build_depth_plane_group():
displacement_strength.min_value = -10000.0
displacement_strength.max_value = 10000.0
displacement_strength.structure_type = "SINGLE"
subdivision = new_socket(
interface,
"Subdivision",
"INPUT",
"NodeSocketInt",
)
subdivision.default_value = 5
subdivision.min_value = 0
subdivision.max_value = 10
background_panel = interface.new_panel(
name="Remove Background",
default_closed=True,
)
remove_background = new_socket(
interface,
"Remove Background",
"INPUT",
"NodeSocketBool",
parent=background_panel,
)
remove_background.default_value = False
remove_background.is_panel_toggle = True
background_depth = new_socket(
interface,
"Background Depth",
"INPUT",
"NodeSocketFloat",
parent=background_panel,
)
background_depth.default_value = 0.0
background_depth.min_value = -10000.0
background_depth.max_value = 10000.0
background_depth.subtype = "DISTANCE"
background_depth.structure_type = "SINGLE"
data_panel = interface.new_panel(name="Data", default_closed=True)
depth_image = new_socket(
interface,
@@ -774,6 +737,7 @@ def build_depth_plane_group():
)
frame_data = []
for name, socket_type, default_value in (
("Frame", "NodeSocketInt", 1),
("Frame Start", "NodeSocketInt", 1),
("Frames", "NodeSocketInt", 1),
("Cyclic", "NodeSocketBool", False),
@@ -786,6 +750,8 @@ def build_depth_plane_group():
parent=data_panel,
)
socket.default_value = default_value
if name == "Frame":
socket.structure_type = "SINGLE"
if name == "Frames":
socket.min_value = 1
frame_data.append(socket)
@@ -809,6 +775,7 @@ def build_depth_plane_group():
nodes,
(50.0, 1750.0),
{
"Frame",
"Frame Start",
"Frames",
"Cyclic",
@@ -817,7 +784,7 @@ def build_depth_plane_group():
background_input = new_group_input(
nodes,
(350.0, 250.0),
{"Remove Background", "Background Depth"},
{"Background Depth"},
)
displacement_input = new_group_input(
nodes,
@@ -924,12 +891,10 @@ def build_depth_plane_group():
)
links.new(grid.outputs["UV Map"], depth_texture.inputs["Vector"])
links.new(grid.outputs["UV Map"], foreground_texture.inputs["Vector"])
scene_time = nodes.new("GeometryNodeInputSceneTime")
scene_time.location = (50.0, 2200.0)
sequence_frame = build_sequence_frame(
nodes,
links,
scene_time.outputs["Frame"],
frames_input.outputs["Frame"],
frames_input,
2200.0,
)
@@ -964,14 +929,6 @@ def build_depth_plane_group():
masked_depth.inputs[2],
)
links.new(clamp_foreground.outputs[0], masked_depth.inputs[3])
selected_depth = nodes.new("ShaderNodeMix")
selected_depth.data_type = "FLOAT"
selected_depth.location = (1000.0, 650.0)
remove_background_value = background_input.outputs["Remove Background"]
links.new(remove_background_value, selected_depth.inputs[0])
links.new(depth_texture.outputs["Color"], selected_depth.inputs[2])
links.new(masked_depth.outputs["Result"], selected_depth.inputs[3])
displacement_menu = nodes.new("GeometryNodeMenuSwitch")
displacement_menu.data_type = "FLOAT"
displacement_menu.enum_items.clear()
@@ -995,7 +952,7 @@ def build_depth_plane_group():
displacement_scale = nodes.new("ShaderNodeMath")
displacement_scale.operation = "MULTIPLY"
displacement_scale.location = (1200.0, 650.0)
links.new(selected_depth.outputs["Result"], displacement_scale.inputs[0])
links.new(masked_depth.outputs["Result"], displacement_scale.inputs[0])
geometry_strength = nodes.new("ShaderNodeMath")
geometry_strength.operation = "MULTIPLY"
geometry_strength.location = (1150.0, 450.0)
@@ -1024,11 +981,6 @@ def build_depth_plane_group():
shade_smooth.location = (1850.0, 650.0)
links.new(set_material.outputs["Geometry"], shade_smooth.inputs["Geometry"])
attribute_values = (
(
REMOVE_BACKGROUND_ATTRIBUTE,
"BOOLEAN",
remove_background_value,
),
(
MATERIAL_DISPLACEMENT_ATTRIBUTE,
"BOOLEAN",

View File

@@ -14,6 +14,7 @@ from snapform.image_depth import (
load_depth_plane_node_group,
modifier_input_identifier,
)
from snapform.image_mesh import load_geometry_node_group
def main():
@@ -27,6 +28,15 @@ def main():
and item.name == "Background Depth"
)
assert background_depth.subtype == "DISTANCE"
assert background_depth.structure_type == "SINGLE"
frame_input = next(
item
for item in group.interface.items_tree
if item.item_type == "SOCKET"
and item.in_out == "INPUT"
and item.name == "Frame"
)
assert frame_input.structure_type == "SINGLE"
geometry_input = next(
item
for item in group.interface.items_tree
@@ -45,22 +55,15 @@ def main():
assert visible_inputs[:6] == [
"Geometry",
"Displacement",
"Depth Scale",
"Subdivision",
"Remove Background",
"Depth Scale",
"Background Depth",
]
remove_background = next(
item
assert not getattr(background_depth.parent, "name", "")
assert not any(
item.name == "Remove Background"
for item in group.interface.items_tree
if item.item_type == "SOCKET"
and item.in_out == "INPUT"
and item.name == "Remove Background"
)
assert remove_background.socket_type == "NodeSocketBool"
assert remove_background.is_panel_toggle
assert remove_background.parent.name == "Remove Background"
assert background_depth.parent == remove_background.parent
depth_scale = next(
item
for item in group.interface.items_tree
@@ -77,7 +80,6 @@ def main():
if node.bl_idname == "GeometryNodeStoreNamedAttribute"
}
attribute_names = (
"o_remove_background",
"o_material_displacement",
"o_background_depth",
"o_depth_scale",
@@ -160,14 +162,31 @@ def main():
and node.node_tree == material_group
]
assert len(material_group_nodes) == 1
assert material_group_nodes[0].name == "O DA3 Depth Plane"
assert material_group_nodes[0].name == "O DA3 Depth Plane Layer"
bump = next(
node
for node in material_group.nodes
if node.bl_idname == "ShaderNodeBump"
)
bump_height_link = next(
link
for link in material_group.links
if link.to_node == bump and link.to_socket == bump.inputs["Height"]
)
assert bump_height_link.from_node.bl_idname == "NodeGroupInput"
assert bump_height_link.from_socket.name == "Color"
attributes = [
node
for node in material_group.nodes
if node.bl_idname == "ShaderNodeAttribute"
]
assert len(attributes) == 4
assert len(attributes) == 3
assert all(node.attribute_type == "GEOMETRY" for node in attributes)
assert {node.attribute_name for node in attributes} == {
"o_material_displacement",
"o_background_depth",
"o_depth_scale",
}
assert not any(
node.bl_idname == "ShaderNodeAttribute"
for node in material.node_tree.nodes
@@ -193,7 +212,25 @@ def main():
and node.node_tree == surface_group
]
assert len(surface_group_nodes) == 1
assert surface_group_nodes[0].name == "O DA3 Surface"
assert surface_group_nodes[0].name == "O DA3 Surface Layer"
surface_outputs = [
item.name
for item in surface_group.interface.items_tree
if item.item_type == "SOCKET"
and item.in_out == "OUTPUT"
]
assert surface_outputs == ["Color", "Roughness", "Alpha", "Normal"]
assert not any(
node.bl_idname == "ShaderNodeDisplacement"
for node in surface_group.nodes
)
refine_group = load_geometry_node_group()
assert not any(
item.item_type == "SOCKET"
and item.in_out == "INPUT"
and item.name == "Background"
for item in refine_group.interface.items_tree
)
surface_depth_input = next(
item
for item in surface_group.interface.items_tree
@@ -206,7 +243,13 @@ def main():
node.bl_idname in forbidden_outer_nodes
for node in surface_material.node_tree.nodes
)
modifier = add_depth_plane_processing(obj, depth, foreground, material)
modifier = add_depth_plane_processing(
obj,
depth,
foreground,
material,
animate_frame=True,
)
modifier[modifier_input_identifier(group, "Depth Scale")] = -2.0
for name, expected in (
("Frame Start", 1),
@@ -214,6 +257,20 @@ def main():
("Cyclic", False),
):
assert modifier[modifier_input_identifier(group, name)] == expected
assert not any(
node.bl_idname == "GeometryNodeInputSceneTime"
for node in group.nodes
)
frame_identifier = modifier_input_identifier(group, "Frame")
frame_driver = next(
fcurve
for fcurve in obj.animation_data.drivers
if fcurve.data_path == (
f'modifiers["{modifier.name}"]["{frame_identifier}"]'
)
)
assert frame_driver.driver.type == "SCRIPTED"
assert frame_driver.driver.expression == "frame"
image_textures = [
node
for node in group.nodes

View File

@@ -51,7 +51,7 @@ def main():
group_inputs = [
node for node in group.nodes if node.bl_idname == "NodeGroupInput"
]
assert len(group_inputs) >= 8
assert len(group_inputs) >= 7
for node in group_inputs:
visible_outputs = [
output for output in node.outputs if output.name and not output.hide
@@ -75,7 +75,6 @@ def main():
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",
@@ -116,7 +115,7 @@ def main():
evaluated = obj.evaluated_get(bpy.context.evaluated_depsgraph_get())
result = evaluated.to_mesh()
try:
assert len(result.polygons) == 1
assert len(result.polygons) == 2
assert "UVMap" in result.attributes
assert result.attributes["UVMap"].data_type == "FLOAT2"
finally: