Refactor image handling and update foreground processing

- Changed the logic in `remove_background_faces` to keep only faces with all foreground vertices.
- Updated background job to save foreground images with a new naming convention (`color.0001.png`).
- Replaced `copy_frames` with `copy_color_images` in various jobs to ensure consistent image naming.
- Introduced a new function `copy_color_images` to handle the copying of images with a specific naming format.
- Updated the BEN2 model to save output images with a new prefix for better clarity.
- Modified tests to reflect changes in image naming and processing logic.
- Added a new shadeless node group for improved material handling in geometry nodes.
- Enhanced validation scripts to check for new image naming conventions and ensure proper functionality.
This commit is contained in:
2026-08-19 01:45:59 +08:00
parent 835225d264
commit 20b653bb30
28 changed files with 663 additions and 383 deletions

View File

@@ -7,6 +7,7 @@ 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"
SHADELESS_GROUP_NAME = "O Shadeless"
MATERIAL_DISPLACEMENT_ATTRIBUTE = "o_material_displacement"
BACKGROUND_DEPTH_ATTRIBUTE = "o_background_depth"
DEPTH_SCALE_ATTRIBUTE = "o_depth_scale"
@@ -800,9 +801,9 @@ def build_depth_plane_group():
"NodeSocketImage",
parent=data_panel,
)
foreground_image = new_socket(
color_image = new_socket(
interface,
"Foreground Image",
"Color Image",
"INPUT",
"NodeSocketImage",
parent=data_panel,
@@ -835,7 +836,7 @@ def build_depth_plane_group():
socket.min_value = 1
frame_data.append(socket)
depth_image.hide_in_modifier = True
foreground_image.hide_in_modifier = True
color_image.hide_in_modifier = True
material.hide_in_modifier = True
for socket in frame_data:
socket.hide_in_modifier = True
@@ -848,7 +849,7 @@ def build_depth_plane_group():
images_input = new_group_input(
nodes,
(100.0, 1250.0),
{"Depth Image", "Foreground Image"},
{"Depth Image", "Color Image"},
)
frames_input = new_group_input(
nodes,
@@ -973,7 +974,7 @@ def build_depth_plane_group():
foreground_texture.location = (350.0, 900.0)
links.new(images_input.outputs["Depth Image"], depth_texture.inputs["Image"])
links.new(
images_input.outputs["Foreground Image"],
images_input.outputs["Color Image"],
foreground_texture.inputs["Image"],
)
links.new(grid.outputs["UV Map"], depth_texture.inputs["Vector"])
@@ -1141,10 +1142,13 @@ def build_moge_depth_plane_group():
depth_image = new_socket(
interface, "Depth Image", "INPUT", "NodeSocketImage", data_panel
)
color_image = new_socket(
interface, "Color Image", "INPUT", "NodeSocketImage", data_panel
)
material = new_socket(
interface, "Material", "INPUT", "NodeSocketMaterial", data_panel
)
for socket in (depth_image, material):
for socket in (depth_image, color_image, material):
socket.hide_in_modifier = True
frame_sockets = []
for name, socket_type, default in (
@@ -1176,7 +1180,7 @@ def build_moge_depth_plane_group():
background_depth_value.inputs[0],
)
images = new_group_input(
nodes, (-500.0, 1100.0), {"Depth Image"}
nodes, (-500.0, 1100.0), {"Depth Image", "Color Image"}
)
frames = new_group_input(
nodes,
@@ -1241,18 +1245,42 @@ def build_moge_depth_plane_group():
depth_texture.interpolation = "Linear"
depth_texture.extension = "EXTEND"
depth_texture.location = (-250.0, 1100.0)
color_texture = nodes.new("GeometryNodeImageTexture")
color_texture.interpolation = "Closest"
color_texture.extension = "EXTEND"
color_texture.location = (-250.0, 850.0)
links.new(images.outputs["Depth Image"], depth_texture.inputs["Image"])
links.new(images.outputs["Color Image"], color_texture.inputs["Image"])
links.new(grid.outputs["UV Map"], depth_texture.inputs["Vector"])
links.new(grid.outputs["UV Map"], color_texture.inputs["Vector"])
frame = build_sequence_frame(
nodes, links, frames.outputs["Frame"], frames, 1800.0
)
links.new(frame, depth_texture.inputs["Frame"])
links.new(frame, color_texture.inputs["Frame"])
clamp_depth = nodes.new("ShaderNodeMath")
clamp_depth.operation = "MAXIMUM"
clamp_depth.location = (0.0, 750.0)
links.new(depth_texture.outputs["Color"], clamp_depth.inputs[0])
links.new(background_depth_value.outputs[0], clamp_depth.inputs[1])
color_mask = new_compare(
nodes,
"FLOAT",
"GREATER_THAN",
(0.0, 1000.0),
0.5,
)
links.new(
color_texture.outputs["Alpha"],
input_socket(color_mask, "A", 0),
)
masked_depth = nodes.new("ShaderNodeMix")
masked_depth.data_type = "FLOAT"
masked_depth.location = (200.0, 850.0)
links.new(color_mask.outputs["Result"], masked_depth.inputs[0])
links.new(background_depth_value.outputs[0], masked_depth.inputs[2])
links.new(clamp_depth.outputs[0], masked_depth.inputs[3])
displacement_menu = nodes.new("GeometryNodeMenuSwitch")
displacement_menu.data_type = "FLOAT"
displacement_menu.enum_items.clear()
@@ -1276,7 +1304,7 @@ def build_moge_depth_plane_group():
scaled_depth = nodes.new("ShaderNodeMath")
scaled_depth.operation = "MULTIPLY"
scaled_depth.location = (250.0, 750.0)
links.new(clamp_depth.outputs[0], scaled_depth.inputs[0])
links.new(masked_depth.outputs["Result"], scaled_depth.inputs[0])
links.new(geometry_strength.outputs[0], scaled_depth.inputs[1])
normal = nodes.new("GeometryNodeInputNormal")
normal.location = (0.0, 500.0)
@@ -1320,6 +1348,80 @@ def build_moge_depth_plane_group():
return group
def build_shadeless_group():
group = bpy.data.node_groups.new(SHADELESS_GROUP_NAME, "ShaderNodeTree")
interface = group.interface
new_socket(interface, "Shader", "OUTPUT", "NodeSocketShader")
new_socket(interface, "Color", "INPUT", "NodeSocketColor")
nodes = group.nodes
links = group.links
group_input = nodes.new("NodeGroupInput")
group_input.location = (-900.0, -350.0)
group_output = nodes.new("NodeGroupOutput")
group_output.location = (850.0, 100.0)
diffuse = nodes.new("ShaderNodeBsdfDiffuse")
diffuse.location = (250.0, -150.0)
emission = nodes.new("ShaderNodeEmission")
emission.location = (250.0, -400.0)
links.new(group_input.outputs["Color"], diffuse.inputs["Color"])
links.new(group_input.outputs["Color"], emission.inputs["Color"])
light_path = nodes.new("ShaderNodeLightPath")
light_path.location = (-900.0, 150.0)
bounce_count = nodes.new("ShaderNodeMath")
bounce_count.operation = "SUBTRACT"
bounce_count.label = "Bounce Count"
bounce_count.location = (-500.0, -50.0)
links.new(light_path.outputs["Ray Depth"], bounce_count.inputs[0])
links.new(light_path.outputs["Transmission Depth"], bounce_count.inputs[1])
refracted = nodes.new("ShaderNodeMath")
refracted.operation = "SUBTRACT"
refracted.label = "Camera or Refracted"
refracted.inputs[0].default_value = 1.0
refracted.location = (-250.0, -50.0)
links.new(bounce_count.outputs[0], refracted.inputs[1])
reflection_limit = nodes.new("ShaderNodeMath")
reflection_limit.operation = "SUBTRACT"
reflection_limit.label = "Limit Reflections"
reflection_limit.inputs[0].default_value = 2.0
reflection_limit.location = (-650.0, 450.0)
links.new(light_path.outputs["Ray Depth"], reflection_limit.inputs[1])
camera_reflected = nodes.new("ShaderNodeMath")
camera_reflected.operation = "MULTIPLY"
camera_reflected.label = "Camera Ray to Glossy"
camera_reflected.location = (-400.0, 400.0)
links.new(reflection_limit.outputs[0], camera_reflected.inputs[0])
links.new(light_path.outputs["Is Glossy Ray"], camera_reflected.inputs[1])
shadow_or_reflect = nodes.new("ShaderNodeMath")
shadow_or_reflect.operation = "MAXIMUM"
shadow_or_reflect.label = "Shadow or Reflection?"
shadow_or_reflect.location = (-100.0, 350.0)
links.new(camera_reflected.outputs[0], shadow_or_reflect.inputs[0])
links.new(light_path.outputs["Is Shadow Ray"], shadow_or_reflect.inputs[1])
shader_factor = nodes.new("ShaderNodeMath")
shader_factor.operation = "MAXIMUM"
shader_factor.label = "Shadow, Reflect or Refract?"
shader_factor.location = (150.0, 300.0)
links.new(shadow_or_reflect.outputs[0], shader_factor.inputs[0])
links.new(refracted.outputs[0], shader_factor.inputs[1])
mix = nodes.new("ShaderNodeMixShader")
mix.location = (550.0, 100.0)
links.new(shader_factor.outputs[0], mix.inputs["Fac"])
links.new(diffuse.outputs["BSDF"], mix.inputs[1])
links.new(emission.outputs["Emission"], mix.inputs[2])
links.new(mix.outputs["Shader"], group_output.inputs["Shader"])
group.color_tag = "SHADER"
group.description = (
"Display color without scene lighting while preserving shadows "
"and reflections."
)
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
@@ -1336,6 +1438,7 @@ def main():
configure_moge_surface_group(moge_surface_group)
build_depth_plane_group()
build_moge_depth_plane_group()
build_shadeless_group()
OUTPUT_PATH.parent.mkdir(parents=True, exist_ok=True)
bpy.ops.wm.save_as_mainfile(filepath=str(OUTPUT_PATH), check_existing=False)

View File

@@ -13,6 +13,8 @@ sys.path.insert(
from snapform.blender.depth_plane import (
DEPTH_PLANE_MATERIAL_NODE_GROUP_NAME,
MATERIAL_COLOR_SPACE,
SHADELESS_NODE_GROUP_NAME,
SURFACE_MATERIAL_NODE_GROUP_NAME,
add_depth_plane_processing,
create_material,
@@ -133,7 +135,6 @@ def main():
foreground,
1.0,
displacement_image=depth,
foreground_image=foreground,
)
mesh.materials.append(material)
material_group = bpy.data.node_groups[DEPTH_PLANE_MATERIAL_NODE_GROUP_NAME]
@@ -232,6 +233,47 @@ def main():
for node in material.node_tree.nodes
if node.bl_idname == "ShaderNodeTexImage"
)
shadeless_material = create_material(
"Shadeless",
foreground,
1.0,
shadeless=True,
)
shadeless_group = bpy.data.node_groups[SHADELESS_NODE_GROUP_NAME]
shadeless_nodes = [
node
for node in shadeless_material.node_tree.nodes
if node.bl_idname == "ShaderNodeGroup"
and node.node_tree == shadeless_group
]
assert len(shadeless_nodes) == 1
assert not any(
node.bl_idname == "ShaderNodeEmission"
for node in shadeless_material.node_tree.nodes
)
assert [
item.name
for item in shadeless_group.interface.items_tree
if item.item_type == "SOCKET" and item.in_out == "INPUT"
] == ["Color"]
assert [
item.name
for item in shadeless_group.interface.items_tree
if item.item_type == "SOCKET" and item.in_out == "OUTPUT"
] == ["Shader"]
assert {
node.label: node.operation
for node in shadeless_group.nodes
if node.bl_idname == "ShaderNodeMath"
} == {
"Bounce Count": "SUBTRACT",
"Camera or Refracted": "SUBTRACT",
"Limit Reflections": "SUBTRACT",
"Camera Ray to Glossy": "MULTIPLY",
"Shadow or Reflection?": "MAXIMUM",
"Shadow, Reflect or Refract?": "MAXIMUM",
}
assert foreground.colorspace_settings.name == MATERIAL_COLOR_SPACE
surface_material = create_material("Surface", foreground, 1.0)
surface_group = bpy.data.node_groups[SURFACE_MATERIAL_NODE_GROUP_NAME]
surface_group_nodes = [

View File

@@ -1,6 +1,8 @@
import sys
import tempfile
from pathlib import Path
from types import SimpleNamespace
from unittest.mock import patch
import bpy
import numpy as np
@@ -13,13 +15,81 @@ sys.path.insert(
str(next((PROJECT_ROOT / "src" / "snapform" / "wheels").glob("blendjob-*.whl"))),
)
import snapform.blender.depth_plane as depth_plane
from snapform.blender.depth_plane import (
MOGE_DEPTH_PLANE_NODE_GROUP_NAME,
MOGE_MATERIAL_NODE_GROUP_NAME,
build_moge2_depth_plane,
create_color_image,
get_modifier_input,
modifier_input_identifier,
)
def validate_single_frame_source_types():
with tempfile.TemporaryDirectory() as temporary:
directory = Path(temporary)
color_path = directory / "color.0001.png"
seed = bpy.data.images.new("Color Seed", width=2, height=2, alpha=True)
seed.pixels[:] = [0.2, 0.4, 0.8, 1.0] * 4
seed.filepath_raw = str(color_path)
seed.file_format = "PNG"
seed.save()
bpy.data.images.remove(seed, do_unlink=True)
still_source_image = bpy.data.images.load(
str(color_path), check_existing=False
)
still_source = bpy.data.objects.new("Still Source", None)
still_source.empty_display_type = "IMAGE"
still_source.data = still_source_image
bpy.context.collection.objects.link(still_source)
still_color = create_color_image(
still_source, directory, assign_to_source=False
)
assert still_color.source == "FILE"
animated_source = SimpleNamespace(
data=SimpleNamespace(
source="SEQUENCE",
name="Sequence Source.png",
use_fake_user=False,
),
image_user=None,
)
sequence_color = create_color_image(
animated_source, directory, assign_to_source=False
)
assert sequence_color.source == "SEQUENCE"
depth = np.ones((1, 2, 2), dtype=np.float32)
mask = np.ones((1, 2, 2), dtype=bool)
intrinsics = np.array(
[[[1.0, 0.0, 0.5], [0.0, 1.0, 0.5], [0.0, 0.0, 1.0]]],
dtype=np.float32,
)
path = directory / "single-frame-results.npz"
np.savez(path, depth=depth, mask=mask, intrinsics=intrinsics)
with patch.object(depth_plane, "is_animated_image", return_value=True):
plane = build_moge2_depth_plane(
bpy.context,
still_source,
path,
delete_source=False,
color_image=sequence_color,
data_frames_directory=directory,
)
textures = [
node.image
for node in plane.data.materials[0].node_tree.nodes
if node.bl_idname == "ShaderNodeTexImage"
]
assert sequence_color in textures
depth_images = [image for image in textures if image != sequence_color]
assert len(depth_images) == 1
assert depth_images[0].source == "SEQUENCE"
def main():
bpy.ops.wm.read_factory_settings(use_empty=True)
image = bpy.data.images.new("MoGe Test", width=64, height=48, alpha=True)
@@ -45,11 +115,14 @@ def main():
mask=mask,
intrinsics=intrinsics,
)
material_image = image.copy()
material_image.name = "MoGe Test.color"
plane = build_moge2_depth_plane(
bpy.context,
source,
path,
delete_source=True,
color_image=material_image,
)
assert plane.modifiers[0].node_group.name == MOGE_DEPTH_PLANE_NODE_GROUP_NAME
assert MOGE_MATERIAL_NODE_GROUP_NAME in bpy.data.node_groups
@@ -73,6 +146,9 @@ def main():
assert sockets["Background Depth"].structure_type == "SINGLE"
assert sockets["Background Depth"].subtype == "DISTANCE"
assert sockets["Background Depth"].default_value == 1.0
assert sockets["Color Image"].hide_in_modifier
color_identifier = modifier_input_identifier(geometry_group, "Color Image")
assert get_modifier_input(plane.modifiers[0], color_identifier) == material_image
material_group = bpy.data.node_groups[MOGE_MATERIAL_NODE_GROUP_NAME]
assert not any(node.bl_idname == "ShaderNodeNormalMap" for node in material_group.nodes)
assert any(node.bl_idname == "ShaderNodeBump" for node in material_group.nodes)
@@ -84,14 +160,90 @@ 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)
static_depth = next(
node.image
for node in image_textures
if node.image.name == "MoGe Test.depth"
)
assert static_depth.source != "SEQUENCE"
geometry_textures = [
node
for node in geometry_group.nodes
if node.bl_idname == "GeometryNodeImageTexture"
]
assert geometry_textures
assert len(geometry_textures) == 2
assert all(node.extension == "EXTEND" for node in geometry_textures)
color_texture = next(
node
for node in geometry_textures
if any(
link.to_node == node
and link.to_socket == node.inputs["Image"]
and link.from_socket.name == "Color Image"
for link in geometry_group.links
)
)
depth_texture = next(node for node in geometry_textures if node != color_texture)
assert color_texture.interpolation == "Closest"
assert depth_texture.interpolation == "Linear"
color_frame_link = next(
link
for link in geometry_group.links
if link.to_node == color_texture
and link.to_socket == color_texture.inputs["Frame"]
)
depth_frame_link = next(
link
for link in geometry_group.links
if link.to_node == depth_texture
and link.to_socket == depth_texture.inputs["Frame"]
)
assert color_frame_link.from_socket == depth_frame_link.from_socket
alpha_compare = next(
node
for node in geometry_group.nodes
if node.bl_idname == "FunctionNodeCompare"
and node.operation == "GREATER_THAN"
and any(
link.from_node == color_texture
and link.from_socket == color_texture.outputs["Alpha"]
and link.to_node == node
for link in geometry_group.links
)
)
masked_depth = next(
node
for node in geometry_group.nodes
if node.bl_idname == "ShaderNodeMix"
and any(
link.from_node == alpha_compare and link.to_node == node
for link in geometry_group.links
)
)
assert masked_depth.data_type == "FLOAT"
masked_sources = {
link.from_node
for link in geometry_group.links
if link.to_node == masked_depth
}
assert alpha_compare in masked_sources
assert any(
node.bl_idname == "ShaderNodeMath" and node.operation == "MAXIMUM"
for node in masked_sources
)
assert any(
node.bl_idname == "ShaderNodeMath"
and node.operation == "MULTIPLY"
and node.inputs[1].default_value == -1.0
for node in masked_sources
)
assert any(
link.from_node == masked_depth
and link.to_node.bl_idname == "ShaderNodeMath"
and link.to_node.operation == "MULTIPLY"
for link in geometry_group.links
)
validate_single_frame_source_types()
print("MoGe-2 depth plane validation passed")

View File

@@ -62,7 +62,7 @@ def main():
)
image = bpy.data.images.new("MoGe Surface Test", width=width, height=height)
foreground = bpy.data.images.new(
"MoGe Surface Test.foreground",
"MoGe Surface Test.color",
width=width,
height=height,
)
@@ -197,7 +197,8 @@ def main():
node for node in surface.data.materials[0].node_tree.nodes
if node.bl_idname == "ShaderNodeTexImage" and node.image == foreground
)
assert color_texture.image.name == "MoGe Surface Test.foreground"
assert color_texture.image.name == "MoGe Surface Test.color"
assert color_texture.image.colorspace_settings.name == "AgX Base sRGB"
normal_texture = next(
node for node in surface.data.materials[0].node_tree.nodes
if node.bl_idname == "ShaderNodeTexImage"
@@ -220,6 +221,8 @@ def validate_real_result(results_path, image_path):
normal_path = results_path.with_name("normal.png")
if not normal_path.is_file():
raise ValueError(f"Surface normal texture is missing: {normal_path}")
color_image = bpy.data.images.load(str(image_path), check_existing=False)
color_image.name = f"{image_path.stem}.color"
surface = build_image_mesh(
bpy.context,
source,
@@ -228,6 +231,7 @@ def validate_real_result(results_path, image_path):
True,
False,
True,
color_image=color_image,
normal_path=normal_path,
)
assert len(surface.data.vertices) > 100