Add new node asset scripts for geometry and shader operations

- Implemented `build_mesh_plane.py` to create a geometry node group for mesh planes with customizable parameters.
- Created `build_shadeless.py` for a shader node group that displays color without scene lighting while preserving shadows and reflections.
- Added `validate.py` to validate the integrity of the newly created node groups and their inputs.
- Introduced `validate_cutout.py` to ensure the functionality of cutout node groups and their dependencies.
- Developed `validate_mesh_plane.py` to test the mesh plane creation and its modifier settings.
This commit is contained in:
2026-08-30 00:17:29 +08:00
parent 7d39fe8c5e
commit 32bcd45d7b
168 changed files with 7213 additions and 13121 deletions

File diff suppressed because it is too large Load Diff

View File

@@ -0,0 +1,49 @@
from pathlib import Path
import sys
import bpy
PROJECT_ROOT = Path(__file__).parents[2]
sys.path.insert(0, str(Path(__file__).parent))
sys.path.insert(0, str(PROJECT_ROOT / "src"))
sys.path.insert(
0,
str(next((PROJECT_ROOT / "src" / "anyimage" / "wheels").glob("blendjob-*.whl"))),
)
from build_cutout import (
CUTOUT_NODE_GROUP_NAMES,
build_cutout_modifier_group,
build_node_group,
)
from build_mesh_plane import build_mesh_plane_group
from build_shadeless import build_shadeless_group
from anyimage.operators.common.node_graph import expand_smooth
OUTPUT_PATH = (
PROJECT_ROOT
/ "src"
/ "anyimage"
/ "assets"
/ "O_AnyImage.blend"
)
def main():
bpy.ops.wm.read_factory_settings(use_empty=True)
bpy.context.preferences.filepaths.save_version = 0
build_mesh_plane_group()
build_shadeless_group()
shape_groups = {
shape: build_node_group(shape)
for shape in CUTOUT_NODE_GROUP_NAMES
}
build_cutout_modifier_group(shape_groups, expand_smooth)
OUTPUT_PATH.parent.mkdir(parents=True, exist_ok=True)
bpy.ops.wm.save_as_mainfile(filepath=str(OUTPUT_PATH), check_existing=False)
if __name__ == "__main__":
main()

File diff suppressed because it is too large Load Diff

View File

@@ -1,3 +1,5 @@
"""Build the Mesh Plane Geometry Nodes group."""
import bpy

View File

