Source code for blenderproc.python.material.MaterialLoaderUtility

"""Provides a lot of functions to deal with materials."""

import os
import random
from typing import Union, List, Optional, Dict, Any
from pathlib import Path

import bpy

from blenderproc.python.utility.MaterialGetter import MaterialGetter
from blenderproc.python.types.MaterialUtility import Material
from blenderproc.python.utility.Utility import Utility

_x_texture_node = -1500
_y_texture_node = 300


[docs] def collect_all() -> List[Optional[Material]]: """ Returns all existing materials. :return: A list of all materials. """ return convert_to_materials(bpy.data.materials)
[docs] def create(name: str) -> Material: """ Creates a new empty material. :param name: The name of the new material. :return: The new material. """ new_mat = bpy.data.materials.new(name=name) new_mat.use_nodes = True return Material(new_mat)
[docs] 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]
[docs] def find_cc_material_by_name(material_name: str, custom_properties: Dict[str, Any]) -> bpy.types.Material: """ Finds from all loaded materials the cc material, which has the given material_name and the given custom_properties. :param material_name: Name of the searched material :param custom_properties: Custom properties, which have been assigned before :return: bpy.types.Material: Return the searched material, if not found returns None """ # find used cc materials with this name cond = {"cp_is_cc_texture": True, "cp_asset_name": material_name} for key, value in custom_properties.items(): cond[key] = value new_mats = MaterialGetter.perform_and_condition_check(cond, []) if len(new_mats) == 1: new_mat = new_mats[0] return new_mat if len(new_mats) > 1: raise RuntimeError("There was more than one material found!") # the material was not even loaded return None
[docs] def is_material_used(material: bpy.types.Material): """ Checks if the given material is used on any object. :param material: Material, which should be checked :return: True if the material is used """ # check amount of usage of this material return material.users != 0
[docs] def create_new_cc_material(material_name: str, add_custom_properties: dict) -> bpy.types.Material: """ Creates a new material, which gets the given custom properties and the material name. :param material_name: The name of the material :param add_custom_properties: The custom properties, which should be added to the material :return: bpy.types.Material: Return the newly created material """ # create a new material with the name of the asset new_mat = bpy.data.materials.new(material_name) new_mat["is_cc_texture"] = True new_mat["asset_name"] = material_name new_mat.use_nodes = True for key, value in add_custom_properties.items(): if key.startswith("cp_"): cp_key = key[len("cp_"):] else: raise ValueError("All cp have to start with cp_") new_mat[cp_key] = value return new_mat
[docs] def create_image_node(nodes: bpy.types.Nodes, image: Union[str, bpy.types.Image], non_color_mode: bool = False, x_location: float = 0.0, y_location: float = 0.0): """ Creates a texture image node inside a material. :param nodes: Nodes from the current material :param image: Either the path to the image which should be loaded or the bpy.types.Image :param non_color_mode: If this True, the color mode of the image will be "Non-Color" :param x_location: X Location in the node tree :param y_location: Y Location in the node tree :return: bpy.type.Node: Return the newly constructed image node """ image_node = nodes.new('ShaderNodeTexImage') if isinstance(image, bpy.types.Image): image_node.image = image else: image_node.image = bpy.data.images.load(image, check_existing=True) if non_color_mode: image_node.image.colorspace_settings.name = 'Non-Color' image_node.location.x = x_location image_node.location.y = y_location return image_node
[docs] def add_base_color(nodes: bpy.types.Nodes, links: bpy.types.NodeLinks, base_image_path, principled_bsdf: bpy.types.Node): """ Adds base color to the principled bsdf node. :param nodes: Nodes from the current material :param links: Links from the current material :param base_image_path: Path to the base image :param principled_bsdf: Principled BSDF node of the current material :return: bpy.types.Node: The newly constructed texture node """ if os.path.exists(base_image_path): base_color = create_image_node(nodes, base_image_path, False, _x_texture_node, _y_texture_node) links.new(base_color.outputs["Color"], principled_bsdf.inputs["Base Color"]) return base_color return None
[docs] def add_ambient_occlusion(nodes: bpy.types.Nodes, links: bpy.types.NodeLinks, ambient_occlusion_image_path, principled_bsdf: bpy.types.Node, base_color: bpy.types.Node): """ Adds ambient occlusion to the principled bsdf node. :param nodes: Nodes from the current material :param links: Links from the current material :param ambient_occlusion_image_path: Path to the ambient occlusion image :param principled_bsdf: Principled BSDF node of the current material :param base_color: Base color node of the current material :return: bpy.types.Node: The newly constructed texture node """ if os.path.exists(ambient_occlusion_image_path): ao_color = create_image_node(nodes, ambient_occlusion_image_path, True, _x_texture_node, _y_texture_node * 2) math_node = nodes.new(type='ShaderNodeMixRGB') math_node.blend_type = "MULTIPLY" math_node.location.x = _x_texture_node * 0.5 math_node.location.y = _y_texture_node * 1.5 math_node.inputs["Fac"].default_value = 0.333 links.new(base_color.outputs["Color"], math_node.inputs[1]) links.new(ao_color.outputs["Color"], math_node.inputs[2]) links.new(math_node.outputs["Color"], principled_bsdf.inputs["Base Color"]) return ao_color return None
[docs] def add_metal(nodes: bpy.types.Nodes, links: bpy.types.NodeLinks, metalness_image_path: str, principled_bsdf: bpy.types.Node): """ Adds metal to the principled bsdf node. :param nodes: Nodes from the current material :param links: Links from the current material :param metalness_image_path: Path to the metal image :param principled_bsdf: Principled BSDF node of the current material :return: bpy.types.Node: The newly constructed texture node """ if os.path.exists(metalness_image_path): metallic = create_image_node(nodes, metalness_image_path, True, _x_texture_node, 0) links.new(metallic.outputs["Color"], principled_bsdf.inputs["Metallic"]) return metallic return None
[docs] def add_roughness(nodes: bpy.types.Nodes, links: bpy.types.NodeLinks, roughness_image_path: str, principled_bsdf: bpy.types.Node): """ Adds roughness to the principled bsdf node. :param nodes: Nodes from the current material :param links: Links from the current material :param roughness_image_path: Path to the metal image :param principled_bsdf: Principled BSDF node of the current material :return: bpy.types.Node: The newly constructed texture node """ if os.path.exists(roughness_image_path): roughness_texture = create_image_node(nodes, roughness_image_path, True, _x_texture_node, _y_texture_node * -1) links.new(roughness_texture.outputs["Color"], principled_bsdf.inputs["Roughness"]) return roughness_texture return None
[docs] def add_specular(nodes: bpy.types.Nodes, links: bpy.types.NodeLinks, specular_image_path: str, principled_bsdf: bpy.types.Node): """ Adds specular to the principled bsdf node. :param nodes: Nodes from the current material :param links: Links from the current material :param specular_image_path: Path to the metal image :param principled_bsdf: Principled BSDF node of the current material :return: bpy.types.Node: The newly constructed texture node """ if os.path.exists(specular_image_path): specular_texture = create_image_node(nodes, specular_image_path, True, _x_texture_node, 0) links.new(specular_texture.outputs["Color"], principled_bsdf.inputs["Specular"]) return specular_texture return None
[docs] def add_alpha(nodes: bpy.types.Nodes, links: bpy.types.NodeLinks, alpha_image_path: str, principled_bsdf: bpy.types.Node): """ Adds alpha to the principled bsdf node. :param nodes: Nodes from the current material :param links: Links from the current material :param alpha_image_path: Path to the metal image :param principled_bsdf: Principled BSDF node of the current material :return: bpy.types.Node: The newly constructed texture node """ if os.path.exists(alpha_image_path): alpha_texture = create_image_node(nodes, alpha_image_path, True, _x_texture_node, _y_texture_node * -2) links.new(alpha_texture.outputs["Color"], principled_bsdf.inputs["Alpha"]) return alpha_texture return None
[docs] def add_normal(nodes: bpy.types.Nodes, links: bpy.types.NodeLinks, normal_image_path: str, principled_bsdf: bpy.types.Node, invert_y_channel: bool): """ Adds normal to the principled bsdf node. :param nodes: Nodes from the current material :param links: Links from the current material :param normal_image_path: Path to the metal image :param principled_bsdf: Principled BSDF node of the current material :param invert_y_channel: If this is True the Y Color Channel is inverted. :return: bpy.types.Node: The newly constructed texture node """ normal_y_value = _y_texture_node * -3 if os.path.exists(normal_image_path): normal_texture = create_image_node(nodes, normal_image_path, True, _x_texture_node, normal_y_value) if invert_y_channel: separate_rgba = nodes.new('ShaderNodeSeparateRGB') separate_rgba.location.x = 4.0 / 5.0 * _x_texture_node separate_rgba.location.y = normal_y_value links.new(normal_texture.outputs["Color"], separate_rgba.inputs["Image"]) invert_node = nodes.new("ShaderNodeInvert") invert_node.inputs["Fac"].default_value = 1.0 invert_node.location.x = 3.0 / 5.0 * _x_texture_node invert_node.location.y = normal_y_value links.new(separate_rgba.outputs["G"], invert_node.inputs["Color"]) combine_rgba = nodes.new('ShaderNodeCombineRGB') combine_rgba.location.x = 2.0 / 5.0 * _x_texture_node combine_rgba.location.y = normal_y_value links.new(separate_rgba.outputs["R"], combine_rgba.inputs["R"]) links.new(invert_node.outputs["Color"], combine_rgba.inputs["G"]) links.new(separate_rgba.outputs["B"], combine_rgba.inputs["B"]) current_output = combine_rgba.outputs["Image"] else: current_output = normal_texture.outputs["Color"] normal_map = nodes.new("ShaderNodeNormalMap") normal_map.inputs["Strength"].default_value = 1.0 normal_map.location.x = 1.0 / 5.0 * _x_texture_node normal_map.location.y = normal_y_value links.new(current_output, normal_map.inputs["Color"]) links.new(normal_map.outputs["Normal"], principled_bsdf.inputs["Normal"]) return normal_texture return None
[docs] def add_bump(nodes: bpy.types.Nodes, links: bpy.types.NodeLinks, bump_image_path: str, principled_bsdf: bpy.types.Node): """ Adds bump to the principled bsdf node. :param nodes: Nodes from the current material :param links: Links from the current material :param bump_image_path: Path to the metal image :param principled_bsdf: Principled BSDF node of the current material :return: bpy.types.Node: The newly constructed texture node """ bump_y_value = _y_texture_node * -3 if os.path.exists(bump_image_path): bump_texture = create_image_node(nodes, bump_image_path, True, _x_texture_node, bump_y_value) bump_map = nodes.new("ShaderNodeBumpMap") bump_map.inputs["Strength"].default_value = 1.0 bump_map.location.x = 1.0 / 5.0 * _x_texture_node bump_map.location.y = bump_y_value links.new(bump_texture.outputs["Color"], bump_map.inputs["Heights"]) links.new(bump_map.outputs["Normal"], principled_bsdf.inputs["Normal"]) return bump_texture return None
[docs] def add_displacement(nodes: bpy.types.Nodes, links: bpy.types.NodeLinks, displacement_image_path: str, output_node: bpy.types.Node): """ Adds bump to the principled bsdf node. :param nodes: Nodes from the current material :param links: Links from the current material :param displacement_image_path: Path to the metal image :param output_node: Output node of the current material :return: bpy.types.Node: The newly constructed texture node """ if os.path.exists(displacement_image_path): displacement_texture = create_image_node(nodes, displacement_image_path, True, _x_texture_node, _y_texture_node * -4) displacement_node = nodes.new("ShaderNodeDisplacement") displacement_node.inputs["Midlevel"].default_value = 0.5 displacement_node.inputs["Scale"].default_value = 0.15 displacement_node.location.x = _x_texture_node * 0.5 displacement_node.location.y = _y_texture_node * -4 links.new(displacement_texture.outputs["Color"], displacement_node.inputs["Height"]) links.new(displacement_node.outputs["Displacement"], output_node.inputs["Displacement"]) return displacement_texture return None
[docs] def connect_uv_maps(nodes: bpy.types.Nodes, links: bpy.types.NodeLinks, collection_of_texture_nodes: list): """ Connect all given texture nodes to a newly constructed UV node. :param nodes: Nodes from the current material :param links: Links from the current material :param collection_of_texture_nodes: List of :class: `bpy.type.Node` of type :class: `ShaderNodeTexImage` """ if len(collection_of_texture_nodes) > 0: texture_coords = nodes.new("ShaderNodeTexCoord") texture_coords.location.x = _x_texture_node * 1.4 mapping_node = nodes.new("ShaderNodeMapping") mapping_node.location.x = _x_texture_node * 1.2 links.new(texture_coords.outputs["UV"], mapping_node.inputs["Vector"]) for texture_node in collection_of_texture_nodes: if texture_node is not None: links.new(mapping_node.outputs["Vector"], texture_node.inputs["Vector"])
[docs] def add_alpha_channel_to_textures(blurry_edges): """ Adds transparency to all textures, which contain an .png image as an image input :param blurry_edges: If True, the edges of the alpha channel might be blurry, this causes errors if the alpha channel should only be 0 or 1 Be careful, when you replace the original texture with something else (Segmentation, ...), the necessary texture node gets lost. By copying it into a new material as done in the SegMapRenderer, you can keep the transparency even for those nodes. """ obj_with_mats = [obj for obj in bpy.context.scene.objects if hasattr(obj.data, 'materials')] visited_materials = set() # walk over all objects, which have materials for obj in obj_with_mats: for slot in obj.material_slots: material = slot.material if material is None: # this can happen if a material slot was created but no material was assigned continue if material.name in visited_materials: # skip a material if it has been used before continue visited_materials.add(material.name) texture_node = None # check each node of the material for node in material.node_tree.nodes: # if it is a texture image node if 'TexImage' in node.bl_idname: if '.png' in node.image.name: # contains an alpha channel texture_node = node # this material contains an alpha png texture if texture_node is not None: nodes = material.node_tree.nodes links = material.node_tree.links node_connected_to_the_output, material_output = \ Utility.get_node_connected_to_the_output_and_unlink_it(material) if node_connected_to_the_output is not None: mix_node = nodes.new(type='ShaderNodeMixShader') # avoid blurry edges on the edges important for Normal, SegMapRenderer and others if blurry_edges: # add the alpha channel of the image to the mix shader node as a factor links.new(texture_node.outputs['Alpha'], mix_node.inputs['Fac']) else: # Map all alpha values to 0 or 1 by applying the step function: 1 if x > 0.5 else 0 step_function_node = nodes.new("ShaderNodeMath") step_function_node.operation = "GREATER_THAN" links.new(texture_node.outputs['Alpha'], step_function_node.inputs['Value']) links.new(step_function_node.outputs['Value'], mix_node.inputs['Fac']) links.new(node_connected_to_the_output.outputs[0], mix_node.inputs[2]) transparent_node = nodes.new(type='ShaderNodeBsdfTransparent') links.new(transparent_node.outputs['BSDF'], mix_node.inputs[1]) # connect to material output links.new(mix_node.outputs['Shader'], material_output.inputs['Surface']) else: raise RuntimeError(f"Could not find shader node, which is connected to the material " f"output for: {slot.name}")
[docs] def add_alpha_texture_node(used_material, new_material): """ Adds to a predefined new_material a texture node from an existing material (used_material) This is necessary to connect it later on in the add_alpha_channel_to_textures :param used_material: existing material, which might contain a texture node with a .png texture :param new_material: a new material, which will get a copy of this texture node :return: the modified new_material, if no texture node was found, the original new_material """ if used_material is None: # this can happen if a material slot was created but no material was assigned return used_material # find out if there is an .png file used here texture_node = None for node in used_material.node_tree.nodes: # if it is a texture image node if 'TexImage' in node.bl_idname: if '.png' in node.image.name: # contains an alpha channel texture_node = node # this material contains an alpha png texture if texture_node is not None: new_mat_alpha = new_material.copy() # copy the material nodes = new_mat_alpha.node_tree.nodes # copy the texture node into the new material to make sure it is used new_tex_node = nodes.new(type='ShaderNodeTexImage') new_tex_node.image = texture_node.image # use the new material return new_mat_alpha return new_material
[docs] def change_to_texture_less_render(use_alpha_channel): """ Changes the materials, which do not contain a emission shader to a white slightly glossy texture :param use_alpha_channel: If true, the alpha channel stored in .png textures is used. """ new_mat = bpy.data.materials.new(name="TextureLess") new_mat.use_nodes = True nodes = new_mat.node_tree.nodes principled_bsdf = Utility.get_the_one_node_with_type(nodes, "BsdfPrincipled") # setting the color values for the shader principled_bsdf.inputs['Specular'].default_value = 0.65 # specular principled_bsdf.inputs['Roughness'].default_value = 0.2 # roughness for used_object in [obj for obj in bpy.context.scene.objects if hasattr(obj.data, 'materials')]: # replace all materials with the new texture less material for slot in used_object.material_slots: emission_shader = False # check if the material contains an emission shader: for node in slot.material.node_tree.nodes: # check if one of the shader nodes is a Emission Shader if 'Emission' in node.bl_idname: emission_shader = True break # only replace materials, which do not contain any emission shader if not emission_shader: if use_alpha_channel: slot.material = add_alpha_texture_node(slot.material, new_mat) else: slot.material = new_mat
[docs] def create_procedural_texture(pattern_name: Optional[str] = None) -> bpy.types.Texture: """ Creates a new procedural texture based on a specified pattern. :param pattern_name: The name of the pattern. Available: ["CLOUDS", "DISTORTED_NOISE", "MAGIC", "MARBLE", "MUSGRAVE", "NOISE", "STUCCI", "VORONOI", "WOOD"]. If None is given, a random pattern is used. :return: The created texture """ possible_patterns = ["CLOUDS", "DISTORTED_NOISE", "MAGIC", "MARBLE", "MUSGRAVE", "NOISE", "STUCCI", "VORONOI", "WOOD"] # If no pattern has been given, use a random one, otherwise check whether the given pattern is valid. if pattern_name is None: pattern_name = random.choice(possible_patterns) else: pattern_name = pattern_name.upper() if pattern_name not in possible_patterns: raise RuntimeError(f"There is no such pattern: {pattern_name}. Allowed patterns are: {possible_patterns}") return bpy.data.textures.new(f"ct_{pattern_name}", pattern_name)
[docs] def create_material_from_texture(texture: Union[Path, str, bpy.types.Image], material_name: str) -> Material: """ Creates a material based on a given texture, the texture can either be a path to a texture file on disc or a already loaded bpy.types.Image. :param texture: either a path to an image, or a loaded bpy.types.Image texture :param material_name: name of the newly created material :return: the newly created material, which uses the texture as Base Color """ texture_path: Optional[Path] = None if isinstance(texture, (Path, str)): texture_path = Path(texture) elif not isinstance(texture, bpy.types.Image): raise TypeError(f"The given type of texture must be either [str, Path, bpy.types.Image] " f"and not {type(texture)}.") # if a texture path was set, load the image if texture_path: if texture_path.exists(): texture = bpy.data.images.load(str(texture_path), check_existing=True) else: raise FileNotFoundError(f"The given texture path could not be found: \"{texture_path}\"") if isinstance(texture, bpy.types.Image): new_mat = bpy.data.materials.new(material_name) new_mat.use_nodes = True bp_mat = Material(new_mat) bp_mat.set_principled_shader_value("Base Color", texture) return bp_mat raise TypeError("The texture variable should be a bpy.types.Image at this point!")