From 86c5fe3085990544ea0183f505664e5d6ece5210 Mon Sep 17 00:00:00 2001 From: Andrey Yashkin Date: Mon, 1 Apr 2024 14:39:18 +0700 Subject: [PATCH] feat(mesh): Rework material manipulation closes #1082 --- blenderproc/api/material/__init__.py | 4 +- .../python/material/MaterialLoaderUtility.py | 8 ++- blenderproc/python/types/MeshObjectUtility.py | 71 ++++++++++++++++--- tests/testMeshObject.py | 40 +++++++++++ 4 files changed, 109 insertions(+), 14 deletions(-) create mode 100644 tests/testMeshObject.py diff --git a/blenderproc/api/material/__init__.py b/blenderproc/api/material/__init__.py index 7ab03a606..31567146d 100644 --- a/blenderproc/api/material/__init__.py +++ b/blenderproc/api/material/__init__.py @@ -1,6 +1,6 @@ from blenderproc.python.material.MaterialLoaderUtility import add_alpha, add_alpha_channel_to_textures, \ add_alpha_texture_node, add_ambient_occlusion, add_base_color, add_bump, add_displacement, add_metal, \ add_normal, add_roughness, add_specular, change_to_texture_less_render, collect_all, connect_uv_maps, \ - convert_to_materials, create_image_node, create, is_material_used, create_new_cc_material, \ - create_procedural_texture, find_cc_material_by_name, create_material_from_texture + convert_to_material, convert_to_materials, create_image_node, create, is_material_used, \ + create_new_cc_material, create_procedural_texture, find_cc_material_by_name, create_material_from_texture from blenderproc.python.material.Dust import add_dust diff --git a/blenderproc/python/material/MaterialLoaderUtility.py b/blenderproc/python/material/MaterialLoaderUtility.py index e00835d03..d7e262d72 100644 --- a/blenderproc/python/material/MaterialLoaderUtility.py +++ b/blenderproc/python/material/MaterialLoaderUtility.py @@ -34,13 +34,19 @@ def create(name: str) -> Material: return Material(new_mat) +def convert_to_material(blender_material: bpy.types.Material) -> Material: + """ Converts the given blender material to bproc.Material + """ + return None if blender_material is None else Material(blender_material) + + def convert_to_materials(blender_materials: List[Optional[bpy.types.Material]]) -> List[Optional[Material]]: """ Converts the given list of blender materials to bproc.Material(s) :param blender_materials: List of materials. :return: The list of materials. """ - return [(None if obj is None else Material(obj)) for obj in blender_materials] + return [convert_to_material(obj) for obj in blender_materials] def find_cc_material_by_name(material_name: str, custom_properties: Dict[str, Any]) -> bpy.types.Material: diff --git a/blenderproc/python/types/MeshObjectUtility.py b/blenderproc/python/types/MeshObjectUtility.py index b82ac9a8c..dd2f69bef 100644 --- a/blenderproc/python/types/MeshObjectUtility.py +++ b/blenderproc/python/types/MeshObjectUtility.py @@ -27,13 +27,57 @@ class MeshObject(Entity): Every instance of this class is a mesh which can be rendered in the scene. It can have multiple materials and different configurations of vertices with faces and edges. """ + def materials(self) -> int: + """ Returns the number of material slots of the object. - def get_materials(self) -> List[Optional[Material]]: - """ Returns the materials used by the mesh. + :return: The number of material slots. + """ + return len(self.blender_obj.material_slots) + + def get_material_slot_link(self, index: int) -> str: + """ Returns whether object's or object's data material is used in the material slot. + + :return: "DATA" if the material slot is linked to data material or "OBJECT" otherwise. + """ + return self.blender_obj.material_slots[index].link + + def set_material_slot_link(self, index: int, link: str): + """ Sets whether object's or object's data material is used in the material slot. + Available: ["DATA", "OBJECT"]. Type: str + """ + self.blender_obj.material_slots[index].link = link + + def get_material(self, index: int, link = "VISIBLE") -> Material: + """ Returns the material used by the object. + + :link: The mode specifying whether to get material linked to the object or object's data. + Available: ["DATA", "OBJECT", "VISIBLE"]. Type: str + :return: A list of materials. + """ + link = link.upper() + if link == "VISIBLE" and self.get_material_slot_link(index) == "DATA": + link = "DATA" + link2get = "OBJECT" if link == "VISIBLE" else link + + link2return = self.get_material_slot_link(index) + self.set_material_slot_link(index, link2get) + material = self.blender_obj.material_slots[index].material + self.set_material_slot_link(index, link2return) + + # If there is no material in the `OBJECT` slot then the 'DATA' material is displayed. + if material is None and link == "VISIBLE": + return self.get_material(index, "DATA") + else: + return MaterialLoaderUtility.convert_to_material(material) + + def get_materials(self, link = "VISIBLE") -> List[Optional[Material]]: + """ Returns the materials used by the object. + :link: The mode specifying whether to get materials linked to the object or object's data. + Available: ["DATA", "OBJECT", "VISIBLE"]. Type: str :return: A list of materials. """ - return MaterialLoaderUtility.convert_to_materials(self.blender_obj.data.materials) + return [self.get_material(index, link) for index in range(self.materials())] def has_materials(self) -> bool: """ @@ -41,25 +85,30 @@ def has_materials(self) -> bool: :return: True if the object has material slots. """ - return len(self.blender_obj.data.materials) > 0 + return self.materials() > 0 - def set_material(self, index: int, material: Material): - """ Sets the given material at the given index of the objects material list. + def set_material(self, index: int, material: Material, link="DATA"): + """ Sets the given material at the given index of the object's material list. :param index: The index to set the material to. :param material: The material to set. + :link: The mode specifying whether to link material to the object or object's data. + Available: ["DATA", "OBJECT"]. Type: str """ - self.blender_obj.data.materials[index] = material.blender_obj + keep_link = self.get_material_slot_link(index) + self.set_material_slot_link(index, link) + self.blender_obj.material_slots[index].material = None if material is None else material.blender_obj + self.set_material_slot_link(index, keep_link) def add_material(self, material: Material): - """ Adds a new material to the object. + """ Adds a new material to the object's data. :param material: The material to add. """ self.blender_obj.data.materials.append(material.blender_obj) def new_material(self, name: str) -> Material: - """ Creates a new material and adds it to the object. + """ Creates a new material and adds it to the object's data. :param name: The name of the new material. """ @@ -68,11 +117,11 @@ def new_material(self, name: str) -> Material: return new_mat def clear_materials(self): - """ Removes all materials from the object. """ + """ Removes all materials from the object's data. """ self.blender_obj.data.materials.clear() def replace_materials(self, material: bpy.types.Material): - """ Replaces all materials of the object with the given new material. + """ Replaces all materials of the object's data with the given new material. :param material: A material that should exclusively be used as new material for the object. """ diff --git a/tests/testMeshObject.py b/tests/testMeshObject.py new file mode 100644 index 000000000..e7d59cf65 --- /dev/null +++ b/tests/testMeshObject.py @@ -0,0 +1,40 @@ +import blenderproc as bproc + +import unittest + +from blenderproc.python.tests.SilentMode import SilentMode +from blenderproc.python.types.MeshObjectUtility import create_primitive +from blenderproc.python.material import MaterialLoaderUtility + + +class UnitTestCheckUtility(unittest.TestCase): + def test_materials(self): + bproc.clean_up(True) + + mat1 = MaterialLoaderUtility.create("mat1") + mat2 = MaterialLoaderUtility.create("mat2") + + obj = create_primitive("CUBE") + self.assertTrue(obj.has_materials() == False) + + obj.add_material(mat1) + self.assertTrue(obj.materials() == 1) + self.assertTrue(obj.get_material_slot_link(0) == "DATA") + self.assertTrue(obj.get_material(0, "DATA").get_name() == "mat1") + self.assertTrue(obj.get_material(0, "OBJECT") is None) + self.assertTrue(obj.get_material(0, "VISIBLE").get_name() == "mat1") + + obj.set_material(0, mat2, "OBJECT") + self.assertTrue(obj.get_material(0, "DATA").get_name() == "mat1") + self.assertTrue(obj.get_material(0, "OBJECT").get_name() == "mat2") + self.assertTrue(obj.get_material(0, "VISIBLE").get_name() == "mat1") + + obj.set_material_slot_link(0, "OBJECT") + self.assertTrue(obj.get_material(0, "DATA").get_name() == "mat1") + self.assertTrue(obj.get_material(0, "OBJECT").get_name() == "mat2") + self.assertTrue(obj.get_material(0, "VISIBLE").get_name() == "mat2") + + obj.set_material(0, None, "OBJECT") + self.assertTrue(obj.get_material(0, "DATA").get_name() == "mat1") + self.assertTrue(obj.get_material(0, "OBJECT") is None) + self.assertTrue(obj.get_material(0, "VISIBLE").get_name() == "mat1")