@@ -0,0 +1,77 @@
"""Build the Shadeless shader node group."""
import bpy
SHADELESS_GROUP_NAME = "O Shadeless"
def build_shadeless_group():
group = bpy.data.node_groups.new(SHADELESS_GROUP_NAME, "ShaderNodeTree")
group.interface.new_socket(
name="Shader", in_out="OUTPUT", socket_type="NodeSocketShader"
)
group.interface.new_socket(
name="Color", in_out="INPUT", socket_type="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.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.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.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.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.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.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

View File

@@ -0,0 +1,79 @@
from pathlib import Path
import sys
import bpy
PROJECT_ROOT = Path(__file__).parents[2]
sys.path.insert(0, str(PROJECT_ROOT / "src"))
sys.path.insert(
0,
str(next((PROJECT_ROOT / "src" / "anyimage" / "wheels").glob("blendjob-*.whl"))),
)
from anyimage.operators.common.node_asset import (
load_asset_node_group,
load_mesh_plane_node_group,
load_shadeless_node_group,
)
from anyimage.operators.cutout_tool.shape_types import (
CUTOUT_NODE_GROUP_NAME,
CUTOUT_NODE_GROUP_NAMES,
)
def input_names(group):
return [
item.name
for item in group.interface.items_tree
if item.item_type == "SOCKET" and item.in_out == "INPUT"
]
def main():
bpy.ops.wm.read_factory_settings(use_empty=True)
mesh_plane = load_mesh_plane_node_group()
shadeless = load_shadeless_node_group()
cutout = load_asset_node_group(CUTOUT_NODE_GROUP_NAME)
cutout_groups = {
name: bpy.data.node_groups[name]
for name in CUTOUT_NODE_GROUP_NAMES.values()
}
assert mesh_plane.name == "O Mesh Plane"
assert mesh_plane.bl_idname == "GeometryNodeTree"
assert mesh_plane.is_modifier
assert input_names(mesh_plane) == [
"Geometry",
"Subdivide",
"Thickness",
"Material",
]
assert shadeless.name == "O Shadeless"
assert shadeless.bl_idname == "ShaderNodeTree"
assert input_names(shadeless) == ["Color"]
assert cutout.name == CUTOUT_NODE_GROUP_NAME
assert cutout.bl_idname == "GeometryNodeTree"
assert cutout.is_modifier
assert input_names(cutout)[:3] == ["Geometry", "Shape", "Thickness"]
assert set(cutout_groups) == set(CUTOUT_NODE_GROUP_NAMES.values())
for group in cutout_groups.values():
assert group.bl_idname == "GeometryNodeTree"
assert not group.is_modifier
assert input_names(group)[0] == "Geometry"
public_groups = {mesh_plane, shadeless, cutout}
loaded = {*public_groups, *cutout_groups.values()}
assert all(group.use_fake_user for group in public_groups)
assert all(group.users > 0 for group in cutout_groups.values())
if bpy.app.version >= (5, 0, 0):
assert all(group.library is not None for group in loaded)
print(
f"Validated Blender {bpy.app.version_string}: "
+ ", ".join(sorted(group.name for group in loaded))
)
if __name__ == "__main__":
main()

View File

@@ -0,0 +1,430 @@
from pathlib import Path
import sys
import bpy
PROJECT_ROOT = Path(__file__).parents[2]
sys.path.insert(0, str(PROJECT_ROOT / "src"))
sys.path.insert(
0,
str(next((PROJECT_ROOT / "src" / "anyimage" / "wheels").glob("blendjob-*.whl"))),
)
from anyimage.operators.cutout_tool import node_assets as cutout_nodes
from anyimage.operators.cutout_tool import shape_types as shapes
from anyimage.operators.common.node_asset import (
modifier_input_identifier,
set_modifier_input,
)
def evaluated_mesh(obj):
evaluated = obj.evaluated_get(bpy.context.evaluated_depsgraph_get())
result = evaluated.to_mesh()
return evaluated, result
def input_names(group):
return [
item.name
for item in group.interface.items_tree
if item.item_type == "SOCKET" and item.in_out == "INPUT"
]
def grid_object(name):
vertices = [
(0.0, float(y), float(z))
for z in range(3)
for y in range(3)
]
faces = []
for z in range(2):
for y in range(2):
a = z * 3 + y
faces.extend(((a, a + 4, a + 1), (a, a + 3, a + 4)))
data = bpy.data.meshes.new(name)
data.from_pydata(vertices, (), faces)
uv_layer = data.uv_layers.new(name="UVMap")
for loop in data.loops:
coordinate = data.vertices[loop.vertex_index].co
uv_layer.data[loop.index].uv = (coordinate.y * 0.5, coordinate.z * 0.5)
obj = bpy.data.objects.new(name, data)
bpy.context.scene.collection.objects.link(obj)
return obj
def add_modifier(obj, shape, thickness):
group = cutout_nodes.cutout_node_group(shape)
modifier = obj.modifiers.new(group.name, "NODES")
modifier.node_group = group
for name, value in (
("Shape", shapes.CUTOUT_SHAPE_INDICES[shape]),
("Cleanup", 0.0),
("Smooth", 0.0),
("Smooth Iterations", 0),
("Thickness", thickness),
):
set_modifier_input(
modifier,
modifier_input_identifier(group, name),
value,
)
return modifier
def main():
bpy.ops.wm.read_factory_settings(use_empty=True)
wrapper = cutout_nodes.cutout_node_group("CUTOUT")
assert all(
cutout_nodes.cutout_node_group(shape) == wrapper
for shape in shapes.CUTOUT_SHAPES
)
assert wrapper.name == shapes.CUTOUT_NODE_GROUP_NAME
assert wrapper.is_modifier
groups = {
shape: bpy.data.node_groups[name]
for shape, name in shapes.CUTOUT_NODE_GROUP_NAMES.items()
}
assert all(not group.is_modifier for group in groups.values())
menu_switch = next(
node for node in wrapper.nodes
if node.bl_idname == "GeometryNodeMenuSwitch"
)
assert [item.name for item in menu_switch.enum_items] == list(
shapes.CUTOUT_SHAPE_LABELS.values()
)
assert any(
link.from_socket.name == "Shape"
and link.to_node == menu_switch
and link.to_socket.name == "Menu"
for link in wrapper.links
)
child_nodes = {
node.node_tree: node
for node in wrapper.nodes
if node.bl_idname == "GeometryNodeGroup"
}
assert set(child_nodes) == set(groups.values())
for shape, group in groups.items():
assert any(
link.from_node == child_nodes[group]
and link.to_node == menu_switch
and link.to_socket.name == shapes.CUTOUT_SHAPE_LABELS[shape]
for link in wrapper.links
)
assert input_names(wrapper) == [
"Geometry",
"Shape",
"Thickness",
"Depth Scale",
"Double Sided",
"Smooth Iterations",
"Cleanup",
"Smooth",
"Reference Depth",
"Depth Direction",
"Uniform Scale",
"Depth Image",
]
expected_inputs = {
"CUTOUT": ["Geometry", "Thickness"],
"BALLOON": ["Geometry", "Thickness"],
"DEPTH_BALLOON": [
"Geometry",
"Thickness",
"Depth Scale",
"Double Sided",
"Reference Depth",
"Depth Direction",
"Uniform Scale",
"Depth Image",
],
"DEPTH_SURFACE": [
"Geometry",
"Thickness",
"Depth Scale",
"Reference Depth",
"Uniform Scale",
"Depth Image",
],
}
for shape, group in groups.items():
assert input_names(group) == expected_inputs[shape]
dependencies = {}
for link in wrapper.links:
if link.from_node.bl_idname != "NodeGroupInput":
continue
if link.to_node.bl_idname != "GeometryNodeGroup":
continue
shape = next(
shape
for shape, group in groups.items()
if group == link.to_node.node_tree
)
dependencies.setdefault(link.from_socket.name, set()).add(shape)
assert dependencies == {
"Thickness": set(shapes.CUTOUT_SHAPES),
"Depth Scale": {"DEPTH_BALLOON", "DEPTH_SURFACE"},
"Double Sided": {"DEPTH_BALLOON"},
"Reference Depth": {"DEPTH_BALLOON", "DEPTH_SURFACE"},
"Depth Direction": {"DEPTH_BALLOON"},
"Uniform Scale": {"DEPTH_BALLOON", "DEPTH_SURFACE"},
"Depth Image": {"DEPTH_BALLOON", "DEPTH_SURFACE"},
}
repeat_counts = {
shape: sum(
node.bl_idname == "GeometryNodeRepeatInput"
for node in group.nodes
)
for shape, group in groups.items()
}
assert repeat_counts == {
"CUTOUT": 1,
"BALLOON": 0,
"DEPTH_BALLOON": 0,
"DEPTH_SURFACE": 1,
}
assert sum(
node.bl_idname == "GeometryNodeRepeatInput"
for node in wrapper.nodes
) == 1
assert sum(
node.bl_idname == "GeometryNodeInputMeshIsland"
for node in wrapper.nodes
) == 1
assert not any(
node.bl_idname == "GeometryNodeAttributeStatistic"
for node in wrapper.nodes
)
assert all(
not any(
node.bl_idname == "GeometryNodeInputMeshIsland"
for node in group.nodes
)
for group in groups.values()
)
assert any(
node.bl_idname == "GeometryNodeBlurAttribute"
for node in wrapper.nodes
)
smooth = next(
item
for item in wrapper.interface.items_tree
if item.item_type == "SOCKET" and item.name == "Smooth"
)
iterations = next(
item
for item in wrapper.interface.items_tree
if item.item_type == "SOCKET" and item.name == "Smooth Iterations"
)
cleanup = next(
item
for item in wrapper.interface.items_tree
if item.item_type == "SOCKET" and item.name == "Cleanup"
)
assert cleanup.default_value == 0.0
assert cleanup.subtype == "NONE"
assert smooth.default_value == 1.0
assert iterations.default_value == 0
assert iterations.min_value == 0
assert getattr(iterations.parent, "name", "") == ""
assert all(
not any(
node.bl_idname == "GeometryNodeBlurAttribute"
for node in group.nodes
)
for group in groups.values()
)
for group in (wrapper, *groups.values()):
assert all(not node.label for node in group.nodes)
assert all(
link.from_node.location.x <= link.to_node.location.x
for link in group.links
)
group_nodes = list(group.nodes)
assert len({tuple(node.location) for node in group_nodes}) == len(group_nodes)
for index, node in enumerate(group_nodes):
for other in group_nodes[index + 1:]:
assert not (
abs(node.location.x - other.location.x) < 150.0
and abs(node.location.y - other.location.y) < 100.0
)
for group_input in (
node for node in group_nodes if node.bl_idname == "NodeGroupInput"
):
assert all(
output.is_linked or output.hide
for output in group_input.outputs
if output.name
)
for item in group.interface.items_tree:
if item.item_type == "SOCKET" and item.in_out == "INPUT":
assert item.structure_type == "SINGLE"
surface = grid_object("Surface Validation")
add_modifier(surface, "CUTOUT", 2.4)
evaluated, result = evaluated_mesh(surface)
try:
assert len(result.vertices) == 34
assert len(result.polygons) == 40
side_faces = [
polygon for polygon in result.polygons
if abs(polygon.normal.x) < 0.1
]
assert len(side_faces) == 24
finally:
evaluated.to_mesh_clear()
balloon = grid_object("Balloon Validation")
profile = balloon.data.attributes.new(
shapes.BALLOON_ATTRIBUTE_NAME, "FLOAT", "POINT"
)
profile.data.foreach_set(
"value",
(0.0, 0.0, 0.0, 0.0, 1.0, 0.0, 0.0, 0.0, 0.0),
)
modifier = add_modifier(balloon, "BALLOON", 1.0)
evaluated, result = evaluated_mesh(balloon)
try:
initial_faces = len(result.polygons)
initial_extent = max(abs(vertex.co.x) for vertex in result.vertices)
finally:
evaluated.to_mesh_clear()
set_modifier_input(
modifier,
modifier_input_identifier(modifier.node_group, "Thickness"),
2.0,
)
balloon.update_tag(refresh={"DATA"})
bpy.context.view_layer.update()
evaluated, result = evaluated_mesh(balloon)
try:
assert len(result.polygons) == initial_faces
extent = max(abs(vertex.co.x) for vertex in result.vertices)
assert abs(extent - initial_extent * 2.0) < 1e-5
finally:
evaluated.to_mesh_clear()
depth_balloon = grid_object("Depth Balloon Metric Validation")
profile = depth_balloon.data.attributes.new(
shapes.BALLOON_ATTRIBUTE_NAME, "FLOAT", "POINT"
)
profile.data.foreach_set("value", (1.0,) * 9)
modifier = add_modifier(depth_balloon, "DEPTH_BALLOON", 0.0)
point_image = bpy.data.images.new(
"Depth Balloon Points",
width=3,
height=3,
alpha=True,
float_buffer=True,
)
point_image.colorspace_settings.name = "Non-Color"
point_image.alpha_mode = "CHANNEL_PACKED"
pixels = []
for z in range(3):
for y in range(3):
if (y, z) == (1, 1):
depth = 1.5
elif (y, z) == (0, 0):
depth = 2.5
else:
depth = 2.0
pixels.extend((0.0, 0.0, depth, 0.0))
point_image.pixels.foreach_set(pixels)
for name, value in (
("Depth Image", point_image),
("Depth Scale", 1.0),
("Uniform Scale", 1.0),
("Reference Depth", 2.0),
("Depth Direction", (0.0, 0.0, 1.0)),
):
set_modifier_input(
modifier,
modifier_input_identifier(modifier.node_group, name),
value,
)
depth_balloon.update_tag(refresh={"DATA"})
bpy.context.view_layer.update()
evaluated, result = evaluated_mesh(depth_balloon)
try:
x_values = [vertex.co.x for vertex in result.vertices]
assert abs(min(x_values) + 0.5) < 1e-5
assert abs(max(x_values)) < 1e-5
finally:
evaluated.to_mesh_clear()
pixels[(1 * 3 + 1) * 4 + 3] = -1.0
point_image.pixels.foreach_set(pixels)
point_image.update()
depth_balloon.update_tag(refresh={"DATA"})
bpy.context.view_layer.update()
evaluated, result = evaluated_mesh(depth_balloon)
try:
x_values = [vertex.co.x for vertex in result.vertices]
assert abs(min(x_values) + 0.5) < 1e-5
assert abs(max(x_values)) < 1e-5
finally:
evaluated.to_mesh_clear()
depth_surface = grid_object("Depth Surface Projection Validation")
modifier = add_modifier(depth_surface, "DEPTH_SURFACE", 0.0)
point_image = bpy.data.images.new(
"Depth Surface Points",
width=3,
height=3,
alpha=True,
float_buffer=True,
)
point_image.colorspace_settings.name = "Non-Color"
point_image.alpha_mode = "CHANNEL_PACKED"
pixels = []
for z in range(3):
for y in range(3):
pixels.extend((-y * 0.5, -z * 0.5, 2.0 + y * 0.25, 0.0))
point_image.pixels.foreach_set(pixels)
for name, value in (
("Depth Image", point_image),
("Depth Scale", 1.0),
("Uniform Scale", 1.0),
("Reference Depth", 2.0),
("Smooth Iterations", 0),
):
set_modifier_input(
modifier,
modifier_input_identifier(modifier.node_group, name),
value,
)
depth_surface.update_tag(refresh={"DATA"})
bpy.context.view_layer.update()
evaluated, result = evaluated_mesh(depth_surface)
try:
coordinates = [tuple(vertex.co) for vertex in result.vertices]
assert all(
all(abs(actual - expected) < 1e-5 for actual, expected in zip(point, target))
for point, target in zip(
coordinates,
(
(y * 0.25, y * 0.5, z * 0.5)
for z in range(3)
for y in range(3)
),
)
)
finally:
evaluated.to_mesh_clear()
print(
f"Validated Blender {bpy.app.version_string}: "
f"{wrapper.name} wrapping "
f"{', '.join(group.name for group in groups.values())}"
)
if __name__ == "__main__":
main()

View File

@@ -4,15 +4,19 @@ import sys
import bpy
PROJECT_ROOT = Path(__file__).parents[1]
PROJECT_ROOT = Path(__file__).parents[2]
sys.path.insert(0, str(PROJECT_ROOT / "src"))
sys.path.insert(
0,
str(next((PROJECT_ROOT / "src" / "anyimage" / "wheels").glob("blendjob-*.whl"))),
)
from anyimage.operators.material import (
from anyimage.operators.common.node_asset import (
get_modifier_input,
modifier_input_identifier,
set_modifier_input,
)
from anyimage.operators.mesh_plane import (
from anyimage.operators.convert_to_plane.mesh_plane import (
add_mesh_plane_processing,
create_mesh_plane_mesh,
)

View File

@@ -1,165 +0,0 @@
from pathlib import Path
import sys
import bpy
PROJECT_ROOT = Path(__file__).parents[1]
sys.path.insert(0, str(PROJECT_ROOT / "src"))
sys.path.insert(
0,
str(next((PROJECT_ROOT / "src" / "anyimage" / "wheels").glob("blendjob-*.whl"))),
)
from anyimage.operators import mesh
from anyimage.operators.material import modifier_input_identifier, set_modifier_input
def evaluated_mesh(obj):
evaluated = obj.evaluated_get(bpy.context.evaluated_depsgraph_get())
result = evaluated.to_mesh()
return evaluated, result
def grid_object(name):
vertices = [
(0.0, float(y), float(z))
for z in range(3)
for y in range(3)
]
faces = []
for z in range(2):
for y in range(2):
a = z * 3 + y
faces.extend(((a, a + 4, a + 1), (a, a + 3, a + 4)))
data = bpy.data.meshes.new(name)
data.from_pydata(vertices, (), faces)
obj = bpy.data.objects.new(name, data)
bpy.context.scene.collection.objects.link(obj)
return obj
def add_modifier(obj, shape, thickness):
group = mesh._mesh_node_group(shape)
modifier = obj.modifiers.new(group.name, "NODES")
modifier.node_group = group
set_modifier_input(
modifier,
modifier_input_identifier(group, "Cleanup"),
0.0,
)
set_modifier_input(
modifier,
modifier_input_identifier(group, "Thickness"),
thickness,
)
return modifier
def main():
bpy.ops.wm.read_factory_settings(use_empty=True)
groups = {
shape: mesh._mesh_node_group(shape)
for shape in (
mesh.CUTOUT,
mesh.BALLOON,
mesh.DEPTH_BALLOON,
mesh.DEPTH_SURFACE,
)
}
assert [groups[shape].name for shape in groups] == [
"O Image Surface",
"O Image Balloon",
"O Image Depth Balloon",
"O Image Depth Surface",
]
repeat_counts = {
shape: sum(
node.bl_idname == "GeometryNodeRepeatInput"
for node in group.nodes
)
for shape, group in groups.items()
}
assert repeat_counts == {
mesh.CUTOUT: 1,
mesh.BALLOON: 0,
mesh.DEPTH_BALLOON: 0,
mesh.DEPTH_SURFACE: 2,
}
for group in groups.values():
assert group.is_modifier
assert all(not node.label for node in group.nodes)
assert all(
link.from_node.location.x <= link.to_node.location.x
for link in group.links
)
group_nodes = list(group.nodes)
assert len({tuple(node.location) for node in group_nodes}) == len(group_nodes)
for index, node in enumerate(group_nodes):
for other in group_nodes[index + 1:]:
assert not (
abs(node.location.x - other.location.x) < 150.0
and abs(node.location.y - other.location.y) < 100.0
)
for group_input in (
node for node in group_nodes if node.bl_idname == "NodeGroupInput"
):
assert all(
output.is_linked or output.hide
for output in group_input.outputs
if output.name
)
for item in group.interface.items_tree:
if item.item_type == "SOCKET" and item.in_out == "INPUT":
assert item.structure_type == "SINGLE"
surface = grid_object("Surface Validation")
add_modifier(surface, mesh.CUTOUT, 2.4)
evaluated, result = evaluated_mesh(surface)
try:
assert len(result.vertices) == 34
assert len(result.polygons) == 40
side_faces = [
polygon for polygon in result.polygons
if abs(polygon.normal.x) < 0.1
]
assert len(side_faces) == 24
finally:
evaluated.to_mesh_clear()
balloon = grid_object("Balloon Validation")
profile = balloon.data.attributes.new(mesh.BALLOON_ATTRIBUTE, "FLOAT", "POINT")
profile.data.foreach_set(
"value",
(0.0, 0.0, 0.0, 0.0, 1.0, 0.0, 0.0, 0.0, 0.0),
)
modifier = add_modifier(balloon, mesh.BALLOON, 1.0)
evaluated, result = evaluated_mesh(balloon)
try:
initial_faces = len(result.polygons)
initial_extent = max(abs(vertex.co.x) for vertex in result.vertices)
finally:
evaluated.to_mesh_clear()
set_modifier_input(
modifier,
modifier_input_identifier(modifier.node_group, "Thickness"),
2.0,
)
balloon.update_tag(refresh={"DATA"})
bpy.context.view_layer.update()
evaluated, result = evaluated_mesh(balloon)
try:
assert len(result.polygons) == initial_faces
extent = max(abs(vertex.co.x) for vertex in result.vertices)
assert abs(extent - initial_extent * 2.0) < 1e-5
finally:
evaluated.to_mesh_clear()
print(
f"Validated Blender {bpy.app.version_string}: "
f"{', '.join(group.name for group in groups.values())}"
)
if __name__ == "__main__":
main()

View File

@@ -1,410 +0,0 @@
from pathlib import Path
import sys
import bpy
PROJECT_ROOT = Path(__file__).parents[1]
sys.path.insert(0, str(PROJECT_ROOT / "src"))
sys.path.insert(
0,
str(next((PROJECT_ROOT / "src" / "anyimage" / "wheels").glob("blendjob-*.whl"))),
)
from anyimage.operators.material import (
DEPTH_PLANE_MATERIAL_NODE_GROUP_NAME,
SHADELESS_NODE_GROUP_NAME,
SURFACE_MATERIAL_NODE_GROUP_NAME,
create_material,
get_modifier_input,
load_depth_plane_node_group,
material_color_space_name,
modifier_input_driver_path,
modifier_input_identifier,
set_modifier_input,
)
from anyimage.operators.depth_plane.build import add_depth_plane_processing
from anyimage.operators.surface import load_geometry_node_group
def main():
bpy.ops.wm.read_factory_settings(use_empty=True)
group = load_depth_plane_node_group()
geometry_textures = [
node
for node in group.nodes
if node.bl_idname == "GeometryNodeImageTexture"
]
assert geometry_textures
assert all(node.extension == "EXTEND" for node in geometry_textures)
background_depth = next(
item
for item in group.interface.items_tree
if item.item_type == "SOCKET"
and item.in_out == "INPUT"
and item.name == "Background Depth"
)
assert background_depth.subtype == "DISTANCE"
assert background_depth.structure_type == "SINGLE"
assert background_depth.default_value == 1.0
subdivision = next(
item
for item in group.interface.items_tree
if item.item_type == "SOCKET"
and item.in_out == "INPUT"
and item.name == "Subdivision"
)
assert subdivision.default_value == 6
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
if item.item_type == "SOCKET"
and item.in_out == "INPUT"
and item.name == "Geometry"
)
assert geometry_input.structure_type == "SINGLE"
all_inputs = [
(item.name, getattr(item.parent, "name", None), item.hide_in_modifier)
for item in group.interface.items_tree
if item.item_type == "SOCKET"
and item.in_out == "INPUT"
]
visible_inputs = [name for name, parent, hidden in all_inputs if not hidden]
assert visible_inputs[:6] == [
"Geometry",
"Displacement",
"Subdivision",
"Depth Scale",
"Background Depth",
]
assert not getattr(background_depth.parent, "name", "")
assert not any(
item.name == "Remove Background"
for item in group.interface.items_tree
)
depth_scale = next(
item
for item in group.interface.items_tree
if item.item_type == "SOCKET"
and item.in_out == "INPUT"
and item.name == "Depth Scale"
)
assert depth_scale.default_value == 1.0
assert depth_scale.min_value < 0.0
assert depth_scale.structure_type == "SINGLE"
stores = {
node.inputs["Name"].default_value: node
for node in group.nodes
if node.bl_idname == "GeometryNodeStoreNamedAttribute"
}
attribute_names = (
"o_material_displacement",
"o_background_depth",
"o_depth_scale",
)
for name in attribute_names:
assert stores[name].domain == "POINT"
assert not any(
node.bl_idname == "GeometryNodeGeometryToInstance"
for node in group.nodes
)
mesh = bpy.data.meshes.new("Depth Plane")
mesh.from_pydata(
[(-1, -1, 0), (1, -1, 0), (-1, 1, 0), (1, 1, 0)],
[],
[(0, 1, 3, 2)],
)
obj = bpy.data.objects.new("Depth Plane", mesh)
bpy.context.scene.collection.objects.link(obj)
depth = bpy.data.images.new("Depth", width=2, height=2, float_buffer=True)
depth.pixels[:] = [0.25, 0.25, 0.25, 1.0] * 4
foreground = bpy.data.images.new("Foreground", width=2, height=2)
foreground.pixels[:] = [0.8, 0.2, 0.1, 1.0] * 4
foreground.alpha_mode = "CHANNEL_PACKED"
material = create_material(
"Depth Plane",
foreground,
1.0,
displacement_image=depth,
)
mesh.materials.append(material)
material_group = bpy.data.node_groups[DEPTH_PLANE_MATERIAL_NODE_GROUP_NAME]
interface_inputs = [
item.name
for item in material_group.interface.items_tree
if item.item_type == "SOCKET"
and item.in_out == "INPUT"
]
interface_outputs = [
item.name
for item in material_group.interface.items_tree
if item.item_type == "SOCKET"
and item.in_out == "OUTPUT"
]
assert interface_inputs == [
"Color",
"Alpha",
"Depth",
"Bump Scale",
]
assert interface_outputs == [
"Color",
"Roughness",
"Alpha",
"Normal",
"Displacement",
]
depth_input = next(
item
for item in material_group.interface.items_tree
if item.item_type == "SOCKET"
and item.in_out == "INPUT"
and item.name == "Depth"
)
assert depth_input.socket_type == "NodeSocketFloat"
bump_scale = next(
item
for item in material_group.interface.items_tree
if item.item_type == "SOCKET"
and item.in_out == "INPUT"
and item.name == "Bump Scale"
)
assert bump_scale.default_value == 1.0
assert bump_scale.min_value < 0.0
material_group_nodes = [
node
for node in material.node_tree.nodes
if node.bl_idname == "ShaderNodeGroup"
and node.node_tree == material_group
]
assert len(material_group_nodes) == 1
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) == 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
)
forbidden_outer_nodes = {
"ShaderNodeAttribute",
"ShaderNodeBump",
"ShaderNodeDisplacement",
"ShaderNodeFloatCurve",
"ShaderNodeMath",
"ShaderNodeMix",
}
assert not any(
node.bl_idname in forbidden_outer_nodes
for node in material.node_tree.nodes
)
assert all(
node.extension == "EXTEND"
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_name(
bpy.context.scene
)
surface_material = create_material("Surface", foreground, 1.0)
surface_group = bpy.data.node_groups[SURFACE_MATERIAL_NODE_GROUP_NAME]
surface_group_nodes = [
node
for node in surface_material.node_tree.nodes
if node.bl_idname == "ShaderNodeGroup"
and node.node_tree == surface_group
]
assert len(surface_group_nodes) == 1
assert surface_group_nodes[0].name == "O Image Layer"
surface_inputs = [
item.name
for item in surface_group.interface.items_tree
if item.item_type == "SOCKET"
and item.in_out == "INPUT"
]
assert surface_inputs == [
"Color",
"Alpha",
"Normal",
"Normal Scale",
"Object Space",
"Bump Scale",
]
normal_input = next(
item
for item in surface_group.interface.items_tree
if item.item_type == "SOCKET"
and item.in_out == "INPUT"
and item.name == "Normal"
)
assert tuple(normal_input.default_value) == (0.5, 0.5, 1.0, 1.0)
normal_scale = next(
item
for item in surface_group.interface.items_tree
if item.item_type == "SOCKET"
and item.in_out == "INPUT"
and item.name == "Normal Scale"
)
assert normal_scale.default_value == 1.0
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
)
assert not any(
node.bl_idname in forbidden_outer_nodes
for node in surface_material.node_tree.nodes
)
assert all(
node.extension == "EXTEND"
for node in surface_material.node_tree.nodes
if node.bl_idname == "ShaderNodeTexImage"
)
modifier = add_depth_plane_processing(
obj,
depth,
foreground,
material,
animate_frame=True,
)
set_modifier_input(
modifier,
modifier_input_identifier(group, "Depth Scale"),
-2.0,
)
for name, expected in (
("Frame Start", 1),
("Frames", 1),
("Cyclic", False),
):
identifier = modifier_input_identifier(group, name)
assert get_modifier_input(modifier, identifier) == expected
assert not any(
node.bl_idname == "GeometryNodeInputSceneTime"
for node in group.nodes
)
frame_identifier = modifier_input_identifier(group, "Frame")
frame_path = modifier_input_driver_path(modifier, frame_identifier)
frame_driver = next(
fcurve
for fcurve in obj.animation_data.drivers
if fcurve.data_path == frame_path
)
assert frame_driver.driver.type == "SCRIPTED"
assert frame_driver.driver.expression == "frame"
image_textures = [
node
for node in group.nodes
if node.bl_idname == "GeometryNodeImageTexture"
]
assert len(image_textures) == 2
assert all(node.inputs["Frame"].is_linked for node in image_textures)
assert image_textures[0].inputs["Frame"].links[0].from_socket == (
image_textures[1].inputs["Frame"].links[0].from_socket
)
floored_modulo = [
node
for node in group.nodes
if node.bl_idname == "ShaderNodeMath"
and node.operation == "FLOORED_MODULO"
]
assert len(floored_modulo) == 1
bpy.context.view_layer.objects.active = obj
obj.select_set(True)
result = bpy.ops.object.modifier_apply(modifier=modifier.name)
assert result == {"FINISHED"}
assert not obj.modifiers
assert obj.type == "MESH"
assert len(obj.data.vertices) > 4
for name in attribute_names:
assert obj.data.attributes[name].domain == "POINT"
assert all(polygon.use_smooth for polygon in obj.data.polygons)
assert material in obj.data.materials[:]
print("Depth plane asset validation passed")
if __name__ == "__main__":
main()

