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:
File diff suppressed because it is too large
Load Diff
49
tools/node_assets/build.py
Normal file
49
tools/node_assets/build.py
Normal 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()
|
||||
1004
tools/node_assets/build_cutout.py
Normal file
1004
tools/node_assets/build_cutout.py
Normal file
File diff suppressed because it is too large
Load Diff
@@ -1,3 +1,5 @@
|
||||
"""Build the Mesh Plane Geometry Nodes group."""
|
||||
|
||||
import bpy
|
||||
|
||||
|
||||
77
tools/node_assets/build_shadeless.py
Normal file
77
tools/node_assets/build_shadeless.py
Normal 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
|
||||
79
tools/node_assets/validate.py
Normal file
79
tools/node_assets/validate.py
Normal 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()
|
||||
430
tools/node_assets/validate_cutout.py
Normal file
430
tools/node_assets/validate_cutout.py
Normal 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()
|
||||
@@ -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,
|
||||
)
|
||||
@@ -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()
|
||||
@@ -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()
|
||||
@@ -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()
|
||||
@@ -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()
|
||||
@@ -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()
|
||||
Reference in New Issue
Block a user