Source code for blenderproc.python.modules.provider.getter.Material

import json
import re
from random import sample

import mathutils

from blenderproc.python.modules.main.Provider import Provider
from blenderproc.python.utility.BlenderUtility import get_all_materials
from blenderproc.python.utility.Utility import Utility


[docs]class Material(Provider): """ Returns a list of materials that comply with defined conditions. Example 1: Return materials matching a name pattern. .. code-block:: yaml { "provider": "getter.Material", "conditions": { "name": "wood.*" } } Example 2: Return all materials matching a name pattern which also have exactly two textures used. .. code-block:: yaml { "provider": "getter.Material", "conditions": { "name": "wood.*", "cf_texture_amount_eq": "2" } } Example 3: Return all materials matching a name pattern which also have two or more textures used. .. code-block:: yaml { "provider": "getter.Material", "conditions": { "name": "wood.*", "cf_texture_amount_min": "2" } } Example 4: Return all materials which: {match the name pattern 'wood.*' AND have two or less textures used} OR {match the name pattern 'tile.*'} .. code-block:: yaml { "provider": "getter.Material", "conditions": [ { "name": "wood.*", "cf_texture_amount_max": "2" }, { "name: "tile.*" } ] } Example 5: Return all materials which: {are cc_textures (see CCMaterialLoader) and do not have any alpha texture used} .. code-block:: yaml { "provider": "getter.Material", "conditions": [ { "cp_is_cc_texture": True, "cf_principled_bsdf_Alpha_eq": 1.0 } ] } Example 6: Return all materials which: {belong to a certain list of objects and do not have any alpha texture used} .. code-block:: yaml { "provider": "getter.Material", "conditions": [ { "cf_use_materials_of_objects": { "provider": "getter.Entity", "conditions": { "type": "MESH", "name": "Suzanne" } }, "cf_principled_bsdf_Alpha_eq": 1.0 } ] } **Configuration**: .. list-table:: :widths: 25 100 10 :header-rows: 1 * - Parameter - Description - Type * - conditions - List of dicts/a dict of entries of format {attribute_name: attribute_value}. Entries in a dict are conditions connected with AND, if there multiple dicts are defined (i.e. 'conditions' is a list of dicts, each cell is connected by OR. - list/dict * - conditions/attribute_name - Name of any valid material's attribute, custom property, or custom function. Any given attribute_value of the type string will be treated as a REGULAR EXPRESSION. " In order to specify, what exactly one wants to look for: For attribute: key of the pair must be a valid attribute name. For custom property: key of the pair must start with `cp_` prefix. For calling custom function: key of the pair must start with `cf_` prefix. See table below for supported custom functions. - string * - conditions/attribute_value - Any value to set. - string, list/Vector, int, bool or float * - index - If set, after the conditions are applied only the entity with the specified index is returned. - int * - random_samples - If set, this Provider returns random_samples objects from the pool of selected ones. Define index or random_samples property, only one is allowed at a time. Default: 0. - int * - check_empty - If this is True, the returned list can not be empty, if it is empty an error will be thrown. Default: False. - bool **Custom functions** .. list-table:: :widths: 25 100 10 :header-rows: 1 * - Parameter - Description - Type * - cf_texture_amount_{min,max,eq} - Returns materials that have a certain amount of texture nodes inside of the material. Suffix 'min' = less nodes or equal than specified, 'max' = at least as many or 'eq' = for this exact amount of textures nodes. - int * - cf_principled_bsdf_amount_{min,max,eq} - Returns materials that have a certain amount of principled bsdf nodes inside of the material. Suffix 'min' = less nodes or equal than specified, 'max' = at least as many or 'eq' = for this exact amount of principled bsdf nodes. - int * - cf_principled_bsdf_{INPUT}_{min,max,eq} - Returns materials that have a certain value for a certain INPUT of the principled bsdf node. Only works if there is only one principled bsdf node. The INPUT can be any of the input values used of the principled bsdf. By using this all materials, which do not use a float value as an input are also not selected. Suffix 'min' = less nodes or equal than specified, 'max' = at least as many or 'eq' = for this exact amount of principled bsdf nodes. - float * - conditions/cf_use_materials_of_objects - This accepts a provider "getter.Entity", which returns a list of mesh objects, on which the materials have to be in use. This can be used to return all materials, which are currently used on a certain object. - Provider """ def __init__(self, config): Provider.__init__(self, config)
[docs] @staticmethod def perform_and_condition_check(and_condition, materials, used_materials_to_check=None): """ Checks for all materials in the scene if all given conditions are true, collects them in the return list. :param and_condition: Given conditions. Type: dict. :param materials: Materials, that are already in the return list. Type: list. :param used_materials_to_check: a list of materials to perform the check on. Type: list. Default: all materials :return: Materials that fulfilled given conditions. Type: list. """ new_materials = [] if used_materials_to_check is None: used_materials_to_check = get_all_materials() # through every material for material in used_materials_to_check: if material in new_materials or material in materials or material is None: continue select_material = True for key, value in and_condition.items(): # check if the key is a requested custom property requested_custom_property = False requested_custom_function = False if key.startswith('cp_'): requested_custom_property = True key = key[3:] if key.startswith('cf_'): requested_custom_function = True key = key[3:] if hasattr(material, key) and not requested_custom_property and not requested_custom_function: # check if the type of the value of attribute matches desired if isinstance(getattr(material, key), type(value)): new_value = value # if not, try to enforce some mathutils-specific type else: if isinstance(getattr(material, key), mathutils.Vector): new_value = mathutils.Vector(value) elif isinstance(getattr(material, key), mathutils.Euler): new_value = mathutils.Euler(value) elif isinstance(getattr(material, key), mathutils.Color): new_value = mathutils.Color(value) # raise an exception if it is none of them else: raise Exception("Types are not matching: %s and %s !" % (type(getattr(material, key)), type(value))) # or check for equality if not ((isinstance(getattr(material, key), str) and re.fullmatch(value, getattr(material, key)) is not None) or getattr(material, key) == new_value): select_material = False break # check if a custom property with this name exists elif key in material and requested_custom_property: # check if the type of the value of such custom property matches desired if isinstance(material[key], type(value)) or (isinstance(material[key], int) and isinstance(value, bool)): # if it is a string and if the whole string matches the given pattern if not ((isinstance(material[key], str) and re.fullmatch(value, material[key]) is not None) or material[key] == value): select_material = False break else: # raise an exception if not raise Exception("Types are not matching: {} and {} !".format(type(material[key]), type(value))) elif requested_custom_function: if key.startswith("texture_amount_"): if material.use_nodes: value = int(value) nodes = material.node_tree.nodes texture_nodes = Utility.get_nodes_with_type(nodes, "TexImage") amount_of_texture_nodes = len(texture_nodes) if texture_nodes is not None else 0 if "min" in key: if not (amount_of_texture_nodes >= value): select_material = False break elif "max" in key: if not (amount_of_texture_nodes <= value): select_material = False break elif "eq" in key: if not (amount_of_texture_nodes == value): select_material = False break else: raise Exception("This type of key is unknown: {}".format(key)) else: select_material = False break elif key.startswith("principled_bsdf_amount_"): if material.use_nodes: value = int(value) nodes = material.node_tree.nodes principled = Utility.get_nodes_with_type(nodes, "BsdfPrincipled") amount_of_principled_bsdf_nodes = len(principled) if principled is not None else 0 if "min" in key: if not (amount_of_principled_bsdf_nodes >= value): select_material = False break elif "max" in key: if not (amount_of_principled_bsdf_nodes <= value): select_material = False break elif "eq" in key: if not (amount_of_principled_bsdf_nodes == value): select_material = False break else: raise Exception("This type of key is unknown: {}".format(key)) else: select_material = False break elif key.startswith("principled_bsdf_"): # must be after the amount check # This custom function can check the value of a certain Principled BSDF shader input. # For example this can be used to avoid using materials, which have an Alpha Texture by # adding they key: `"cf_principled_bsdf_Alpha_eq": 1.0` if material.use_nodes: value = float(value) # first check if there is only one Principled BSDF node in the material nodes = material.node_tree.nodes principled = Utility.get_nodes_with_type(nodes, "BsdfPrincipled") amount_of_principled_bsdf_nodes = len(principled) if principled is not None else 0 if amount_of_principled_bsdf_nodes != 1: select_material = False break principled = principled[0] # then extract the input name from the key, for the Alpha example: `Alpha` extracted_input_name = key[len("principled_bsdf_"):key.rfind("_")] # check if this key exists, else throw an error if extracted_input_name not in principled.inputs: raise Exception("Only valid inputs of a principled node are allowed: " "{} in: {}".format(extracted_input_name, key)) # extract this input value used_value = principled.inputs[extracted_input_name] # if this input value is not a default value it will be connected via the links if len(used_value.links) > 0: select_material = False break # if no link is found check the default value used_value = used_value.default_value # compare the given value to the default value if key.endswith("min"): if not (used_value >= value): select_material = False break elif key.endswith("max"): if not (used_value <= value): select_material = False break elif key.endswith("eq"): if not (used_value == value): select_material = False break else: raise Exception("This type of key is unknown: {}".format(key)) else: select_material = False break elif key == "use_materials_of_objects": objects = Utility.build_provider_based_on_config(value).run() found_material = False # iterate over all selected objects for obj in objects: # check if they have materials if hasattr(obj, "material_slots"): for mat_slot in obj.material_slots: # if the material is the same as the currently checked one if mat_slot.material == material: found_material = True break if found_material: break if not found_material: select_material = False break else: select_material = False break else: select_material = False break if select_material: new_materials.append(material) return new_materials
[docs] def _get_conditions_as_string(self): """ Returns the used conditions as neatly formatted string :return: str: containing the conditions """ conditions = self.config.get_raw_dict('conditions') text = json.dumps(conditions, indent=2, sort_keys=True) def add_indent(t): return "\n".join(" " * len("Exception: ") + e for e in t.split("\n")) return add_indent(text)
[docs] def run(self): """ Processes defined conditions and compiles a list of materials. :return: List of materials that met the conditional requirement. Type: list. """ conditions = self.config.get_raw_dict('conditions') # the list of conditions is treated as or condition if isinstance(conditions, list): materials = [] # each single condition is treated as and condition for and_condition in conditions: materials.extend(self.perform_and_condition_check(and_condition, materials)) else: # only one condition was given, treat it as and condition materials = self.perform_and_condition_check(conditions, []) random_samples = self.config.get_int("random_samples", 0) has_index = self.config.has_param("index") if has_index and not random_samples: materials = [materials[self.config.get_int("index")]] elif random_samples and not has_index: materials = sample(materials, k=min(random_samples, len(materials))) elif has_index and random_samples: raise RuntimeError("Please, define only one of two: `index` or `random_samples`.") check_if_return_is_empty = self.config.get_bool("check_empty", False) if check_if_return_is_empty and not materials: raise Exception(f"There were no materials selected with the following " f"condition: \n{self._get_conditions_as_string()}") return materials