Refactor and enhance geometry node groups for depth plane and material layers

- Updated the depth plane node group to load from an asset and simplified the interface.
- Created a new build script for depth plane geometry nodes.
- Introduced material layer groups for image and depth materials with appropriate sockets.
- Added functionality to connect balloon normal scale to the image material.
- Enhanced tests to validate the new node groups and their connections.
- Ensured that production code does not create node groups directly.
This commit is contained in:
2026-08-31 12:25:23 +08:00
parent 87d7721128
commit 377eee43f5
18 changed files with 602 additions and 405 deletions

View File

@@ -495,6 +495,23 @@ def test_balloon_build_starts_from_the_unified_single_sheet():
profile = np.empty(len(result.data.vertices), dtype=np.float32)
profile_attribute.data.foreach_get("value", profile)
assert profile.min() == 0.0 and profile.max() > 0.0
material_tree = result.data.materials[0].node_tree
normal_scale_attribute = next(
node
for node in material_tree.nodes
if node.bl_idname == "ShaderNodeAttribute"
)
assert normal_scale_attribute.attribute_type == "GEOMETRY"
assert normal_scale_attribute.attribute_name == shapes.BALLOON_ATTRIBUTE_NAME
image_layer = _group_node(material.material_node_group(), material_tree)
normal_scale_link = next(
link
for link in material_tree.links
if link.to_node == image_layer
and link.to_socket == image_layer.inputs["Normal Scale"]
)
assert normal_scale_link.from_node == normal_scale_attribute
assert normal_scale_link.from_socket == normal_scale_attribute.outputs["Fac"]
assert result.modifiers[0].node_group.name == shapes.CUTOUT_NODE_GROUP_NAME
inward = result.matrix_world.to_3x3() @ Vector((1.0, 0.0, 0.0))
assert np.allclose(tuple(inward), (0.0, 0.0, -1.0), atol=1e-6)
@@ -523,6 +540,10 @@ def test_teddy_preference_writes_the_shared_balloon_attribute():
profile = np.empty(len(result.data.vertices), dtype=np.float32)
attribute.data.foreach_get("value", profile)
assert profile.min() == 0.0 and profile.max() > 0.0
assert not any(
node.bl_idname == "ShaderNodeAttribute"
for node in result.data.materials[0].node_tree.nodes
)
def test_cutout_material_uses_ior_1_2():
@@ -1228,3 +1249,38 @@ def test_local_cutout_builds_from_blender_image_pixels_without_a_job():
assert np.allclose(
copied[:, :, 3], np.flipud(rgba[top:bottom, left:right, 3])
)
def test_balloon_normal_scale_uses_existing_image_layer_input():
bpy.ops.wm.read_factory_settings(use_empty=True)
assert shapes.BALLOON_CUTOUT_SHAPES == frozenset(
{"BALLOON", "DEPTH_BALLOON"}
)
group = bpy.data.node_groups.new(
material.IMAGE_MATERIAL_NODE_GROUP_NAME,
"ShaderNodeTree",
)
group.interface.new_socket(
name="Normal Scale",
in_out="INPUT",
socket_type="NodeSocketFloat",
)
image_material = bpy.data.materials.new("Balloon")
image_material.use_nodes = True
image_material.node_tree.nodes.clear()
image_layer = image_material.node_tree.nodes.new("ShaderNodeGroup")
image_layer.node_tree = group
cutout_object._connect_balloon_normal_scale(image_material)
assert _inputs(group) == ["Normal Scale"]
attribute = next(
node
for node in image_material.node_tree.nodes
if node != image_layer
)
assert attribute.bl_idname == "ShaderNodeAttribute"
assert attribute.attribute_name == shapes.BALLOON_ATTRIBUTE_NAME
link = next(iter(image_material.node_tree.links))
assert link.from_socket == attribute.outputs["Fac"]
assert link.to_socket == image_layer.inputs["Normal Scale"]

32
tests/test_node_assets.py Normal file
View File

@@ -0,0 +1,32 @@
import unittest
from pathlib import Path
PROJECT_ROOT = Path(__file__).resolve().parents[1]
SOURCE_ROOT = PROJECT_ROOT / "src" / "anyimage"
class NodeAssetTest(unittest.TestCase):
def test_production_code_does_not_create_node_groups(self):
offenders = []
for path in SOURCE_ROOT.rglob("*.py"):
if "bpy.data.node_groups.new" in path.read_text(encoding="utf-8"):
offenders.append(path.relative_to(PROJECT_ROOT).as_posix())
self.assertEqual(offenders, [])
def test_runtime_node_group_entries_load_assets(self):
material_source = (
SOURCE_ROOT / "operators" / "common" / "mesh_result.py"
).read_text(encoding="utf-8")
plane_source = (
SOURCE_ROOT / "operators" / "convert_to_plane" / "nodes.py"
).read_text(encoding="utf-8")
self.assertIn("return load_asset_node_group(group_name)", material_source)
self.assertIn(
"return load_asset_node_group(DEPTH_PLANE_NODE_GROUP_NAME)",
plane_source,
)
if __name__ == "__main__":
unittest.main()