Add comprehensive documentation for SnapForm extension

- Created development.md for development setup and licensing information.
- Added features.md detailing functionalities and usage instructions for developers and users.
- Introduced index.md to serve as a navigation hub for the documentation.
- Developed outputs.md to explain output formats and their specifications.
- Established service.md to describe the AI Service architecture and environment setup.
- Created surface.md to outline the process of converting Image Empty to perspective meshes.
This commit is contained in:
2026-08-13 23:57:09 +08:00
parent caeb6d706c
commit 13fab9ce88
28 changed files with 998 additions and 830 deletions

View File

@@ -25,12 +25,15 @@ def input_socket(node, name, fallback):
def new_socket(interface, name, in_out, socket_type, parent=None):
return interface.new_socket(
socket = interface.new_socket(
name=name,
in_out=in_out,
socket_type=socket_type,
parent=parent,
)
if in_out == "INPUT" and hasattr(socket, "structure_type"):
socket.structure_type = "SINGLE"
return socket
def new_compare(nodes, data_type, operation, location, value):
@@ -361,8 +364,11 @@ def add_uv_reprojection(group, source, geometry, location):
def build_group(
group_name=GROUP_NAME,
include_confidence=True,
include_stretch=True,
include_discontinuity=True,
include_mask=False,
discontinuity_name="Depth Continuity",
discontinuity_attribute="o_depth_discontinuity",
discontinuity_default=50.0,
):
group = bpy.data.node_groups.new(group_name, "GeometryNodeTree")
group.is_modifier = True
@@ -381,7 +387,7 @@ def build_group(
"INPUT",
"NodeSocketFloat",
)
resolution.default_value = 0.01
resolution.default_value = 0.1
resolution.min_value = 0.000001
resolution.subtype = "DISTANCE"
remesh_iterations = new_socket(
@@ -403,22 +409,24 @@ def build_group(
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 = None
if include_confidence or include_stretch:
if include_confidence or include_discontinuity or include_mask:
culling_panel = interface.new_panel(
name="Culling",
default_closed=False,
)
if include_discontinuity:
discontinuity = new_socket(
interface,
discontinuity_name,
"INPUT",
"NodeSocketFloat",
parent=culling_panel,
)
discontinuity.default_value = discontinuity_default
discontinuity.min_value = 0.0
discontinuity.max_value = 100.0
discontinuity.subtype = "PERCENTAGE"
if include_confidence:
confidence = new_socket(
interface,
@@ -431,18 +439,15 @@ def build_group(
confidence.min_value = 0.0
confidence.max_value = 100.0
confidence.subtype = "PERCENTAGE"
if include_stretch:
edge = new_socket(
if include_mask:
mask_socket = new_socket(
interface,
"Stretch",
"Mask",
"INPUT",
"NodeSocketFloat",
"NodeSocketBool",
parent=culling_panel,
)
edge.default_value = 10.0
edge.min_value = 0.0
edge.max_value = 100.0
edge.subtype = "PERCENTAGE"
mask_socket.default_value = False
options_panel = interface.new_panel(
name="Options",
@@ -470,9 +475,13 @@ def build_group(
(-1800.0, 800.0),
{"Confidence"},
)
stretch_input = None
if include_stretch:
stretch_input = new_group_input(nodes, (-1800.0, 50.0), {"Stretch"})
discontinuity_input = None
if include_discontinuity:
discontinuity_input = new_group_input(
nodes,
(-1800.0, 50.0),
{discontinuity_name},
)
remesh_input = new_group_input(
nodes,
(-100.0, -500.0),
@@ -524,54 +533,67 @@ def build_group(
geometry = delete_confidence.outputs["Geometry"]
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(
if include_discontinuity:
discontinuity_data = nodes.new("GeometryNodeInputNamedAttribute")
discontinuity_data.data_type = "FLOAT"
discontinuity_data.inputs["Name"].default_value = discontinuity_attribute
discontinuity_data.location = (-1800.0, 350.0)
culling_scale = nodes.new("ShaderNodeMath")
culling_scale.operation = "MULTIPLY"
culling_scale.inputs[1].default_value = 0.01
culling_scale.location = (-1650.0, -50.0)
inverse_culling = nodes.new("ShaderNodeMath")
inverse_culling.operation = "SUBTRACT"
inverse_culling.inputs[0].default_value = 0.5
inverse_culling.location = (-1450.0, -50.0)
exponent_scale = nodes.new("ShaderNodeMath")
exponent_scale.operation = "MULTIPLY"
exponent_scale.inputs[1].default_value = 10.0
exponent_scale.location = (-1250.0, -50.0)
threshold_power = nodes.new("ShaderNodeMath")
threshold_power.operation = "POWER"
threshold_power.inputs[0].default_value = 2.0
threshold_power.location = (-1050.0, -50.0)
threshold_scale = nodes.new("ShaderNodeMath")
threshold_scale.operation = "MULTIPLY"
threshold_scale.inputs[1].default_value = 0.04
threshold_scale.location = (-850.0, -50.0)
discontinuity_compare = new_compare(
nodes,
"FLOAT",
"GREATER_THAN",
(-1200.0, 350.0),
(-650.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))
links.new(
discontinuity_input.outputs[discontinuity_name],
culling_scale.inputs[0],
)
links.new(culling_scale.outputs[0], inverse_culling.inputs[1])
links.new(inverse_culling.outputs[0], exponent_scale.inputs[0])
links.new(exponent_scale.outputs[0], threshold_power.inputs[1])
links.new(threshold_power.outputs[0], threshold_scale.inputs[0])
links.new(
discontinuity_data.outputs["Attribute"],
input_socket(discontinuity_compare, "A", 0),
)
links.new(
threshold_scale.outputs[0],
input_socket(discontinuity_compare, "B", 1),
)
delete_geometry = nodes.new("GeometryNodeDeleteGeometry")
delete_geometry.domain = "EDGE"
delete_geometry.domain = "FACE"
delete_geometry.location = (-350.0, 100.0)
links.new(geometry, delete_geometry.inputs["Geometry"])
links.new(edge_compare.outputs["Result"], delete_geometry.inputs["Selection"])
links.new(
discontinuity_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)
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(culled_geometry, delete_orphans.inputs["Geometry"])
links.new(orphan_vertex.outputs["Result"], delete_orphans.inputs["Selection"])
remeshed = add_remesh_zone(
group,
delete_orphans.outputs["Geometry"],
culled_geometry,
remesh_input.outputs["Resolution"],
remesh_input.outputs["Relax Iterations"],
topology_input.outputs["Fix Topology"],
@@ -634,7 +656,11 @@ def build_group(
def configure_moge_surface_group(group):
nodes = group.nodes
links = group.links
mask_input = new_group_input(nodes, (-1800.0, 1050.0), {"Mask"})
controls = 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"
@@ -646,7 +672,7 @@ def configure_moge_surface_group(group):
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(controls.outputs["Mask"], use_mask.inputs[0])
links.new(invert_mask.outputs[0], use_mask.inputs[1])
first_processing = min(
(
@@ -667,7 +693,7 @@ def configure_moge_surface_group(group):
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, "
"Cull MoGe-2 mask and depth-edge faces, repair topology, preserve boundaries, "
"and reproject UVs."
)
group.use_fake_user = True
@@ -1301,8 +1327,11 @@ def main():
moge_surface_group = build_group(
MOGE_SURFACE_GROUP_NAME,
include_confidence=False,
include_stretch=False,
include_discontinuity=True,
include_mask=True,
discontinuity_name="Depth Continuity",
discontinuity_attribute="o_depth_discontinuity",
discontinuity_default=50.0,
)
configure_moge_surface_group(moge_surface_group)
build_depth_plane_group()

View File

@@ -21,14 +21,9 @@ def set_input(modifier, group, name, value):
modifier[item.identifier] = value
def add_face_attributes(mesh, count, background_values=None):
def add_face_attributes(mesh, count):
confidence = mesh.attributes.new("o_face_confidence", "FLOAT", "FACE")
confidence.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():
@@ -44,6 +39,13 @@ def main():
and item.name == "Geometry"
)
assert geometry_input.structure_type == "SINGLE"
input_sockets = [
item for item in group.interface.items_tree
if item.item_type == "SOCKET" and item.in_out == "INPUT"
]
assert all(item.structure_type == "SINGLE" for item in input_sockets)
resolution = next(item for item in input_sockets if item.name == "Resolution")
assert abs(resolution.default_value - 0.1) < 1e-6
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)
@@ -72,8 +74,8 @@ def main():
assert not panels["Culling"].default_closed
assert panels["Options"].default_closed
assert [item.name for item in panels["Culling"].interface_items] == [
"Depth Continuity",
"Confidence",
"Stretch",
]
assert [item.name for item in panels["Options"].interface_items] == [
"Fix Topology",
@@ -86,7 +88,10 @@ def main():
and item.name == "Smooth"
)
assert smooth.default_value == 0.5
for name, default_value in (("Confidence", 0.0), ("Stretch", 10.0)):
for name, default_value in (
("Confidence", 0.0),
("Depth Continuity", 50.0),
):
item = next(
item
for item in panels["Culling"].interface_items
@@ -105,11 +110,21 @@ def main():
[],
[(0, 2, 1), (1, 2, 3)],
)
edge_stretch = mesh.attributes.new("o_edge_stretch", "FLOAT", "EDGE")
edge_stretch.data.foreach_set("value", [0.05] * len(mesh.edges))
discontinuity = mesh.attributes.new(
"o_depth_discontinuity",
"FLOAT",
"FACE",
)
discontinuity.data.foreach_set("value", [0.03] * len(mesh.polygons))
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])
add_face_attributes(mesh, 2)
assert not {
"o_background",
"o_background_face",
"o_confidence",
"o_confidence_percentile",
}.intersection(mesh.attributes.keys())
obj = bpy.data.objects.new("Validation Object", mesh)
bpy.context.scene.collection.objects.link(obj)
@@ -130,13 +145,13 @@ def main():
finally:
evaluated.to_mesh_clear()
set_input(modifier, group, "Stretch", 0.0)
stretch_item = next(
set_input(modifier, group, "Depth Continuity", 100.0)
discontinuity_item = next(
item
for item in group.interface.items_tree
if item.item_type == "SOCKET" and item.name == "Stretch"
if item.item_type == "SOCKET" and item.name == "Depth Continuity"
)
assert modifier[stretch_item.identifier] == 0.0
assert modifier[discontinuity_item.identifier] == 100.0
obj.update_tag(refresh={"DATA"})
bpy.context.view_layer.update()
culled = obj.evaluated_get(bpy.context.evaluated_depsgraph_get())

View File

@@ -80,6 +80,7 @@ def main():
]
assert image_textures
assert all(node.extension == "EXTEND" for node in image_textures)
assert any(node.image.name == "MoGe Test.depth" for node in image_textures)
geometry_textures = [
node
for node in geometry_group.nodes

View File

@@ -51,6 +51,10 @@ def main():
mask=mask,
points=points,
intrinsics=intrinsics,
normal=np.broadcast_to(
np.array([0.0, 0.0, -1.0], dtype=np.float32),
(1, height, width, 3),
),
)
prepare_moge_surface_archive(results_path, mesh_path)
surface = build_image_mesh(
@@ -60,7 +64,6 @@ def main():
False,
True,
False,
False,
True,
)
assert len(surface.data.vertices) > 0
@@ -71,12 +74,16 @@ def main():
if getattr(item, "in_out", None) == "INPUT"
}
assert "Stretch" not in inputs
assert "Depth Continuity" in inputs
assert "Mask" in inputs
assert "Confidence" not in inputs
assert "o_mask" in surface.data.attributes
assert surface.data.attributes["o_mask"].domain == "FACE"
assert not all(item.value for item in surface.data.attributes["o_mask"].data)
attribute_names = set(surface.data.attributes.keys())
assert attribute_names >= {"o_depth", "o_mask"}
assert "o_depth_discontinuity" in attribute_names
assert surface.data.attributes["o_depth_discontinuity"].data_type == "FLOAT"
assert "o_edge_stretch" not in attribute_names
assert not {
"o_confidence",
@@ -90,13 +97,35 @@ def main():
for item in surface.modifiers[0].node_group.interface.items_tree
if getattr(item, "in_out", None) == "INPUT" and item.name == "Mask"
)
assert mask_socket.default_value is True
assert mask_socket.default_value is False
assert mask_socket.structure_type == "SINGLE"
assert not mask_socket.parent.name
assert not any(
item.item_type == "PANEL" and item.name == "Culling"
assert mask_socket.parent.name == "Culling"
depth_edge_socket = next(
item
for item in surface.modifiers[0].node_group.interface.items_tree
if getattr(item, "in_out", None) == "INPUT"
and item.name == "Depth Continuity"
)
assert depth_edge_socket.default_value == 50.0
assert depth_edge_socket.subtype == "PERCENTAGE"
assert depth_edge_socket.structure_type == "SINGLE"
culling_panel = next(
item for item in surface.modifiers[0].node_group.interface.items_tree
if item.item_type == "PANEL" and item.name == "Culling"
)
assert [item.name for item in culling_panel.interface_items] == [
"Depth Continuity",
"Mask",
]
input_sockets = [
item for item in surface.modifiers[0].node_group.interface.items_tree
if item.item_type == "SOCKET" and item.in_out == "INPUT"
]
assert all(item.structure_type == "SINGLE" for item in input_sockets)
resolution_socket = next(
item for item in input_sockets if item.name == "Resolution"
)
assert abs(resolution_socket.default_value - 0.1) < 1e-6
smooth_socket = next(
item
for item in surface.modifiers[0].node_group.interface.items_tree
@@ -111,6 +140,26 @@ def main():
assert MOGE_SURFACE_MATERIAL_NODE_GROUP_NAME in bpy.data.node_groups
material_group = bpy.data.node_groups[MOGE_SURFACE_MATERIAL_NODE_GROUP_NAME]
assert any(node.bl_idname == "ShaderNodeBump" for node in material_group.nodes)
normal_map = next(
node for node in material_group.nodes
if node.bl_idname == "ShaderNodeNormalMap"
)
assert normal_map.space == "OBJECT"
normal_strength = next(
item for item in material_group.interface.items_tree
if getattr(item, "in_out", None) == "INPUT"
and item.name == "Normal Strength"
)
assert normal_strength.default_value == 1.0
assert normal_strength.min_value == 0.0
assert normal_map.inputs["Strength"].is_linked
assert not surface.data.has_custom_normals
normal_texture = next(
node for node in surface.data.materials[0].node_tree.nodes
if node.bl_idname == "ShaderNodeTexImage"
and node.image.colorspace_settings.name == "Non-Color"
)
assert normal_texture.image.name == "MoGe Surface Test.normal"
assert any(child.type == "CAMERA" for child in surface.children)
print("MoGe-2 surface validation passed")
@@ -131,23 +180,24 @@ def validate_real_result(results_path, image_path):
False,
True,
False,
False,
True,
)
assert len(surface.data.vertices) > 100
assert len(surface.data.polygons) > 100
assert surface.modifiers[0].node_group.name == MOGE_NODE_GROUP_NAME
assert MOGE_SURFACE_MATERIAL_NODE_GROUP_NAME in bpy.data.node_groups
assert surface.data.has_custom_normals
normals = np.empty(len(surface.data.corner_normals) * 3, dtype=np.float32)
surface.data.corner_normals.foreach_get("vector", normals)
normals = normals.reshape(-1, 3)
assert np.isfinite(normals).all()
assert np.min(np.linalg.norm(normals, axis=1)) > 0.99
assert not surface.data.has_custom_normals
material_group = bpy.data.node_groups[MOGE_SURFACE_MATERIAL_NODE_GROUP_NAME]
assert any(node.bl_idname == "ShaderNodeBump" for node in material_group.nodes)
normal_map = next(
node for node in material_group.nodes
if node.bl_idname == "ShaderNodeNormalMap"
)
assert normal_map.space == "OBJECT"
assert normal_map.inputs["Strength"].is_linked
attribute_names = set(surface.data.attributes.keys())
assert {"o_depth", "o_mask"}.issubset(attribute_names)
assert "o_depth_discontinuity" in attribute_names
assert "o_edge_stretch" not in attribute_names
assert not {
"o_confidence",