feat: Add MoGe-2 surface generation and export functionality

- Introduced new enum items for depth plane and surface models with caching.
- Added MOGE2_MESH_JOB to operators for converting images to surfaces.
- Implemented surface mesh building and exporting in RunJob operator.
- Enhanced UI to include export format options for MoGe-2.
- Created validation tools for MoGe-2 surface assets and depth planes.
- Updated geometry nodes to support new surface generation features.
- Refactored existing tests to accommodate new functionality and ensure stability.
This commit is contained in:
2026-08-13 22:07:36 +08:00
parent a6370d678d
commit caeb6d706c
17 changed files with 902 additions and 251 deletions

View File

@@ -4,6 +4,7 @@ import bpy
GROUP_NAME = "O DA3 Surface"
MOGE_SURFACE_GROUP_NAME = "O MoGe2 Surface"
DEPTH_PLANE_GROUP_NAME = "O DA3 Depth Plane"
MOGE_DEPTH_PLANE_GROUP_NAME = "O MoGe2 Depth Plane"
MATERIAL_DISPLACEMENT_ATTRIBUTE = "o_material_displacement"
@@ -357,8 +358,13 @@ def add_uv_reprojection(group, source, geometry, location):
return store_uv.outputs["Geometry"]
def build_group():
group = bpy.data.node_groups.new(GROUP_NAME, "GeometryNodeTree")
def build_group(
group_name=GROUP_NAME,
include_confidence=True,
include_stretch=True,
include_mask=False,
):
group = bpy.data.node_groups.new(group_name, "GeometryNodeTree")
group.is_modifier = True
interface = group.interface
geometry_input_socket = new_socket(
@@ -393,37 +399,50 @@ def build_group():
"INPUT",
"NodeSocketFloat",
)
smooth_amount.default_value = 1.0
smooth_amount.default_value = 0.5
smooth_amount.min_value = 0.0
smooth_amount.max_value = 1.0
smooth_amount.subtype = "FACTOR"
if include_mask:
mask_socket = new_socket(
interface,
"Mask",
"INPUT",
"NodeSocketBool",
)
mask_socket.default_value = True
mask_socket.structure_type = "SINGLE"
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 = 100.0
confidence.subtype = "PERCENTAGE"
edge = new_socket(
interface,
"Stretch",
"INPUT",
"NodeSocketFloat",
parent=culling_panel,
)
edge.default_value = 10.0
edge.min_value = 0.0
edge.max_value = 100.0
edge.subtype = "PERCENTAGE"
culling_panel = None
if include_confidence or include_stretch:
culling_panel = interface.new_panel(
name="Culling",
default_closed=False,
)
if include_confidence:
confidence = new_socket(
interface,
"Confidence",
"INPUT",
"NodeSocketFloat",
parent=culling_panel,
)
confidence.default_value = 0.0
confidence.min_value = 0.0
confidence.max_value = 100.0
confidence.subtype = "PERCENTAGE"
if include_stretch:
edge = new_socket(
interface,
"Stretch",
"INPUT",
"NodeSocketFloat",
parent=culling_panel,
)
edge.default_value = 10.0
edge.min_value = 0.0
edge.max_value = 100.0
edge.subtype = "PERCENTAGE"
options_panel = interface.new_panel(
name="Options",
@@ -444,8 +463,16 @@ def build_group():
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"})
confidence_input = None
if include_confidence:
confidence_input = new_group_input(
nodes,
(-1800.0, 800.0),
{"Confidence"},
)
stretch_input = None
if include_stretch:
stretch_input = new_group_input(nodes, (-1800.0, 50.0), {"Stretch"})
remesh_input = new_group_input(
nodes,
(-100.0, -500.0),
@@ -457,89 +484,71 @@ def build_group():
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_scale = nodes.new("ShaderNodeMath")
confidence_scale.operation = "MULTIPLY"
confidence_scale.inputs[1].default_value = 0.01
confidence_scale.location = (-1600.0, 800.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"],
confidence_scale.inputs[0],
)
links.new(
confidence_scale.outputs[0],
input_socket(confidence_compare, "B", 1),
)
geometry = geometry_input.outputs["Geometry"]
if include_confidence:
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_scale = nodes.new("ShaderNodeMath")
confidence_scale.operation = "MULTIPLY"
confidence_scale.inputs[1].default_value = 0.01
confidence_scale.location = (-1600.0, 800.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"],
confidence_scale.inputs[0],
)
links.new(
confidence_scale.outputs[0],
input_socket(confidence_compare, "B", 1),
)
delete_confidence = nodes.new("GeometryNodeDeleteGeometry")
delete_confidence.domain = "FACE"
delete_confidence.location = (-1050.0, 600.0)
links.new(geometry, delete_confidence.inputs["Geometry"])
links.new(
confidence_compare.outputs["Result"],
delete_confidence.inputs["Selection"],
)
geometry = delete_confidence.outputs["Geometry"]
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_scale = nodes.new("ShaderNodeMath")
edge_scale.operation = "MULTIPLY"
edge_scale.inputs[1].default_value = 0.01
edge_scale.location = (-1600.0, -50.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_scale.inputs[0])
links.new(edge_scale.outputs[0], 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))
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])
full_stretch = new_compare(
nodes,
"FLOAT",
"GREATER_EQUAL",
(-950.0, -20.0),
0.999999,
)
links.new(
edge_scale.outputs[0],
input_socket(full_stretch, "A", 0),
)
culling_selection = nodes.new("FunctionNodeBooleanMath")
culling_selection.operation = "OR"
culling_selection.location = (-550.0, 300.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"
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"])
culled_geometry = geometry
if include_stretch:
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_scale = nodes.new("ShaderNodeMath")
edge_scale.operation = "MULTIPLY"
edge_scale.inputs[1].default_value = 0.01
edge_scale.location = (-1600.0, -50.0)
edge_compare = new_compare(
nodes,
"FLOAT",
"GREATER_THAN",
(-1200.0, 350.0),
0.0,
)
links.new(stretch_input.outputs["Stretch"], edge_scale.inputs[0])
links.new(edge_attribute.outputs["Attribute"], input_socket(edge_compare, "A", 0))
links.new(edge_scale.outputs[0], input_socket(edge_compare, "B", 1))
delete_geometry = nodes.new("GeometryNodeDeleteGeometry")
delete_geometry.domain = "EDGE"
delete_geometry.location = (-350.0, 100.0)
links.new(geometry, delete_geometry.inputs["Geometry"])
links.new(edge_compare.outputs["Result"], delete_geometry.inputs["Selection"])
culled_geometry = delete_geometry.outputs["Geometry"]
vertex_neighbors = nodes.new("GeometryNodeInputMeshVertexNeighbors")
vertex_neighbors.location = (-550.0, -320.0)
@@ -557,7 +566,7 @@ def build_group():
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(culled_geometry, delete_orphans.inputs["Geometry"])
links.new(orphan_vertex.outputs["Result"], delete_orphans.inputs["Selection"])
remeshed = add_remesh_zone(
@@ -622,6 +631,49 @@ def build_group():
return group
def configure_moge_surface_group(group):
nodes = group.nodes
links = group.links
mask_input = new_group_input(nodes, (-1800.0, 1050.0), {"Mask"})
mask_attribute = nodes.new("GeometryNodeInputNamedAttribute")
mask_attribute.data_type = "BOOLEAN"
mask_attribute.inputs["Name"].default_value = "o_mask"
mask_attribute.location = (-1550.0, 1050.0)
invert_mask = nodes.new("FunctionNodeBooleanMath")
invert_mask.operation = "NOT"
invert_mask.location = (-1350.0, 1050.0)
use_mask = nodes.new("FunctionNodeBooleanMath")
use_mask.operation = "AND"
use_mask.location = (-1150.0, 1050.0)
links.new(mask_attribute.outputs["Attribute"], invert_mask.inputs[0])
links.new(mask_input.outputs["Mask"], use_mask.inputs[0])
links.new(invert_mask.outputs[0], use_mask.inputs[1])
first_processing = min(
(
node
for node in nodes
if node.bl_idname == "GeometryNodeDeleteGeometry"
),
key=lambda node: node.location.x,
)
geometry_input = first_processing.inputs["Geometry"]
previous_link = geometry_input.links[0]
previous_geometry = previous_link.from_socket
links.remove(previous_link)
delete_mask = nodes.new("GeometryNodeDeleteGeometry")
delete_mask.domain = "FACE"
delete_mask.location = (-750.0, 650.0)
links.new(previous_geometry, delete_mask.inputs["Geometry"])
links.new(use_mask.outputs[0], delete_mask.inputs["Selection"])
links.new(delete_mask.outputs["Geometry"], geometry_input)
group.description = (
"Cull MoGe-2 stretched faces, repair topology, preserve boundaries, "
"and reproject UVs."
)
group.use_fake_user = True
return group
def build_sequence_frame(nodes, links, scene_frame, frames_input, y):
relative_frame = nodes.new("ShaderNodeMath")
relative_frame.operation = "SUBTRACT"
@@ -1246,6 +1298,13 @@ def main():
bpy.ops.wm.read_factory_settings(use_empty=True)
bpy.context.preferences.filepaths.save_version = 0
build_group()
moge_surface_group = build_group(
MOGE_SURFACE_GROUP_NAME,
include_confidence=False,
include_stretch=False,
include_mask=True,
)
configure_moge_surface_group(moge_surface_group)
build_depth_plane_group()
build_moge_depth_plane_group()
OUTPUT_PATH.parent.mkdir(parents=True, exist_ok=True)