Source code for blenderproc.python.modules.manipulators.EntityManipulator

import warnings
from random import choice

import bpy
import numpy as np

import blenderproc.python.utility.BlenderUtility as BlenderUtility
from blenderproc.python.modules.main.Module import Module
from blenderproc.python.modules.provider.getter.Material import Material
from blenderproc.python.modules.utility.Config import Config
from mathutils import Matrix

from blenderproc.python.types.MeshObjectUtility import MeshObject


[docs]class EntityManipulator(Module): """ Performs manipulation on selected entities of different Blender built-in types, e.g. Mesh objects, Camera objects, Light objects, etc. Example 1: For all 'MESH' type objects with a name matching a 'Cube.*' pattern set rotation Euler vector and set custom property `physics`. .. code-block:: yaml { "module": "manipulators.EntityManipulator", "config": { "selector": { "provider": "getter.Entity", "conditions": { "name": 'Cube.*', "type": "MESH" } }, "rotation_euler": [1, 1, 0], "cp_physics": True } } Example 2: Set a shared (sampled once and set for all selected objects) location for all 'MESH' type objects with a name matching a 'Cube.*' pattern. .. code-block:: yaml { "module": "manipulators.EntityManipulator", "config": { "selector": { "provider": "getter.Entity", "conditions": { "name": 'Cube.*', "type": "MESH" } }, "mode": "once_for_all", "location": { "provider": "sampler.Uniform3d", "max":[1, 2, 3], "min":[0, 1, 2] } } } Example 3: Set a unique (sampled once for each selected object) location and apply a 'Solidify' object modifier with custom 'thickness' attribute value to all 'MESH' type objects with a name matching a 'Cube.*' pattern. .. code-block:: yaml { "module": "manipulators.EntityManipulator", "config": { "selector": { "provider": "getter.Entity", "conditions": { "name": 'Cube.*', "type": "MESH" } }, "mode": "once_for_each", # can be omitted, `once_for_each` is a default value of `mode` parameter "location": { "provider": "sampler.Uniform3d", "max":[1, 2, 3], "min":[0, 1, 2] }, "cf_add_modifier": { "name": "Solidify", "thickness": 0.001 } } } Example 4: Add a displacement modifier with a newly generated texture. .. code-block:: yaml { "module": "manipulators.EntityManipulator", "config": { "selector": { "provider": "getter.Entity", "conditions": { "name": 'Cube.*', "type": "MESH" } }, "cf_add_displace_modifier_with_texture": { "texture": 'VORONOI' } } } Example 5: Add a displacement modifier with a newly random generated texture with custom texture, noise scale, modifier mid_level, modifier render_level and modifier strength. With prior addition of a uv_map to all object without an uv map and adding of a Subdivision Surface Modifier if the number of vertices of an object is less than 10000. .. code-block:: yaml { "module": "manipulators.EntityManipulator", "config": { "selector": { "provider": "getter.Entity", "conditions": { "name": 'apple', "type": "MESH" } }, "cf_add_uv_mapping":{ "projection": "cylinder" }, "cf_add_displace_modifier_with_texture": { "texture": { "provider": "sampler.Texture" }, "min_vertices_for_subdiv": 10000, "mid_level": 0.5, "subdiv_level": { "provider": "sampler.Value", "type": "int", "min": 1, "max": 3 }, "strength": { "provider": "sampler.Value", "type": "float", "mode": "normal", "mean": 0.0, "std_dev": 0.7 } } } } **Configuration**: .. list-table:: :widths: 25 100 10 :header-rows: 1 * - Parameter - Description - Type * - selector - Objects to become subjects of manipulation. - Provider * - mode - Default: "once_for_each". Available: 'once_for_each' (if samplers are called, new sampled value is set to each selected entity), 'once_for_all' (if samplers are called, value is sampled once and set to all selected entities). - string **Values to set**: .. list-table:: :widths: 25 100 10 :header-rows: 1 * - Parameter - Description - Type * - key - Name of the attribute/custom property to change or a name of a custom function to perform on entities. " In order to specify, what exactly one wants to modify (e.g. attribute, custom property, etc.): For attribute: key of the pair must be a valid attribute name of the selected object. 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 function names. - string * - value - Value of the attribute/custom prop. to set or input value(s) for a custom function. - string **Custom functions** .. list-table:: :widths: 25 100 10 :header-rows: 1 * - Parameter - Description - Type * - cf_add_modifier - Adds a modifier to the selected object. - dict * - cf_add_modifier/name - Name of the modifier to add. Available values: 'Solidify'. - string. * - cf_add_modifier/thickness - 'thickness' attribute of the 'Solidify' modifier. - float * - cf_set_shading - Custom function to set the shading of the selected object. Default: 'FLAT'. Available: ['FLAT', 'SMOOTH', 'AUTO']. - str * - cf_shading_auto_smooth_angle_in_deg - Angle in degrees at which flat shading is activated in `AUTO` mode. Default: 30. - float * - cf_add_displace_modifier_with_texture - Adds a displace modifier with texture to an object. - dict * - cf_add_displace_modifier_with_texture/texture - The structure is either a given or a random texture. Default: []. Available:['CLOUDS'," 'DISTORTED_NOISE', 'MAGIC', 'MARBLE', 'MUSGRAVE', 'NOISE', 'STUCCI', 'VORONOI', 'WOOD'] - str * - cf_add_displace_modifier_with_texture/min_vertices_for_subdiv - Checks if a subdivision is necessary. If the vertices of a object are less than 'min_vertices_for_subdiv' a Subdivision modifier will be add to the object. Default: 10000. - int * - cf_add_displace_modifier_with_texture/mid_level - Texture value that gives no displacement. Parameter of displace modifier. Default: 0.5 - float * - cf_add_displace_modifier_with_texture/subdiv_level - Numbers of Subdivisions to perform when rendering. Parameter of Subdivision modifier. Default: 2 - int * - cf_add_displace_modifier_with_texture/strength - Amount to displace geometry. Parameter of displace modifier. Default: 0.1 - float * - cf_add_uv_mapping - Adds a uv map to an object if uv map is missing. - dict * - cf_add_uv_mapping/projection - Name of the projection as str. Default: []. Available: ["cube", "cylinder", "smart", "sphere"] - str * - cf_add_uv_mapping/forced_recalc_of_uv_maps - If this is set to True, all UV maps are recalculated not just the missing ones - bool * - cf_randomize_materials - Randomizes the materials for the selected objects with certain probability. - dict * - cf_randomize_materials/randomization_level - Expected fraction of the selected objects for which the texture should be randomized. Default: 0.2. Range: [0, 1] - float * - cf_randomize_materials/materials_to_replace_with - Material(s) to participate in randomization. Sampling from the pool of elegible material (that comply with conditions is performed in the Provider). Make sure you use 'random_samples" config parameter of the Provider, if multiple materials are returned, the first one will be considered as a substitute during randomization. Default: random material. - Provider * - cf_randomize_materials/obj_materials_cond_to_be_replaced - A dict of materials and corresponding conditions making it possible to only replace materials with certain properties. These are similar to the conditions mentioned in the getter.Material. Default: {}. - dict * - cf_randomize_materials/add_to_objects_without_material - If set to True, objects which didn't have any material before will also get a random material assigned. Default: False. - bool """ def __init__(self, config): Module.__init__(self, config)
[docs] def run(self): """ Sets according values of defined attributes/custom properties or applies custom functions to the selected entities. """ # separating defined part with the selector from ambiguous part with attribute names and their values to set set_params = {} sel_objs = {} for key in self.config.data.keys(): if key != 'selector' and key != "mode": # if its not a selector -> to the set parameters dict set_params[key] = self.config.data[key] else: sel_objs[key] = self.config.data[key] # create Config objects params_conf = Config(set_params) sel_conf = Config(sel_objs) # invoke a Getter, get a list of entities to manipulate entities = sel_conf.get_list("selector") op_mode = self.config.get_string("mode", "once_for_each") if not entities: warnings.warn("Warning: No entities are selected. Check Providers conditions.") return else: print("Amount of objects to modify: {}.".format(len(entities))) # get raw value from the set parameters if it is to be sampled once for all selected entities if op_mode == "once_for_all": params = self._get_the_set_params(params_conf) for entity in entities: # get raw value from the set parameters if it is to be sampled anew for each selected entity if op_mode == "once_for_each": params = self._get_the_set_params(params_conf) for key, value in params.items(): # used so we don't modify original key when having more than one entity key_copy = key # check if the key is a requested custom property requested_cp = False if key.startswith('cp_'): requested_cp = True key_copy = key[3:] requested_cf = False if key.startswith('cf_'): requested_cf = True key_copy = key[3:] # as an attribute of this value if hasattr(entity, key_copy) and not requested_cp: # Some properties like matrix_world would interpret numpy arrays / lists as column-wise matrices. # To make sure matrices are always interpreted row-wise, we first convert them to a mathutils matrix. if isinstance(getattr(entity, key_copy), Matrix): value = Matrix(value) setattr(entity, key_copy, value) # custom functions elif key_copy == "add_modifier" and requested_cf: self._add_modifier(entity, value) elif key_copy == "set_shading" and requested_cf: self._set_shading(entity, value) elif key_copy == "add_displace_modifier_with_texture" and requested_cf: self._add_displace(entity, value) elif key_copy == "add_uv_mapping" and requested_cf: self._add_uv_mapping(entity, value) elif key_copy == "randomize_materials" and requested_cf: self._randomize_materials(entity, value) # if key had a cp_ prefix - treat it as a custom property # values will be overwritten for existing custom property, # but if the name is new then new custom property will be created elif requested_cp: entity[key_copy] = value
[docs] def _get_the_set_params(self, params_conf: Config): """ Extracts actual values to set from a Config object. :param params_conf: Object with all user-defined data. Type: Config. :return: Parameters to set as {name of the parameter: it's value} pairs. Type: dict. """ params = {} for key in params_conf.data.keys(): if key == "cf_add_modifier": modifier_config = Config(params_conf.get_raw_dict(key)) # instruction about unpacking the data: key, corresponding Config method to extract the value, # it's default value and a postproc function instructions = {"name": (Config.get_string, None, str.upper), "thickness": (Config.get_float, None, None)} # unpack result = self._unpack_params(modifier_config, instructions) elif key == "cf_set_shading": result = {"shading_mode": params_conf.get_string("cf_set_shading"), "angle_value": params_conf.get_float("cf_shading_auto_smooth_angle_in_deg", 30)} elif key == "cf_add_displace_modifier_with_texture": displace_config = Config(params_conf.get_raw_dict(key)) # instruction about unpacking the data: key, corresponding Config method to extract the value, # it's default value and a postproc function instructions = {"texture": (Config.get_raw_value, [], None), "mid_level": (Config.get_float, 0.5, None), "subdiv_level": (Config.get_int, 2, None), "strength": (Config.get_float, 0.1, None), "min_vertices_for_subdiv": (Config.get_int, 10000, None)} # unpack result = self._unpack_params(displace_config, instructions) elif key == "cf_add_uv_mapping": uv_config = Config(params_conf.get_raw_dict(key)) # instruction about unpacking the data: key, corresponding Config method to extract the value, # it's default value and a postproc function instructions = {"projection": (Config.get_string, None, str.lower), "forced_recalc_of_uv_maps": (Config.get_bool, False, None)} # unpack result = self._unpack_params(uv_config, instructions) elif key == "cf_randomize_materials": rand_config = Config(params_conf.get_raw_dict(key)) # instruction about unpacking the data: key, corresponding Config method to extract the value, # it's default value and a postproc function instructions = {"randomization_level": (Config.get_float, 0.2, None), "add_to_objects_without_material": (Config.get_bool, False, None), "materials_to_replace_with": (Config.get_list, BlenderUtility.get_all_materials(), None), "obj_materials_cond_to_be_replaced": (Config.get_raw_dict, {}, None)} result = self._unpack_params(rand_config, instructions) result["material_to_replace_with"] = choice(result["materials_to_replace_with"]) else: result = params_conf.get_raw_value(key) params.update({key: result}) return params
[docs] def _add_modifier(self, entity: bpy.types.Object, value: dict): """ Adds modifier to a selected entity. :param entity: An entity to modify. Type: bpy.types.Object :param value: Configuration data. Type: dict. """ if value["name"] == "SOLIDIFY": bpy.context.view_layer.objects.active = entity bpy.ops.object.modifier_add(type=value["name"]) bpy.context.object.modifiers["Solidify"].thickness = value["thickness"] else: raise Exception("Unknown modifier: {}.".format(value["name"]))
[docs] def _set_shading(self, entity: bpy.types.Object, value: dict): """ Switches shading mode of the selected entity. :param entity: An entity to modify. Type: bpy.types.Object :param value: Configuration data. Type: dict. """ MeshObject(entity).set_shading_mode(value["shading_mode"], value["angle_value"])
[docs] def _add_displace(self, entity: bpy.types.Object, value: dict): """ Adds a displace modifier with texture to an object. :param entity: An object to modify. Type: bpy.types.Object. :param value: Configuration data. Type: dict. """ MeshObject(entity).add_displace_modifier( texture=value["texture"], mid_level=value["mid_level"], strength=value["strength"], min_vertices_for_subdiv=value["min_vertices_for_subdiv"], subdiv_level=value["subdiv_level"] )
[docs] def _add_uv_mapping(self, entity: bpy.types.Object, value: dict): """ Adds a uv map to an object if uv map is missing. :param entity: An object to modify. Type: bpy.types.Object. :param value: Configuration data. Type: dict. """ MeshObject(entity).add_uv_mapping(value["projection"], overwrite=value["forced_recalc_of_uv_maps"])
[docs] def _randomize_materials(self, entity: bpy.types.Object, value: dict): """ Replaces each material of an entity with certain probability. :param entity: An object to modify. Type: bpy.types.Object. :param value: Configuration data. Type: dict. """ if hasattr(entity, 'material_slots'): if entity.material_slots: for mat in entity.material_slots: use_mat = True if value["obj_materials_cond_to_be_replaced"]: use_mat = len(Material.perform_and_condition_check(value["obj_materials_cond_to_be_replaced"], [], [mat.material])) == 1 if use_mat: if np.random.uniform(0, 1) <= value["randomization_level"]: mat.material = value["material_to_replace_with"] elif value["add_to_objects_without_material"]: # this object didn't have a material before if np.random.uniform(0, 1) <= value["randomization_level"]: entity.data.materials.append(value["material_to_replace_with"])
[docs] def _unpack_params(self, param_config: Config, instructions: dict): """ Unpacks the data from a config object following the instructions in the dict. :param param_config: Structure that contains the unpacked data. Type: Config. :param instructions: Instruction for unpacking: keys, corresponding Config method to extract the value, \ default value, and a function to perform on the value after extraction. Type: dict. :return: Unpacked data. Type: dict. """ # check what was defined by the user for defined_key in param_config.data: if defined_key not in instructions: warnings.warn("Warning: key '{}' is not expected. Check spelling/docu for this cf.".format(defined_key)) result = {} # for each key and a corresponding instructions for key, (config_fct, default_val, result_fct) in instructions.items(): # check if whatever was defined as a desired Config method is callable if callable(config_fct): # extract the value of the requested type val = config_fct(param_config, key, default_val) # if a function to be applied to this value after extraction is defined - use it if result_fct: val = result_fct(val) result.update({key: val}) return result