View File

@@ -1,245 +0,0 @@
from pathlib import Path
import sys
import bpy
import numpy as np
PROJECT_ROOT = Path(__file__).parents[1]
sys.path.insert(0, str(PROJECT_ROOT / "src"))
sys.path.insert(
0,
str(next((PROJECT_ROOT / "src" / "anyimage" / "wheels").glob("blendjob-*.whl"))),
)
from anyimage.operators.material import get_modifier_input, set_modifier_input
from anyimage.operators.surface import create_matching_camera, load_geometry_node_group
def set_input(modifier, group, name, value):
item = next(
item
for item in group.interface.items_tree
if item.item_type == "SOCKET"
and item.in_out == "INPUT"
and item.name == name
)
set_modifier_input(modifier, item.identifier, value)
def add_face_attributes(mesh, count):
confidence = mesh.attributes.new("o_confidence", "FLOAT", "FACE")
confidence.data.foreach_set("value", [1.0] * count)
def main():
bpy.ops.wm.read_factory_settings(use_empty=True)
group = load_geometry_node_group()
assert load_geometry_node_group() == group
assert group.is_modifier
geometry_input = next(
item
for item in group.interface.items_tree
if item.item_type == "SOCKET"
and item.in_out == "INPUT"
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)
assert [item.name for item in input_sockets[:4]] == [
"Geometry",
"Iterations",
"Resolution",
"Smooth",
]
iterations = next(item for item in input_sockets if item.name == "Iterations")
assert iterations.default_value == 2
assert iterations.min_value == 0
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)
group_inputs = [
node for node in group.nodes if node.bl_idname == "NodeGroupInput"
]
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
]
assert 1 <= len(visible_outputs) <= 4
uv_stores = [
node
for node in group.nodes
if node.bl_idname == "GeometryNodeStoreNamedAttribute"
and node.inputs["Name"].default_value == "UVMap"
]
assert len(uv_stores) == 1
assert uv_stores[0].data_type == "FLOAT2"
panels = {
item.name: item
for item in group.interface.items_tree
if item.item_type == "PANEL"
}
assert not panels["Culling"].default_closed
assert panels["Options"].default_closed
assert [item.name for item in panels["Culling"].interface_items] == [
"Depth Continuity",
"Confidence",
]
assert [item.name for item in panels["Options"].interface_items] == [
"Fix Topology",
]
smooth = next(
item
for item in group.interface.items_tree
if item.item_type == "SOCKET"
and item.in_out == "INPUT"
and item.name == "Smooth"
)
assert smooth.default_value == 0.5
for name, default_value in (
("Confidence", 0.0),
("Depth Continuity", 50.0),
):
item = next(
item
for item in panels["Culling"].interface_items
if item.name == name
)
assert item.min_value == 0.0
assert item.max_value == 100.0
assert item.default_value == default_value
assert item.subtype == "PERCENTAGE"
if bpy.app.version >= (5, 0, 0):
assert group.library is not None
mesh = bpy.data.meshes.new("Validation Mesh")
mesh.from_pydata(
[(0, 0, 0), (1, 0, 0), (0, 1, 0), (1, 1, 0)],
[],
[(0, 2, 1), (1, 2, 3)],
)
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)
assert "o_confidence" in mesh.attributes
assert not {
"o_background",
"o_background_face",
"o_face_confidence",
"o_confidence_percentile",
}.intersection(mesh.attributes.keys())
obj = bpy.data.objects.new("Validation Object", mesh)
bpy.context.scene.collection.objects.link(obj)
modifier = obj.modifiers.new(name=group.name, type="NODES")
modifier.node_group = group
modifier.show_group_selector = False
assert not modifier.show_group_selector
set_input(modifier, group, "Resolution", 0.000001)
set_input(modifier, group, "Iterations", 0)
set_input(modifier, group, "Fix Topology", 0)
set_input(modifier, group, "Smooth", 0.0)
evaluated = obj.evaluated_get(bpy.context.evaluated_depsgraph_get())
result = evaluated.to_mesh()
try:
assert len(result.polygons) == 2
assert "UVMap" in result.attributes
assert result.attributes["UVMap"].data_type == "FLOAT2"
finally:
evaluated.to_mesh_clear()
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 == "Depth Continuity"
)
assert get_modifier_input(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())
culled_result = culled.to_mesh()
try:
assert len(culled_result.polygons) == 0
assert len(culled_result.vertices) == 0
finally:
culled.to_mesh_clear()
size = 10
dense_vertices = [
(x * 0.1, y * 0.1, 0.0)
for y in range(size)
for x in range(size)
]
dense_faces = []
for y in range(size - 1):
for x in range(size - 1):
a = y * size + x
b = a + 1
c = a + size
d = c + 1
dense_faces.extend(((a, c, b), (b, c, d)))
dense_mesh = bpy.data.meshes.new("Dense Validation Mesh")
dense_mesh.from_pydata(dense_vertices, [], dense_faces)
dense_uv = dense_mesh.uv_layers.new(name="UVMap")
dense_uv.data.foreach_set("uv", [0.0] * (len(dense_mesh.loops) * 2))
add_face_attributes(dense_mesh, len(dense_faces))
dense_object = bpy.data.objects.new("Dense Validation Object", dense_mesh)
bpy.context.scene.collection.objects.link(dense_object)
dense_modifier = dense_object.modifiers.new(name=group.name, type="NODES")
dense_modifier.node_group = group
set_input(dense_modifier, group, "Resolution", 0.22)
dense_evaluated = dense_object.evaluated_get(
bpy.context.evaluated_depsgraph_get()
)
dense_result = dense_evaluated.to_mesh()
try:
assert 4 < len(dense_result.polygons) < len(dense_faces)
assert all(polygon.normal.z < -0.01 for polygon in dense_result.polygons)
finally:
dense_evaluated.to_mesh_clear()
source = bpy.data.objects.new("Validation Source", None)
bpy.context.scene.collection.objects.link(source)
source.location = (2.0, 3.0, 4.0)
bpy.context.view_layer.update()
obj.matrix_world = source.matrix_world.copy()
bpy.context.view_layer.update()
camera = create_matching_camera(
bpy.context,
source,
obj,
"Validation",
np.asarray(
((100.0, 0.0, 50.0), (0.0, 100.0, 50.0), (0.0, 0.0, 1.0))
),
(101, 101),
{"scale_x": 1.0, "scale_z": 1.0, "location": (0.0, 0.0, 2.0)},
)
assert camera.parent == obj
camera_location = tuple(
round(value, 5) for value in camera.matrix_world.translation
)
assert camera_location == (
2.0,
3.0,
6.0,
)
print(f"Validated Blender {bpy.app.version_string}: {group.name}")
if __name__ == "__main__":
main()

