Source code for blenderproc.python.types.MaterialUtility

""" The material class containing the texture and material properties. """

from typing import List, Union

import bpy

from blenderproc.python.utility import BlenderUtility
from blenderproc.python.types.StructUtility import Struct
from blenderproc.python.utility.Utility import Utility


[docs] class Material(Struct): """ The material class containing the texture and material properties, which are assigned to the surfaces of MeshObjects. """ def __init__(self, material: bpy.types.Material): super().__init__(material) if not material.use_nodes: raise RuntimeError(f"The given material {material.name} does not have nodes enabled and can " f"therefore not be handled by BlenderProc's Material wrapper class.") self.nodes = material.node_tree.nodes self.links = material.node_tree.links
[docs] def update_blender_ref(self, name): """ Updates the contained blender reference using the given name of the instance. :param name: The name of the instance which will be used to update its blender reference. """ self.blender_obj = bpy.data.materials[name] self.nodes = bpy.data.materials[name].node_tree.nodes self.links = bpy.data.materials[name].node_tree.links
[docs] def get_users(self) -> int: """ Returns the number of users of the material. :return: The number of users. """ return self.blender_obj.users
[docs] def duplicate(self) -> "Material": """ Duplicates the material. :return: The new material which is a copy of this one. """ return Material(self.blender_obj.copy())
[docs] def get_the_one_node_with_type(self, node_type: str, created_in_func: str = "") -> bpy.types.Node: """ Returns the one node which is of the given node_type This function will only work if there is only one of the nodes of this type. :param node_type: The node type to look for. :param created_in_func: only return node created by the specified function :return: The node. """ return Utility.get_the_one_node_with_type(self.nodes, node_type, created_in_func)
[docs] def get_nodes_with_type(self, node_type: str, created_in_func: str = "") -> List[bpy.types.Node]: """ Returns all nodes which are of the given node_type :param node_type: The note type to look for. :param created_in_func: only return nodes created by the specified function :return: The list of nodes with the given type. """ return Utility.get_nodes_with_type(self.nodes, node_type, created_in_func)
[docs] def get_nodes_created_in_func(self, created_in_func: str) -> List[bpy.types.Node]: """ Returns all nodes which are of the given node_type :param created_in_func: return all nodes created in the given function :return: The list of nodes with the given type. """ return Utility.get_nodes_created_in_func(self.nodes, created_in_func)
[docs] def new_node(self, node_type: str, created_in_func: str = "") -> bpy.types.Node: """ Creates a new node in the material's node tree. :param node_type: The desired type of the new node. :param created_in_func: Save the function name in which this node was created as a custom property. Allows to later retrieve and delete specific nodes again. :return: The new node. """ new_node = self.nodes.new(node_type) if created_in_func: new_node["created_in_func"] = created_in_func return new_node
[docs] def remove_node(self, node: bpy.types.Node): """ Removes the node from the material's node tree. :param node: The node to remove. """ self.nodes.remove(node)
[docs] def map_vertex_color(self, layer_name: str = 'Col', active_shading: bool = True): """ Maps existing vertex color to the base color of the principled bsdf node or a new background color node. :param layer_name: Name of the vertex color layer. Type: string. :param active_shading: Whether to keep the principled bsdf shader. If True, the material properties influence light reflections such as specularity, roughness, etc. alter the object's appearance. Type: bool. """ if active_shading: # create new shader node attribute attr_node = self.nodes.new(type='ShaderNodeAttribute') attr_node.attribute_name = layer_name # connect it to base color of principled bsdf principled_bsdf = self.get_the_one_node_with_type("BsdfPrincipled") self.links.new(attr_node.outputs['Color'], principled_bsdf.inputs['Base Color']) else: # create new vertex color shade node vcol = self.nodes.new(type="ShaderNodeVertexColor") vcol.layer_name = layer_name result = Utility.get_node_connected_to_the_output_and_unlink_it(self.blender_obj) node_connected_to_output, material_output = result # remove principled bsdf self.nodes.remove(node_connected_to_output) background_color_node = self.nodes.new(type="ShaderNodeBackground") if 'Color' in background_color_node.inputs: self.links.new(vcol.outputs['Color'], background_color_node.inputs['Color']) self.links.new(background_color_node.outputs["Background"], material_output.inputs["Surface"]) else: raise RuntimeError(f"Material '{self.blender_obj.name}' has no node connected to the output, " f"which has as a 'Base Color' input.")
[docs] def remove_emissive(self): """ Remove emissive part of the material. """ for node in self.get_nodes_created_in_func(self.make_emissive.__name__): self.remove_node(node)
[docs] def make_emissive(self, emission_strength: float, replace: bool = False, emission_color: List[float] = None, non_emissive_color_socket: bpy.types.NodeSocket = None): """ Makes the material emit light. :param emission_strength: The strength of the emitted light. :param replace: When replace is set to True, the existing material will be completely replaced by the emission shader, otherwise it still looks the same, while emitting light. :param emission_color: The color of the light to emit. Default: Color of the original object. :param non_emissive_color_socket: An output socket that defines how the material should look like. By default, that is the output of the principled shader node. Has no effect if replace is set to True. """ self.remove_emissive() output_node = self.get_the_one_node_with_type("OutputMaterial") if not replace: mix_node = self.new_node('ShaderNodeMixShader', self.make_emissive.__name__) if non_emissive_color_socket is None: principled_bsdf = self.get_the_one_node_with_type("BsdfPrincipled") non_emissive_color_socket = principled_bsdf.outputs['BSDF'] self.insert_node_instead_existing_link(non_emissive_color_socket, mix_node.inputs[2], mix_node.outputs['Shader'], output_node.inputs['Surface']) # The light path node returns 1, if the material is hit by a ray coming from the camera, else it # returns 0. In this way the mix shader will use the principled shader for rendering the color of # the emitting surface itself, while using the emission shader for lighting the scene. light_path_node = self.new_node('ShaderNodeLightPath', self.make_emissive.__name__) self.link(light_path_node.outputs['Is Camera Ray'], mix_node.inputs['Fac']) output_socket = mix_node.inputs[1] else: output_socket = output_node.inputs['Surface'] emission_node = self.new_node('ShaderNodeEmission', self.make_emissive.__name__) if emission_color is None: principled_bsdf = self.get_the_one_node_with_type("BsdfPrincipled") if len(principled_bsdf.inputs["Base Color"].links) == 1: # get the node connected to the Base Color socket_connected_to_the_base_color = principled_bsdf.inputs["Base Color"].links[0].from_socket self.link(socket_connected_to_the_base_color, emission_node.inputs["Color"]) else: emission_node.inputs["Color"].default_value = principled_bsdf.inputs["Base Color"].default_value else: emission_node.inputs["Color"].default_value = emission_color # set the emission strength of the shader emission_node.inputs['Strength'].default_value = emission_strength self.link(emission_node.outputs["Emission"], output_socket)
[docs] def set_principled_shader_value(self, input_name: str, value: Union[float, bpy.types.Image, bpy.types.NodeSocket]): """ Sets value of an input to the principled shader node. :param input_name: The name of the input socket of the principled shader node. :param value: The value to set. Can be a simple value to use as default_value, a socket which will be connected to the input or an image which will be used for a new TextureNode connected to the input. """ principled_bsdf = self.get_the_one_node_with_type("BsdfPrincipled") if isinstance(value, bpy.types.Image): node = self.new_node('ShaderNodeTexImage') node.label = input_name node.image = value self.link(node.outputs['Color'], principled_bsdf.inputs[input_name]) elif isinstance(value, bpy.types.NodeSocket): self.link(value, principled_bsdf.inputs[input_name]) else: if principled_bsdf.inputs[input_name].links: self.links.remove(principled_bsdf.inputs[input_name].links[0]) principled_bsdf.inputs[input_name].default_value = value
[docs] def get_principled_shader_value(self, input_name: str) -> Union[float, bpy.types.NodeSocket]: """ Gets the default value or the connected node socket to an input socket of the principled shader node of the material. :param input_name: The name of the input socket of the principled shader node. :return: the connected socket to the input socket or the default_value of the given input_name """ # get the one node from type Principled BSDF principled_bsdf = self.get_the_one_node_with_type("BsdfPrincipled") # check if the input name is a valid input if input_name in principled_bsdf.inputs: # check if there are any connections to this input socket if principled_bsdf.inputs[input_name].links: if len(principled_bsdf.inputs[input_name].links) == 1: # return the connected node return principled_bsdf.inputs[input_name].links[0].from_socket raise RuntimeError(f"The input socket has more than one input link: " f"{[link.from_node.name for link in principled_bsdf.inputs[input_name].links]}") # else return the default value return principled_bsdf.inputs[input_name].default_value raise RuntimeError(f"The input name could not be found in the inputs: {input_name}")
[docs] def infuse_texture(self, texture: bpy.types.Texture, mode: str = "overlay", connection: str = "Base Color", texture_scale: float = 0.05, strength: float = 0.5, invert_texture: bool = False): """ Overlays the selected material with a texture, this can be either a color texture like for example dirt or it can be a texture, which is used as an input to the Principled BSDF of the given material. :param texture: A texture which should be infused in the material. :param mode: The mode determines how the texture is used. There are three options: "overlay" in which the selected texture is overlayed over a preexisting one. If there is none, nothing happens. The second option: "mix" is similar to overlay, just that the textures are mixed there. The last option: "set" replaces any existing texture and is even added if there was none before. :param connection: By default the "Base Color" input of the principled shader will be used. This can be changed to any valid input of a principled shader. Default: "Base Color". For available check the blender documentation. :param texture_scale: The used texture can be scaled down or up by a factor, to make it match the preexisting UV mapping. Make sure that the object has a UV mapping beforehand. :param strength: The strength determines how much the newly generated texture is going to be used. :param invert_texture: It might be sometimes useful to invert the input texture, this can be done by setting this to True. """ used_mode = mode.lower() if used_mode not in ["overlay", "mix", "set"]: raise Exception(f'This mode is unknown here: {used_mode}, only ["overlay", "mix", "set"]!') used_connector = connection.title() principled_bsdf = self.get_the_one_node_with_type("BsdfPrincipled") if used_connector not in principled_bsdf.inputs: raise Exception(f"The {used_connector} is not an input to Principled BSDF!") node_socket_connected_to_the_connector = None for link in principled_bsdf.inputs[used_connector].links: node_socket_connected_to_the_connector = link.from_socket # remove this connection self.links.remove(link) if node_socket_connected_to_the_connector is not None or used_mode == "set": texture_node = self.new_node("ShaderNodeTexImage") texture_node.image = texture.image # add texture coords to make the scaling of the dust texture possible texture_coords = self.new_node("ShaderNodeTexCoord") mapping_node = self.new_node("ShaderNodeMapping") mapping_node.vector_type = "TEXTURE" mapping_node.inputs["Scale"].default_value = [texture_scale] * 3 self.link(texture_coords.outputs["UV"], mapping_node.inputs["Vector"]) self.link(mapping_node.outputs["Vector"], texture_node.inputs["Vector"]) texture_node_output = texture_node.outputs["Color"] if invert_texture: invert_node = self.new_node("ShaderNodeInvert") invert_node.inputs["Fac"].default_value = 1.0 self.link(texture_node_output, invert_node.inputs["Color"]) texture_node_output = invert_node.outputs["Color"] if node_socket_connected_to_the_connector is not None and used_mode != "set": mix_node = self.new_node("ShaderNodeMixRGB") if used_mode in "mix_node": mix_node.blend_type = "OVERLAY" elif used_mode in "mix": mix_node.blend_type = "MIX" mix_node.inputs["Fac"].default_value = strength self.link(texture_node_output, mix_node.inputs["Color2"]) # hopefully 0 is the color node! self.link(node_socket_connected_to_the_connector, mix_node.inputs["Color1"]) self.link(mix_node.outputs["Color"], principled_bsdf.inputs[used_connector]) elif used_mode == "set": self.link(texture_node_output, principled_bsdf.inputs[used_connector])
[docs] def infuse_material(self, material: "Material", mode: str = "mix", mix_strength: float = 0.5): """ Infuse a material inside another material. The given material, will be adapted and the used material, will be added, depending on the mode either as add or as mix. This change is applied to all outputs of the material, this includes the Surface (Color) and also the displacement and volume. For displacement mix means multiply. :param material: Material to infuse. :param mode: The mode determines how the two materials are mixed. There are two options "mix" in which the preexisting material is mixed with the selected one in "used_material" or "add" in which they are just added on top of each other. Available: ["mix", "add"] :param mix_strength: In the "mix" mode a strength can be set to determine how much of each material is going to be used. A strength of 1.0 means that the new material is going to be used completely. """ # determine the mode used_mode = mode.lower() if used_mode not in ["add", "mix"]: raise Exception(f'This mode is unknown here: {used_mode}, only ["mix", "add"]!') # move the copied material inside of a group group_node = self.new_node("ShaderNodeGroup") group = BlenderUtility.add_nodes_to_group(material.nodes, f"{used_mode.title()}_{material.get_name()}") group_node.node_tree = group # get the current material output and put the used material in between the last node and the material output material_output = self.get_the_one_node_with_type("OutputMaterial") for mat_output_input in material_output.inputs: if len(mat_output_input.links) > 0: if "Float" in mat_output_input.bl_idname or "Vector" in mat_output_input.bl_idname: # For displacement infuse_node = self.new_node("ShaderNodeMixRGB") if used_mode == "mix": # as there is no mix mode, we use multiply here, which is similar infuse_node.blend_type = "MULTIPLY" infuse_node.inputs["Fac"].default_value = mix_strength input_offset = 1 elif used_mode == "add": infuse_node.blend_type = "ADD" input_offset = 0 else: raise Exception(f"This mode is not supported here: {used_mode}!") infuse_output = infuse_node.outputs["Color"] else: # for the normal surface output (Color) if used_mode == "mix": infuse_node = self.new_node('ShaderNodeMixShader') infuse_node.inputs[0].default_value = mix_strength input_offset = 1 elif used_mode == "add": infuse_node = self.new_node('ShaderNodeMixShader') input_offset = 0 else: raise Exception(f"This mode is not supported here: {used_mode}!") infuse_output = infuse_node.outputs["Shader"] # link the infuse node with the correct group node and the material output for link in mat_output_input.links: self.link(link.from_socket, infuse_node.inputs[input_offset]) self.link(group_node.outputs[mat_output_input.name], infuse_node.inputs[input_offset + 1]) self.link(infuse_output, mat_output_input)
[docs] def set_displacement_from_principled_shader_value(self, input_name: str, multiply_factor: float): """ Connects the node that is connected to the specified input of the principled shader node with the displacement output of the material. :param input_name: The name of the input socket of the principled shader node. :param multiply_factor: A factor by which the displacement should be multiplied. """ # Find socket that is connected with the specified input of the principled shader node. input_socket = self.get_principled_shader_value(input_name) if not isinstance(input_socket, bpy.types.NodeSocket): raise Exception(f"The input {input_name} of the principled shader does not have any incoming connection.") # Create multiplication node and connect with retrieved socket math_node = self.new_node('ShaderNodeMath') math_node.operation = "MULTIPLY" math_node.inputs[1].default_value = multiply_factor self.link(input_socket, math_node.inputs[0]) # Connect multiplication node with displacement output output = self.get_the_one_node_with_type("OutputMaterial") self.link(math_node.outputs["Value"], output.inputs["Displacement"])
def __setattr__(self, key, value): if key not in ["links", "nodes", "blender_obj"]: raise RuntimeError("The API class does not allow setting any attribute. Use the corresponding method or " "directly access the blender attribute via entity.blender_obj.attribute_name") object.__setattr__(self, key, value)