from types import SimpleNamespace import bpy import numpy as np from anyimage.common import image as image_data from anyimage.common import material as material_data from anyimage.operators.cutout_tool import object as cutout_object from anyimage.operators.cutout_tool import shape as cutout_shape from tests.support.image_objects import _create_cutout_shape, _group_node, _inputs def test_image_layer_switches_between_tangent_and_object_space_normal(): bpy.ops.wm.read_factory_settings(use_empty=True) assert material_data.IMAGE_MATERIAL_NODE_GROUP_NAME == "O Image Layer" group = material_data.material_node_group() normal = next( item for item in group.interface.items_tree if item.item_type == "SOCKET" and item.in_out == "INPUT" and item.name == "Normal" ) assert tuple(normal.default_value) == (0.5, 0.5, 1.0, 1.0) bump_scale = next( item for item in group.interface.items_tree if item.item_type == "SOCKET" and item.in_out == "INPUT" and item.name == "Bump Scale" ) assert bump_scale.default_value == 0.0 normal_scale = next( item for item in 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 object_space = next( item for item in group.interface.items_tree if item.item_type == "SOCKET" and item.in_out == "INPUT" and item.name == "Object Space" ) assert object_space.default_value is False normal_maps = { node.space for node in group.nodes if node.bl_idname == "ShaderNodeNormalMap" } assert normal_maps == {"TANGENT", "OBJECT"} assert all( not node.label for node in group.nodes if node.bl_idname in {"ShaderNodeNormalMap", "ShaderNodeMix"} ) color = bpy.data.images.new("Color", width=2, height=2) normal_image = bpy.data.images.new("Normal", width=2, height=2) object_material = material_data.create_image_material( SimpleNamespace(name="Object Normal"), color, 1.0, normal_image=normal_image, normal_space="OBJECT", ) layer = _group_node(group, object_material.node_tree) assert layer.inputs["Object Space"].default_value is True tangent_material = material_data.create_image_material( SimpleNamespace(name="Tangent Normal"), color, 1.0, normal_image=normal_image, ) layer = _group_node(group, tangent_material.node_tree) assert layer.inputs["Object Space"].default_value is False reused = material_data.material_node_group() assert { node.space for node in reused.nodes if node.bl_idname == "ShaderNodeNormalMap" } == {"TANGENT", "OBJECT"} def test_cutout_material_uses_ior_1_2(): bpy.ops.wm.read_factory_settings(use_empty=True) image = bpy.data.images.new("Cutout", width=16, height=16, alpha=True) source = bpy.data.objects.new("Cutout", None) source.empty_display_type = "IMAGE" source.data = image source.empty_display_size = 2.0 bpy.context.collection.objects.link(source) result = _create_cutout_shape(source, "FLAT", 0.1) shader = next( node for node in result.data.materials[0].node_tree.nodes if node.bl_idname == "ShaderNodeBsdfPrincipled" ) assert np.isclose(shader.inputs["IOR"].default_value, 1.2) image_layer = _group_node( material_data.material_node_group(), result.data.materials[0].node_tree, ) ior_level_link = next( link for link in result.data.materials[0].node_tree.links if link.to_node == shader and link.to_socket == shader.inputs["Specular IOR Level"] ) assert ior_level_link.from_node == image_layer assert ior_level_link.from_socket == image_layer.outputs["Color"] def test_plane_material_color_drives_ior_level_but_shadeless_does_not(): bpy.ops.wm.read_factory_settings(use_empty=True) image = bpy.data.images.new("Plane", width=2, height=2, alpha=True) source = SimpleNamespace(data=SimpleNamespace(name="Plane")) material = material_data.create_image_material(source.data, image) shader = next( node for node in material.node_tree.nodes if node.bl_idname == "ShaderNodeBsdfPrincipled" ) assert np.isclose(shader.inputs["IOR"].default_value, 1.2) image_layer = _group_node(material_data.material_node_group(), material.node_tree) assert image_layer.inputs["Bump Scale"].default_value == 0.0 assert any( link.from_node == image_layer and link.from_socket == image_layer.outputs["Color"] and link.to_node == shader and link.to_socket == shader.inputs["Specular IOR Level"] for link in material.node_tree.links ) shadeless = material_data.create_image_material( source.data, image, 0.0, shadeless=True, ) assert not any( node.bl_idname == "ShaderNodeBsdfPrincipled" for node in shadeless.node_tree.nodes ) def test_cutout_material_uses_flat_tangent_normal(): bpy.ops.wm.read_factory_settings(use_empty=True) source = bpy.data.objects.new("Source", None) source.empty_display_type = "IMAGE" source.data = bpy.data.images.new("Source", width=2, height=2) color = bpy.data.images.new("Color", width=2, height=2) normal = bpy.data.images.new("Normal", width=1, height=1) result = material_data.create_image_material( source.data, color, 0.0, normal_image=normal, scene=bpy.context.scene, ) layer = _group_node(material_data.material_node_group(), result.node_tree) assert layer.inputs["Object Space"].default_value is False def test_loaded_normal_image_uses_normal_texture_name(tmp_path): from PIL import Image path = tmp_path / "normal.png" Image.new("RGB", (1, 1), (128, 128, 255)).save(path) source = SimpleNamespace(data=SimpleNamespace(name="Poster.png")) image = image_data.load_normal_result_image(path, source) assert image.name == "Poster_normal.png" assert image.colorspace_settings.name == "Non-Color" assert image.packed_file is not None def test_shared_conversion_images_use_blender_duplicate_suffix(): bpy.ops.wm.read_factory_settings(use_empty=True) image = bpy.data.images.new("Poster", width=16, height=16) source = bpy.data.objects.new("Poster", None) source.empty_display_type = "IMAGE" source.data = image bpy.context.collection.objects.link(source) other = source.copy() bpy.context.collection.objects.link(other) from tests.support.color_image import color_image first = color_image(source.data) second = color_image(source.data) assert first is not image assert second is not image assert first is not second assert first.name == "Poster_color.png" assert second.name == "Poster_color.png.001" def test_balloon_normal_scale_uses_existing_image_layer_input(): bpy.ops.wm.read_factory_settings(use_empty=True) group = material_data.material_node_group() original_inputs = _inputs(group) 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_cutout_normal_scale(image_material) assert _inputs(group) == original_inputs attribute = next( node for node in image_material.node_tree.nodes if node != image_layer ) assert attribute.bl_idname == "ShaderNodeAttribute" assert attribute.attribute_name == cutout_shape.NORMAL_SCALE_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"]