- Implemented `image_plane.py` to build the Image Plane Geometry Nodes group with customizable parameters for geometry, subdivide, and thickness. - Created `image_plane_validation.py` to validate the functionality of the image plane, ensuring correct geometry generation and material assignment. - Developed `material_layers.py` to construct shader node groups for image materials, including depth and normal handling. - Authored `plane_layouts.py` to define node arrangements for image geometry assets. - Introduced `shadeless.py` to create a shadeless shader node group that displays color without scene lighting while preserving shadows and reflections.
201 lines
8.5 KiB
Python
201 lines
8.5 KiB
Python
from pathlib import Path
|
|
from types import SimpleNamespace
|
|
from unittest.mock import Mock, patch
|
|
from blender_test_support import BlenderTestCase
|
|
|
|
|
|
class PlaneOperatorsTest(BlenderTestCase):
|
|
def test_plane_depth_off_accepts_animated_image_without_server(self):
|
|
operator = SimpleNamespace(
|
|
depth_mode="OFF",
|
|
remove_background=False,
|
|
shadeless=False,
|
|
profile="SURFACE",
|
|
mesh_detail=64,
|
|
thickness=0.1,
|
|
inflation=0.1,
|
|
algorithm="POISSON",
|
|
)
|
|
source = SimpleNamespace(data=SimpleNamespace(name="Sequence"))
|
|
context = SimpleNamespace(object=source, scene=object())
|
|
|
|
with (
|
|
patch.object(self.convert_to_plane, "video_total_frames", return_value=24),
|
|
patch.object(
|
|
self.convert_to_plane,
|
|
"image_user_settings",
|
|
return_value=(1, 0, 24, True),
|
|
),
|
|
patch.object(self.convert_to_plane, "create_image_material", return_value="Material"),
|
|
patch.object(self.convert_to_plane, "create_color_image", return_value="Color"),
|
|
patch.object(self.convert_to_plane, "create_plane_object") as build_mesh,
|
|
):
|
|
result = self.convert_to_plane.ConvertToPlane.execute(operator, context)
|
|
|
|
self.assertEqual(result, {"FINISHED"})
|
|
build_mesh.assert_called_once()
|
|
|
|
|
|
def test_depth_plane_rejects_animation_and_requests_ai_setup(self):
|
|
operator = SimpleNamespace(mesh_detail=6, report=Mock())
|
|
context = SimpleNamespace(object=SimpleNamespace(data=object()))
|
|
with (
|
|
patch.object(self.convert_to_plane, "video_total_frames", return_value=2),
|
|
patch.object(self.convert_to_plane, "invoke_ai_setup_if_needed") as setup,
|
|
):
|
|
self.assertEqual(self.convert_to_plane.ConvertToDepthPlane.execute(operator, context), {"CANCELLED"})
|
|
setup.assert_not_called()
|
|
with (
|
|
patch.object(self.convert_to_plane, "video_total_frames", return_value=1),
|
|
patch.object(self.convert_to_plane, "invoke_ai_setup_if_needed", return_value={"FINISHED"}) as setup,
|
|
patch.object(self.convert_to_plane, "prepare_image_input") as prepare,
|
|
):
|
|
self.assertEqual(self.convert_to_plane.ConvertToDepthPlane.execute(operator, context), {"FINISHED"})
|
|
setup.assert_called_once()
|
|
prepare.assert_not_called()
|
|
|
|
|
|
def test_depth_plane_starts_one_job_without_a_format_selection(self):
|
|
module = self.convert_to_plane
|
|
source = SimpleNamespace(name="Source", data=object())
|
|
context = SimpleNamespace(object=source, scene=object())
|
|
operator = SimpleNamespace(mesh_detail=4, report=Mock())
|
|
generate = Mock(return_value={"FINISHED"})
|
|
with (
|
|
patch.object(module, "video_total_frames", return_value=1),
|
|
patch.object(module, "invoke_ai_setup_if_needed", return_value=None),
|
|
patch.object(module, "prepare_image_input", return_value=(Path("input.png"), True)),
|
|
patch.object(module, "depth_plane_settings", return_value=("moge", 5)),
|
|
patch.object(module, "configured_max_ai_input_size", return_value=1024),
|
|
patch.object(module.bpy, "ops", SimpleNamespace(anyimage=SimpleNamespace(generate_depth_plane=generate)), create=True),
|
|
):
|
|
self.assertEqual(module.ConvertToDepthPlane.execute(operator, context), {"FINISHED"})
|
|
self.assertNotIn("depth_mode", generate.call_args.kwargs)
|
|
self.assertEqual(generate.call_args.kwargs["source_object_name"], "Source")
|
|
|
|
|
|
def test_depth_plane_result_connects_the_tangent_normal_material(self):
|
|
module = self.convert_to_plane_object
|
|
source = SimpleNamespace(data=SimpleNamespace(name="Source"))
|
|
color_image = SimpleNamespace(name="", users=0)
|
|
depth_image = SimpleNamespace(users=0)
|
|
normal_image = SimpleNamespace(users=0)
|
|
material = SimpleNamespace(users=0)
|
|
metadata = {"reference_depth": 1.0}
|
|
files = {
|
|
"depth_metadata": Path("depth.json"),
|
|
"depth": Path("depth.exr"),
|
|
"tangent_normal": Path("tangent-normal.png"),
|
|
}
|
|
result = SimpleNamespace(
|
|
directory=Path("result"),
|
|
file=lambda key: files[key],
|
|
)
|
|
operator = SimpleNamespace(source_object_name="Source", mesh_detail=6, generate_depth=True)
|
|
context = SimpleNamespace(scene=object())
|
|
ops = SimpleNamespace(
|
|
ed=SimpleNamespace(undo_push=Mock(return_value={"FINISHED"}))
|
|
)
|
|
|
|
with (
|
|
patch.object(module, "require_image_empty", return_value=source),
|
|
patch.object(module, "load_color_result_image", return_value=color_image),
|
|
patch.object(module, "load_depth_metadata", return_value=metadata),
|
|
patch.object(module, "load_depth_result_image", return_value=depth_image) as load_depth,
|
|
patch.object(
|
|
module,
|
|
"load_normal_result_image",
|
|
return_value=normal_image,
|
|
) as load_normal,
|
|
patch.object(
|
|
module,
|
|
"create_image_material",
|
|
return_value=material,
|
|
) as create_material,
|
|
patch.object(module, "create_depth_plane_object") as create_object,
|
|
patch.object(module.bpy, "ops", ops, create=True),
|
|
):
|
|
module.create_depth_plane_from_result(context, result, operator)
|
|
|
|
load_depth.assert_called_once_with(files["depth"], source, metadata)
|
|
ops.ed.undo_push.assert_called_once_with(message="Convert to Depth Plane")
|
|
load_normal.assert_called_once_with(files["tangent_normal"], source)
|
|
create_material.assert_called_once_with(
|
|
source,
|
|
color_image,
|
|
0.0,
|
|
normal_image=normal_image,
|
|
normal_space="TANGENT",
|
|
ior=1.2,
|
|
scene=context.scene,
|
|
)
|
|
create_object.assert_called_once_with(
|
|
context,
|
|
source,
|
|
6,
|
|
material,
|
|
depth_image,
|
|
metadata,
|
|
)
|
|
|
|
|
|
def test_depth_plane_result_cleans_unused_depth_and_normal_on_failure(self):
|
|
module = self.convert_to_plane_object
|
|
source = SimpleNamespace(data=SimpleNamespace(name="Source"))
|
|
color_image = SimpleNamespace(name="", users=0)
|
|
depth_image = SimpleNamespace(users=0)
|
|
normal_image = SimpleNamespace(users=0)
|
|
material = SimpleNamespace(users=0)
|
|
files = {
|
|
"depth_metadata": Path("depth.json"),
|
|
"depth": Path("depth.exr"),
|
|
"tangent_normal": Path("tangent-normal.png"),
|
|
}
|
|
result = SimpleNamespace(
|
|
directory=Path("result"),
|
|
file=lambda key: files[key],
|
|
)
|
|
remove_material = Mock()
|
|
remove_image = Mock()
|
|
data = SimpleNamespace(
|
|
materials=SimpleNamespace(remove=remove_material),
|
|
images=SimpleNamespace(remove=remove_image),
|
|
)
|
|
|
|
with (
|
|
patch.object(module, "require_image_empty", return_value=source),
|
|
patch.object(module, "load_color_result_image", return_value=color_image),
|
|
patch.object(
|
|
module,
|
|
"load_depth_metadata",
|
|
return_value={"reference_depth": 1.0},
|
|
),
|
|
patch.object(module, "load_depth_result_image", return_value=depth_image),
|
|
patch.object(module, "load_normal_result_image", return_value=normal_image),
|
|
patch.object(module, "create_image_material", return_value=material),
|
|
patch.object(
|
|
module,
|
|
"create_depth_plane_object",
|
|
side_effect=RuntimeError("failed"),
|
|
),
|
|
patch.object(module.bpy, "data", data),
|
|
self.assertRaisesRegex(RuntimeError, "failed"),
|
|
):
|
|
module.create_depth_plane_from_result(
|
|
SimpleNamespace(scene=object()),
|
|
result,
|
|
SimpleNamespace(source_object_name="Source", mesh_detail=6, generate_depth=True),
|
|
)
|
|
|
|
remove_material.assert_called_once_with(material, do_unlink=True)
|
|
self.assertEqual(
|
|
[entry.args[0] for entry in remove_image.call_args_list],
|
|
[color_image, depth_image, normal_image],
|
|
)
|
|
self.assertTrue(
|
|
all(
|
|
entry.kwargs == {"do_unlink": True}
|
|
for entry in remove_image.call_args_list
|
|
)
|
|
)
|