feat: Add restore cutout depth extension with edge depth correction

- Introduced a new extension for restoring cutout depth with edge correction.
- Implemented edge depth extension algorithm in `edge_depth.py` to enhance depth accuracy near cutout edges.
- Updated `generate_moge_artifacts` to include an option for extending depth edges during depth generation.
- Modified cutout job processing to enable edge depth extension based on depth request.
- Added comprehensive tests for edge depth functionality, ensuring correct behavior under various scenarios.
- Updated existing node loading functions to remove asset revision dependencies, simplifying the loading process.
- Created new design and proposal documentation outlining the goals and decisions for the extension.
This commit is contained in:
2026-09-13 20:36:52 +08:00
parent 2a00ed0ee6
commit a0dd6e4a88
25 changed files with 482 additions and 32 deletions

View File

@@ -53,6 +53,7 @@
"GeometryNodeRepeatOutput": 3,
"GeometryNodeSampleIndex": 3,
"GeometryNodeSampleNearest": 1,
"GeometryNodeSampleNearestSurface": 1,
"GeometryNodeSeparateGeometry": 1,
"GeometryNodeSetMaterialIndex": 0,
"GeometryNodeSetPosition": 0,
@@ -72,6 +73,7 @@
"ShaderNodeMath": 2,
"ShaderNodeMix": 3,
"ShaderNodeSeparateXYZ": 0,
"ShaderNodeValue": 1,
"ShaderNodeVectorMath": 1
}
}

View File

@@ -0,0 +1,78 @@
"""Build a shared center on the original Cutout mesh."""
from .nodes import evaluate_field, group_input, sample_field
def vector(group, operation, first, second=None):
node = group.nodes.new("ShaderNodeVectorMath")
node.operation = operation
for value, socket in ((first, node.inputs[0]), (second, node.inputs[3 if operation == "SCALE" else 1])):
if value is None:
continue
if hasattr(value, "node"):
group.links.new(value, socket)
else:
socket.default_value = value
return node.outputs[0]
def axes(group, value):
node = group.nodes.new("ShaderNodeSeparateXYZ")
group.links.new(value, node.inputs[0])
return node.outputs
def combine(group, x, y, z=0):
node = group.nodes.new("ShaderNodeCombineXYZ")
for value, socket in zip((x, y, z), node.inputs):
if hasattr(value, "node"):
group.links.new(value, socket)
else:
socket.default_value = value
return node.outputs[0]
def set_position(group, geometry, position):
node = group.nodes.new("GeometryNodeSetPosition")
group.links.new(geometry, node.inputs["Geometry"])
group.links.new(position, node.inputs["Position"])
return node.outputs[0]
def sample_surface(group, geometry, value, data_type, domain_position, query):
"""Keep point indices on points before the face interpolation adapts domains."""
field = evaluate_field(group, sample_field(group, geometry, value, data_type), data_type, "POINT")
flat = set_position(group, geometry, domain_position)
sample = group.nodes.new("GeometryNodeSampleNearestSurface")
sample.data_type = data_type
group.links.new(flat, sample.inputs["Mesh"])
group.links.new(field, sample.inputs["Value"])
group.links.new(query, sample.inputs["Sample Position"])
return sample.outputs[0]
def build_center_surface(group, unsplit):
"""Smooth the unsplit displaced surface without moving its XY coordinates."""
nodes, links = group.nodes, group.links
controls = group_input(nodes, {"Center Smooth"})
position = nodes.new("GeometryNodeInputPosition").outputs[0]
components = axes(group, position)
uv = nodes.new("GeometryNodeInputNamedAttribute")
uv.data_type = "FLOAT_VECTOR"
uv.inputs["Name"].default_value = "UVMap"
coordinates = uv.outputs["Attribute"]
blur = nodes.new("GeometryNodeBlurAttribute")
blur.data_type = "FLOAT"
links.new(components["Z"], blur.inputs["Value"])
links.new(controls.outputs["Center Smooth"], blur.inputs["Iterations"])
support = set_position(group, unsplit, combine(group, components["X"], components["Y"], blur.outputs[0]))
height = sample_surface(group, support, components["Z"], "FLOAT", coordinates, coordinates)
normal = nodes.new("GeometryNodeInputNormal").outputs[0]
normal_blur = nodes.new("GeometryNodeBlurAttribute")
normal_blur.data_type = "FLOAT_VECTOR"
links.new(evaluate_field(group, normal, "FLOAT_VECTOR", "POINT"), normal_blur.inputs["Value"])
links.new(controls.outputs["Center Smooth"], normal_blur.inputs["Iterations"])
normal = normal_blur.outputs[0]
normal = vector(group, "NORMALIZE", sample_surface(group, support, normal, "FLOAT_VECTOR", coordinates, coordinates))
return {"geometry": support, "preview": support, "height": height, "normal": normal}

View File

@@ -155,7 +155,6 @@ def build_image_cutout_group():
group = bpy.data.node_groups.new(name, 'GeometryNodeTree')
group.is_modifier, group.use_fake_user = (True, True)
group.color_tag = 'GEOMETRY'
group['anyimage_asset_revision'] = 13
interface_socket(group, 'Geometry', 'INPUT', 'NodeSocketGeometry')
interface_socket(group, 'Mode', 'INPUT', 'NodeSocketMenu')
float_input(group, 'Thickness', 1.0).description = 'Control how much the surface bulges in Balloon mode. Zero removes the bulge.'

View File

@@ -107,7 +107,6 @@ def build_image_depth_balloon_group():
group = bpy.data.node_groups.new(name, 'GeometryNodeTree')
group.is_modifier, group.use_fake_user = (True, True)
group.color_tag = 'GEOMETRY'
group['anyimage_asset_revision'] = 14
interface_socket(group, 'Geometry', 'INPUT', 'NodeSocketGeometry')
float_input(group, 'Thickness', 1.0).description = 'Control how much the surface bulges. Zero removes the bulge.'
float_input(group, 'Depth Scale', 1.0)

View File

@@ -39,7 +39,6 @@ def _quad_sphere(group, segments):
def build_image_depth_panorama_group():
group = bpy.data.node_groups.new("O Image Depth Panorama", "GeometryNodeTree")
group.is_modifier = group.use_fake_user = True
group["anyimage_asset_revision"] = 15
interface_socket(group, "Geometry", "INPUT", "NodeSocketGeometry")
subdivide = interface_socket(group, "Subdivide", "INPUT", "NodeSocketInt")
subdivide.description = "Increase mesh density to represent finer shapes. Higher levels create more vertices and require more processing time."

View File

@@ -25,7 +25,6 @@ def build_image_depth_plane_group(image_plane_group):
"GeometryNodeTree",
)
group.is_modifier = True
group["anyimage_asset_revision"] = 16
geometry = _interface_socket(group, "Geometry", "INPUT", "NodeSocketGeometry")
geometry.structure_type = "SINGLE"
subdivide = _interface_socket(group, "Subdivide", "INPUT", "NodeSocketInt")