View File

@@ -1,249 +0,0 @@
import sys
import tempfile
from pathlib import Path
from types import SimpleNamespace
from unittest.mock import patch
import bpy
import numpy as np
PROJECT_ROOT = Path(__file__).parents[1]
sys.path.insert(0, str(PROJECT_ROOT / "src"))
sys.path.insert(
0,
str(next((PROJECT_ROOT / "src" / "anyimage" / "wheels").glob("blendjob-*.whl"))),
)
from anyimage.operators.image import create_color_image
from anyimage.operators.material import (
MOGE_DEPTH_PLANE_NODE_GROUP_NAME,
MOGE_MATERIAL_NODE_GROUP_NAME,
get_modifier_input,
modifier_input_identifier,
)
from anyimage.operators.depth_plane.build import build_moge2_depth_plane
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)
source = bpy.data.objects.new("MoGe Test", None)
source.empty_display_type = "IMAGE"
source.data = image
bpy.context.collection.objects.link(source)
depth = np.linspace(1.0, 2.0, 48 * 64, dtype=np.float32).reshape(1, 48, 64)
normal = np.zeros((1, 48, 64, 3), dtype=np.float32)
normal[..., 2] = -1.0
mask = np.ones((1, 48, 64), dtype=bool)
mask[:, :5] = False
intrinsics = np.array(
[[[1.0, 0.0, 0.5], [0.0, 1.0, 0.5], [0.0, 0.0, 1.0]]],
dtype=np.float32,
)
with tempfile.TemporaryDirectory() as temporary:
path = Path(temporary) / "results.npz"
np.savez(
path,
depth=depth,
normal=normal,
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
geometry_group = plane.modifiers[0].node_group
inputs = {
item.name
for item in geometry_group.interface.items_tree
if getattr(item, "in_out", None) == "INPUT"
}
assert "Displacement" in inputs
assert "Background Depth" in inputs
assert "Mask Image" not in inputs
assert "Use Mask" not in inputs
sockets = {
item.name: item
for item in geometry_group.interface.items_tree
if getattr(item, "in_out", None) == "INPUT"
}
assert sockets["Subdivision"].default_value == 6
assert sockets["Depth Scale"].structure_type == "SINGLE"
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)
material = plane.data.materials[0]
image_textures = [
node
for node in material.node_tree.nodes
if node.bl_idname == "ShaderNodeTexImage"
]
assert image_textures
assert all(node.extension == "EXTEND" for node in image_textures)
static_depth = next(
node.image
for node in image_textures
if node.image.name == "MoGe Test_depth.exr"
)
assert static_depth.source != "SEQUENCE"
geometry_textures = [
node
for node in geometry_group.nodes
if node.bl_idname == "GeometryNodeImageTexture"
]
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")
if __name__ == "__main__":
main()

View File

@@ -1,298 +0,0 @@
import sys
import tempfile
from pathlib import Path
import bpy
import numpy as np
PROJECT_ROOT = Path(__file__).parents[1]
sys.path.insert(0, str(PROJECT_ROOT / "src"))
sys.path.insert(
0,
str(next((PROJECT_ROOT / "src" / "anyimage" / "wheels").glob("blendjob-*.whl"))),
)
from anyimage.operators.material import (
MOGE_SURFACE_MATERIAL_NODE_GROUP_NAME,
material_color_space_name,
)
from anyimage.operators.surface import MOGE_NODE_GROUP_NAME, build_image_mesh
from anyimage.server.geometry import GeometryFrame, build_surface
from anyimage.server.outputs.surface import write_surface_archive
def write_test_normal_texture(normal, output_path):
converted = normal * np.array([1.0, -1.0, -1.0], dtype=np.float32)
rgb = np.clip((converted + 1.0) * 0.5, 0.0, 1.0)
alpha = np.ones((*rgb.shape[:2], 1), dtype=np.float32)
pixels = np.concatenate((rgb, alpha), axis=-1)
image = bpy.data.images.new(
"MoGe Test Normal",
width=rgb.shape[1],
height=rgb.shape[0],
alpha=True,
float_buffer=False,
)
image.pixels.foreach_set(pixels.reshape(-1))
image.file_format = "PNG"
image.filepath_raw = str(output_path)
image.save()
return output_path
def main():
bpy.ops.wm.read_factory_settings(use_empty=True)
arguments = sys.argv[sys.argv.index("--") + 1 :] if "--" in sys.argv else []
if arguments:
validate_real_result(Path(arguments[0]), Path(arguments[1]))
return
height, width = 48, 64
u, v = np.meshgrid(
np.linspace(-0.5, 0.5, width, dtype=np.float32),
np.linspace(-0.375, 0.375, height, dtype=np.float32),
)
depth = np.full((1, height, width), 2.0, dtype=np.float32)
points = np.stack((u * 2.0, v * 2.0, depth[0]), axis=-1)[None]
mask = np.ones((1, height, width), dtype=bool)
mask[:, :2] = False
intrinsics = np.array(
[[[1.0, 0.0, 0.5], [0.0, 1.0, 0.5], [0.0, 0.0, 1.0]]],
dtype=np.float32,
)
image = bpy.data.images.new("MoGe Surface Test", width=width, height=height)
foreground = bpy.data.images.new(
"MoGe Surface Test.color",
width=width,
height=height,
)
source = bpy.data.objects.new("MoGe Surface Test", None)
source.empty_display_type = "IMAGE"
source.data = image
bpy.context.collection.objects.link(source)
with tempfile.TemporaryDirectory() as temporary:
directory = Path(temporary)
mesh_path = directory / "mesh.npz"
normal = np.broadcast_to(
np.array([0.0, 0.0, -1.0], dtype=np.float32),
(height, width, 3),
)
pixel_intrinsics = intrinsics[0].copy()
pixel_intrinsics[0] *= width
pixel_intrinsics[1] *= height
pixel_intrinsics[2] = (0.0, 0.0, 1.0)
frame = GeometryFrame(
depth=depth[0],
points=points[0],
intrinsics=pixel_intrinsics,
valid_mask=np.isfinite(depth[0]) & (depth[0] > 0.0),
model_mask=mask[0],
)
write_surface_archive(build_surface(frame), mesh_path)
normal_path = write_test_normal_texture(
normal,
directory / "normal.png",
)
surface = build_image_mesh(
bpy.context,
source,
mesh_path,
False,
True,
False,
True,
color_image=foreground,
normal_path=normal_path,
)
assert len(surface.data.vertices) > 0
assert all(len(polygon.vertices) == 3 for polygon in surface.data.polygons)
assert surface.modifiers[0].node_group.name == MOGE_NODE_GROUP_NAME
inputs = {
item.name
for item in surface.modifiers[0].node_group.interface.items_tree
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",
"o_confidence_percentile",
"o_confidence",
"o_background",
"o_background_face",
}.intersection(attribute_names)
mask_socket = next(
item
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 False
assert mask_socket.structure_type == "SINGLE"
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
if getattr(item, "in_out", None) == "INPUT" and item.name == "Smooth"
)
assert smooth_socket.default_value == 0.5
assert not any(
node.bl_idname == "GeometryNodeInputNamedAttribute"
and node.inputs["Name"].default_value == "o_confidence"
for node in surface.modifiers[0].node_group.nodes
)
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_maps = [
node for node in material_group.nodes if node.bl_idname == "ShaderNodeNormalMap"
]
assert {node.space for node in normal_maps} == {"TANGENT", "OBJECT"}
normal_strength = next(
item
for item in material_group.interface.items_tree
if getattr(item, "in_out", None) == "INPUT" and item.name == "Normal Scale"
)
assert normal_strength.default_value == 1.0
assert normal_strength.min_value == 0.0
normal_input = next(
item
for item in material_group.interface.items_tree
if getattr(item, "in_out", None) == "INPUT" and item.name == "Normal"
)
assert tuple(normal_input.default_value) == (0.5, 0.5, 1.0, 1.0)
assert all(node.inputs["Strength"].is_linked for node in normal_maps)
layer = next(
node
for node in surface.data.materials[0].node_tree.nodes
if node.bl_idname == "ShaderNodeGroup" and node.node_tree == material_group
)
assert layer.inputs["Object Space"].default_value is True
assert not surface.data.has_custom_normals
color_texture = next(
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.color"
assert color_texture.image.colorspace_settings.name == material_color_space_name(
bpy.context.scene
)
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")
def validate_real_result(results_path, image_path):
image = bpy.data.images.load(str(image_path), check_existing=False)
source = bpy.data.objects.new("MoGe Real Surface", None)
source.empty_display_type = "IMAGE"
source.data = image
bpy.context.collection.objects.link(source)
mesh_path = results_path.with_name("mesh.npz")
if not mesh_path.is_file():
raise ValueError(f"Surface archive is missing: {mesh_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,
mesh_path,
False,
True,
False,
True,
color_image=color_image,
normal_path=normal_path,
)
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 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_maps = [
node for node in material_group.nodes if node.bl_idname == "ShaderNodeNormalMap"
]
assert {node.space for node in normal_maps} == {"TANGENT", "OBJECT"}
assert all(node.inputs["Strength"].is_linked for node in normal_maps)
layer = next(
node
for node in surface.data.materials[0].node_tree.nodes
if node.bl_idname == "ShaderNodeGroup" and node.node_tree == material_group
)
assert layer.inputs["Object Space"].default_value is True
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",
"o_confidence_percentile",
"o_confidence",
"o_background",
"o_background_face",
}.intersection(attribute_names)
camera = next(child for child in surface.children if child.type == "CAMERA")
dimensions = surface.dimensions
assert max(dimensions) < 10.0
assert min(dimensions.x, dimensions.y) > 0.1
assert 1.0 <= camera.data.lens <= 500.0
assert abs(camera.data.shift_x) <= 1.0
assert abs(camera.data.shift_y) <= 1.0
print(
f"Real MoGe-2 surface validation passed: "
f"{len(surface.data.vertices)} vertices, {len(surface.data.polygons)} faces, "
f"dimensions={tuple(round(value, 4) for value in dimensions)}, "
f"lens={camera.data.lens:.3f}, shift=({camera.data.shift_x:.4f}, "
f"{camera.data.shift_y:.4f})"
)
if __name__ == "__main__":
main()