Пример #1
0
    def _switch_to_emission_shader(self, material: Material, value: dict):
        """ Adds the Emission shader to the target material, sets it's color and strength values, connects it to
            the Material Output node.

        :param material: Material to be modified.
        :param value: Light color and strength data.
        """

        if ('strength_keyframe' in value):
            strength_keyframe = value['strength_keyframe']
            strength = strength_keyframe[0]
        else:
            strength_keyframe = None
            strength = value['strength']

        if ('color_keyframe' in value):
            color_keyframe = value['color_keyframe']
            emission_color = None
        else:
            color_keyframe = None
            emission_color = value['color']

        material.make_emissive(emission_strength=strength,
                               emission_color=emission_color,
                               replace=True,
                               keep_using_base_color=False,
                               color_keyframe=color_keyframe,
                               strength_keyframe=strength_keyframe)
Пример #2
0
    def _map_vertex_color(material: Material, layer_name: str):
        """ Replaces the material with a mapping of the vertex color to a background color node.

        :param material: Material to be modified.
        :param layer_name: Name of the vertex color layer.
        """
        material.map_vertex_color(layer_name)
Пример #3
0
    def _adjust_material_nodes(mat: Material, adjustments: dict):
        """ Adjust the material node of the given material according to the given adjustments.

        Textures or diffuse colors will be changed according to the given material_adjustments.

        :param mat: The blender material.
        :param adjustments: A dict containing a new "diffuse" color or a new "texture" path
        """

        if "diffuse" in adjustments:
            principle_node = mat.get_the_one_node_with_type("BsdfPrincipled")
            principle_node.inputs[
                'Base Color'].default_value = Utility.hex_to_rgba(
                    adjustments["diffuse"])

        if "texture" in adjustments:
            image_path = os.path.join(SuncgLoader._suncg_dir, "texture",
                                      adjustments["texture"])
            image_path = Utility.resolve_path(image_path)

            if os.path.exists(image_path + ".png"):
                image_path += ".png"
            else:
                image_path += ".jpg"

            image_node = mat.get_the_one_node_with_type("ShaderNodeTexImage")
            if os.path.exists(image_path):
                image_node.image = bpy.data.images.load(image_path,
                                                        check_existing=True)
            else:
                print(
                    "Warning: Cannot load texture, path does not exist: {}, remove image node again"
                    .format(image_path))
                mat.remove_node(image_node)
Пример #4
0
    def _get_type_and_value_from_mat(mat: Material) -> Tuple[str, str]:
        """
        Returns the type of the material -> either diffuse or with texture (there are only two in SUNCG)

        :param mat: the material where the type and value should be determined
        :return: mat_type, value: mat_type is either "diffuse" or "texture", the value contains either name of the \
                                 image or the color mapped to an RGB string of the values
        """
        image_node = mat.get_nodes_with_type('TexImage')
        if len(image_node) == 1:
            # there is an image node -> type texture
            mat_type = "texture"
            image_node = image_node[0]
            if image_node.image is None:
                raise Exception(
                    "The image does not have a texture for material: {}".
                    format(mat.get_name()))
            value = image_node.image.name
            if "." in value:
                value = value[:value.find(".")]
        else:
            mat_type = "diffuse"
            principled_node = mat.get_the_one_node_with_type("BsdfPrincipled")
            used_keys = list(
                principled_node.inputs["Base Color"].default_value)
            alpha = principled_node.inputs['Alpha'].default_value
            used_keys.append(alpha)
            value = "_".join([str(int(255. * ele)) for ele in used_keys])
        return mat_type, value
Пример #5
0
    def _switch_to_emission_shader(self, material: Material, value: dict):
        """ Adds the Emission shader to the target material, sets it's color and strength values, connects it to
            the Material Output node.

        :param material: Material to be modified.
        :param value: Light color and strength data.
        """
        material.make_emissive(emission_strength=value["strength"], emission_color=value["color"], replace=True, keep_using_base_color=False)
Пример #6
0
    def _set_textures(self, loaded_textures: dict, material: Material):
        """ Creates a ShaderNodeTexImage node, assigns a loaded image to it and connects it to the shader of the
            selected material.

        :param loaded_textures: Loaded texture data.
        :param material: Material to be modified.
        """
        # for each Image Texture node set a texture (image) if one was loaded
        for key, texture in loaded_textures.items():
            material.set_principled_shader_value(key, texture)
Пример #7
0
    def _op_principled_shader_value(material: Material, shader_input_key: str, value: Any, operation: str):
        """
        Sets or adds the given value to the shader_input_key of the principled shader in the material

        :param material: Material to be modified.
        :param shader_input_key: Name of the shader's input.
        :param value: Value to set.
        """
        principled_bsdf = material.get_the_one_node_with_type("BsdfPrincipled")
        shader_input_key_copy = shader_input_key.replace("_", " ").title()
        if principled_bsdf.inputs[shader_input_key_copy].links:
            material.links.remove(principled_bsdf.inputs[shader_input_key_copy].links[0])
        if shader_input_key_copy in principled_bsdf.inputs:
            if operation == "set":
                principled_bsdf.inputs[shader_input_key_copy].default_value = value
            elif operation == "add":
                if isinstance(principled_bsdf.inputs[shader_input_key_copy].default_value, float):
                    principled_bsdf.inputs[shader_input_key_copy].default_value += value
                else:
                    if len(principled_bsdf.inputs[shader_input_key_copy].default_value) != len(value):
                        raise Exception(f"The shapder input key '{shader_input_key_copy}' needs a value with "
                                        f"{len(principled_bsdf.inputs[shader_input_key_copy].default_value)} "
                                        f"dimensions, the used config value only has {len(value)} dimensions.")
                    for i in range(len(principled_bsdf.inputs[shader_input_key_copy].default_value)):
                        principled_bsdf.inputs[shader_input_key_copy].default_value[i] += value[i]
        else:
            raise Exception("Shader input key '{}' is not a part of the shader.".format(shader_input_key_copy))
Пример #8
0
    def new_material(self, name: str):
        """ Creates a new material and adds it to the object.

        :param name: The name of the new material.
        """
        new_mat = Material.create(name)
        self.add_material(new_mat)
        return new_mat
Пример #9
0
    def _link_color_to_displacement_for_mat(material: Material, multiply_factor: float):
        """ Link the output of the texture image to the displacement. Fails if there is more than one texture image.

        :param material: Material to be modified.
        :param multiply_factor: Multiplication factor of the displacement.
        """
        output = material.get_the_one_node_with_type("OutputMaterial")
        texture = material.get_nodes_with_type("ShaderNodeTexImage")
        if texture is not None:
            if len(texture) == 1:
                texture = texture[0]
                math_node = material.new_node('ShaderNodeMath')
                math_node.operation = "MULTIPLY"
                math_node.inputs[1].default_value = multiply_factor
                material.link(texture.outputs["Color"], math_node.inputs[0])
                material.link(math_node.outputs["Value"], output.inputs["Displacement"])
            else:
                raise Exception("The amount of output and texture nodes of the material '{}' is not supported by "
                                "this custom function.".format(material.get_name()))
Пример #10
0
    def _recreate_material_nodes(mat: Material, force_texture: bool):
        """ Remove all nodes and recreate a diffuse node, optionally with texture.

        This will replace all material nodes with only a diffuse and a texturing node (to speedup rendering).

        :param mat: The blender material
        :param force_texture: True, if there always should be a texture node created even if the material has at the moment no texture
        """
        image_node = mat.get_nodes_with_type('TexImage')
        # if there is no image no create one
        if force_texture and len(image_node) == 0:
            # The principled BSDF node contains all imported material properties
            principled_node = mat.get_the_one_node_with_type("BsdfPrincipled")

            uv_node = mat.new_node('ShaderNodeTexCoord')
            # create an image node and link it
            image_node = mat.new_node('ShaderNodeTexImage')
            mat.link(uv_node.outputs['UV'], image_node.inputs['Vector'])
            mat.link(image_node.outputs['Color'],
                     principled_node.inputs['Base Color'])
Пример #11
0
    def run(self):
        # use a loader module to load objects
        bpy.ops.object.select_all(action='SELECT')
        previously_selected_objects = set(bpy.context.selected_objects)
        module_list_config = self.config.get_list("used_loader_config")
        modules = Utility.initialize_modules(module_list_config)
        for module in modules:
            print("Running module " + module.__class__.__name__)
            module.run()
        bpy.ops.object.select_all(action='SELECT')
        loaded_objects = list(
            set(bpy.context.selected_objects) - previously_selected_objects)

        # only select non see through materials
        config = {
            "conditions": {
                "cp_is_cc_texture": True,
                "cf_principled_bsdf_Alpha_eq": 1.0
            }
        }
        material_getter = MaterialProvider(Config(config))
        all_cc_materials = Material.convert_to_materials(material_getter.run())

        RandomRoomConstructor.construct(
            used_floor_area=self.used_floor_area,
            interior_objects=MeshObject.convert_to_meshes(loaded_objects),
            materials=all_cc_materials,
            amount_of_extrusions=self.amount_of_extrusions,
            fac_from_square_room=self.fac_from_square_room,
            corridor_width=self.corridor_width,
            wall_height=self.wall_height,
            amount_of_floor_cuts=self.amount_of_floor_cuts,
            only_use_big_edges=self.only_use_big_edges,
            create_ceiling=self.create_ceiling,
            assign_material_to_ceiling=self.assign_material_to_ceiling,
            placement_tries_per_face=self.tries_per_face,
            amount_of_objects_per_sq_meter=self.amount_of_objects_per_sq_meter)
Пример #12
0
    def run(self):
        """ Sets according values of defined attributes or applies custom functions to the selected materials.
            1. Select materials.
            2. For each parameter to modify, set it's value to all selected objects.
        """
        set_params = {}
        sel_objs = {}
        for key in self.config.data.keys():
            # if its not a selector -> to the set parameters dict
            if key != 'selector':
                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
        materials = sel_conf.get_list("selector")
        materials = Material.convert_to_materials(materials)

        op_mode = self.config.get_string("mode", "once_for_each")

        if not materials:
            warnings.warn("Warning: No materials selected inside of the MaterialManipulator")
            return

        if op_mode == "once_for_all":
            # get values to set if they are to be set/sampled once for all selected materials
            params = self._get_the_set_params(params_conf)

        for material in materials:
            if not material.blender_obj.use_nodes:
                raise Exception("This material does not use nodes -> not supported here.")

            if op_mode == "once_for_each":
                # get values to set if they are to be set/sampled anew for each selected entity
                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 material
                key_copy = key

                requested_cf = False
                if key.startswith('cf_'):
                    requested_cf = True
                    key_copy = key[3:]

                # if an attribute with such name exists for this entity
                if key_copy == "color_link_to_displacement" and requested_cf:
                    MaterialManipulator._link_color_to_displacement_for_mat(material, value)
                elif key_copy == "change_to_vertex_color" and requested_cf:
                    MaterialManipulator._map_vertex_color(material, value)
                elif key_copy == "textures" and requested_cf:
                    loaded_textures = self._load_textures(value)
                    self._set_textures(loaded_textures, material)
                elif key_copy == "switch_to_emission_shader" and requested_cf:
                    self._switch_to_emission_shader(material, value)
                elif key_copy == "infuse_texture" and requested_cf:
                    MaterialManipulator._infuse_texture(material, value)
                elif key_copy == "infuse_material" and requested_cf:
                    MaterialManipulator._infuse_material(material, value)
                elif key_copy == "add_dust" and requested_cf:
                    self._add_dust_to_material(material, value)
                elif "set_" in key_copy and requested_cf:
                    # sets the value of the principled shader
                    self._op_principled_shader_value(material, key_copy[len("set_"):], value, "set")
                elif "add_" in key_copy and requested_cf:
                    # sets the value of the principled shader
                    self._op_principled_shader_value(material, key_copy[len("add_"):], value, "add")
                elif hasattr(material, key_copy):
                    # set the value
                    setattr(material, key_copy, value)
Пример #13
0
    def _infuse_material(material: Material, config: Config):
        """
        Infuse a material inside of 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 include the Surface (Color) and also the displacement and volume. For displacement mix means multiply.

        :param material: Used material
        :param config: Used config
        """
        # determine the mode
        used_mode = config.get_string("mode", "mix").lower()
        if used_mode not in ["add", "mix"]:
            raise Exception(f'This mode is unknown here: {used_mode}, only ["mix", "add"]!')

        mix_strength = 0.0
        if used_mode == "mix":
            mix_strength = config.get_float("mix_strength", 0.5)
        elif used_mode == "add" and config.has_param("mix_strength"):
            raise Exception("The mix_strength only works in the mix mode not in the add mode!")

        # get the material, which will be used to infuse the given material
        used_materials = config.get_list("used_material")
        if len(used_materials) == 0:
            raise Exception(f"You have to select a material, which is {used_mode}ed over the material!")

        used_material = random.choice(used_materials)

        # move the copied material inside of a group
        group_node = material.new_node("ShaderNodeGroup")
        group = BlenderUtility.add_nodes_to_group(used_material.node_tree.nodes,
                                                  f"{used_mode.title()}_{used_material.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 = material.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 = material.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 = material.new_node('ShaderNodeMixShader')
                        infuse_node.inputs[0].default_value = mix_strength
                        input_offset = 1
                    elif used_mode == "add":
                        infuse_node = material.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:
                    material.link(link.from_socket, infuse_node.inputs[input_offset])
                material.link(group_node.outputs[mat_output_input.name], infuse_node.inputs[input_offset + 1])
                material.link(infuse_output, mat_output_input)
Пример #14
0
    def get_materials(self) -> List[Material]:
        """ Returns the materials used by the mesh.

        :return: A list of materials.
        """
        return Material.convert_to_materials(self.blender_obj.data.materials)
Пример #15
0
    def _create_mesh_objects_from_file(data: dict,
                                       ceiling_light_strength: float,
                                       mapping: dict,
                                       json_path: str) -> List[MeshObject]:
        """
        This creates for a given data json block all defined meshes and assigns the correct materials.
        This means that the json file contains some mesh, like walls and floors, which have to built up manually.

        It also already adds the lighting for the ceiling

        :param data: json data dir. Must contain "material" and "mesh"
        :param ceiling_light_strength: Strength of the emission shader used in the ceiling.
        :param mapping: A dict which maps the names of the objects to ids.
        :param json_path: Path to the json file, where the house information is stored.
        :return: The list of loaded mesh objects.
        """
        # extract all used materials -> there are more materials defined than used
        used_materials = []
        for mat in data["material"]:
            used_materials.append({
                "uid": mat["uid"],
                "texture": mat["texture"],
                "normaltexture": mat["normaltexture"],
                "color": mat["color"]
            })

        created_objects = []
        for mesh_data in data["mesh"]:
            # extract the obj name, which also is used as the category_id name
            used_obj_name = mesh_data["type"].strip()
            if used_obj_name == "":
                used_obj_name = "void"
            if "material" not in mesh_data:
                warnings.warn(
                    f"Material is not defined for {used_obj_name} in this file: {json_path}"
                )
                continue
            # create a new mesh
            obj = MeshObject.create_with_empty_mesh(used_obj_name,
                                                    used_obj_name + "_mesh")
            created_objects.append(obj)

            # set two custom properties, first that it is a 3D_future object and second the category_id
            obj.set_cp("is_3D_future", True)
            obj.set_cp("category_id", mapping[used_obj_name.lower()])

            # get the material uid of the current mesh data
            current_mat = mesh_data["material"]
            used_mat = None
            # search in the used materials after this uid
            for u_mat in used_materials:
                if u_mat["uid"] == current_mat:
                    used_mat = u_mat
                    break
            # If there should be a material used
            if used_mat:
                if used_mat["texture"]:
                    raise Exception(
                        "The material should use a texture, this was not implemented yet!"
                    )
                if used_mat["normaltexture"]:
                    raise Exception(
                        "The material should use a normal texture, this was not implemented yet!"
                    )
                # if there is a normal color used
                if used_mat["color"]:
                    # Create a new material
                    mat = Material.create(name=used_obj_name + "_material")
                    # create a principled node and set the default color
                    principled_node = mat.get_the_one_node_with_type(
                        "BsdfPrincipled")
                    principled_node.inputs[
                        "Base Color"].default_value = mathutils.Vector(
                            used_mat["color"]) / 255.0
                    # if the object is a ceiling add some light output
                    if "ceiling" in used_obj_name.lower():
                        mat.make_emissive(ceiling_light_strength,
                                          keep_using_base_color=False,
                                          emission_color=mathutils.Vector(
                                              used_mat["color"]) / 255.0)

                    # as this material was just created the material is just append it to the empty list
                    obj.add_material(mat)

            # extract the vertices from the mesh_data
            vert = [float(ele) for ele in mesh_data["xyz"]]
            # extract the faces from the mesh_data
            faces = mesh_data["faces"]
            # extract the normals from the mesh_data
            normal = [float(ele) for ele in mesh_data["normal"]]

            # map those to the blender coordinate system
            num_vertices = int(len(vert) / 3)
            vertices = np.reshape(np.array(vert), [num_vertices, 3])
            normal = np.reshape(np.array(normal), [num_vertices, 3])
            # flip the first and second value
            vertices[:, 1], vertices[:, 2] = vertices[:,
                                                      2], vertices[:,
                                                                   1].copy()
            normal[:, 1], normal[:, 2] = normal[:, 2], normal[:, 1].copy()
            # reshape back to a long list
            vertices = np.reshape(vertices, [num_vertices * 3])
            normal = np.reshape(normal, [num_vertices * 3])

            # add this new data to the mesh object
            mesh = obj.get_mesh()
            mesh.vertices.add(num_vertices)
            mesh.vertices.foreach_set("co", vertices)
            mesh.vertices.foreach_set("normal", normal)

            # link the faces as vertex indices
            num_vertex_indicies = len(faces)
            mesh.loops.add(num_vertex_indicies)
            mesh.loops.foreach_set("vertex_index", faces)

            # the loops are set based on how the faces are a ranged
            num_loops = int(num_vertex_indicies / 3)
            mesh.polygons.add(num_loops)
            # always 3 vertices form one triangle
            loop_start = np.arange(0, num_vertex_indicies, 3)
            # the total size of each triangle is therefore 3
            loop_total = [3] * num_loops
            mesh.polygons.foreach_set("loop_start", loop_start)
            mesh.polygons.foreach_set("loop_total", loop_total)

            # the uv coordinates are reshaped then the face coords are extracted
            uv_mesh_data = [
                float(ele) for ele in mesh_data["uv"] if ele is not None
            ]
            # bb1737bf-dae6-4215-bccf-fab6f584046b.json includes one mesh which only has no UV mapping
            if uv_mesh_data:
                uv = np.reshape(np.array(uv_mesh_data), [num_vertices, 2])
                used_uvs = uv[faces, :]
                # and again reshaped back to the long list
                used_uvs = np.reshape(used_uvs, [2 * num_vertex_indicies])

                mesh.uv_layers.new(name="new_uv_layer")
                mesh.uv_layers[-1].data.foreach_set("uv", used_uvs)
            else:
                warnings.warn(
                    f"This mesh {obj.name} does not have a specified uv map!")

            # this update converts the upper data into a mesh
            mesh.update()

            # the generation might fail if the data does not line up
            # this is not used as even if the data does not line up it is still able to render the objects
            # We assume that not all meshes in the dataset do conform with the mesh standards set in blender
            #result = mesh.validate(verbose=False)
            #if result:
            #    raise Exception("The generation of the mesh: {} failed!".format(used_obj_name))

        return created_objects
Пример #16
0
    def _infuse_texture(material: Material, config: Config):
        """
        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 material: Material, which will be changed
        :param config: containing the config information
        """
        used_mode = config.get_string("mode", "overlay").lower()
        if used_mode not in ["overlay", "mix", "set"]:
            raise Exception(f'This mode is unknown here: {used_mode}, only ["overlay", "mix", "set"]!')

        used_textures = config.get_list("used_texture")
        if len(used_textures) == 0:
            raise Exception(f"You have to select a texture, which is {used_mode} over the material!")

        invert_texture = config.get_bool("invert_texture", False)

        used_texture = random.choice(used_textures)
        used_connector = config.get_string("connection", "Base Color").title()
        texture_scale = config.get_float("texture_scale", 0.05)

        if config.has_param("strength") and used_mode == "set":
            raise Exception("The strength can only be used if the mode is not \"set\"!")
        strength = config.get_float("strength", 0.5)

        principled_bsdfs = material.get_nodes_with_type("BsdfPrincipled")
        if len(principled_bsdfs) != 1:
            raise Exception("This only works with materials, which have exactly one Prinicpled BSDF, "
                            "use a different selector!")
        principled_bsdf = principled_bsdfs[0]
        if used_connector not in principled_bsdf.inputs:
            raise Exception(f"The {used_connector} not an input to Principled BSDF!")

        node_connected_to_the_connector = None
        for link in material.links:
            if link.to_socket == principled_bsdf.inputs[used_connector]:
                node_connected_to_the_connector = link.from_node
                # remove this connection
                material.links.remove(link)
        if node_connected_to_the_connector is not None or used_mode == "set":
            texture_node = material.new_node("ShaderNodeTexImage")
            texture_node.image = used_texture.image
            # add texture coords to make the scaling of the dust texture possible
            texture_coords = material.new_node("ShaderNodeTexCoord")
            mapping_node = material.new_node("ShaderNodeMapping")
            mapping_node.vector_type = "TEXTURE"
            mapping_node.inputs["Scale"].default_value = [texture_scale] * 3
            material.link(texture_coords.outputs["UV"], mapping_node.inputs["Vector"])
            material.link(mapping_node.outputs["Vector"], texture_node.inputs["Vector"])
            texture_node_output = texture_node.outputs["Color"]
            if invert_texture:
                invert_node = material.new_node("ShaderNodeInvert")
                invert_node.inputs["Fac"].default_value = 1.0
                material.link(texture_node_output, invert_node.inputs["Color"])
                texture_node_output = invert_node.outputs["Color"]
            if node_connected_to_the_connector is not None and used_mode != "set":
                mix_node = material.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
                material.link(texture_node_output, mix_node.inputs["Color2"])
                # hopefully 0 is the color node!
                material.link(node_connected_to_the_connector.outputs[0], mix_node.inputs["Color1"])
                material.link(mix_node.outputs["Color"], principled_bsdf.inputs[used_connector])
            elif used_mode == "set":
                material.link(texture_node_output, principled_bsdf.inputs[used_connector])
Пример #17
0
    def load(folder_path: str = "resources/cctextures", used_assets: list = None, preload: bool = False,
             fill_used_empty_materials: bool = False, add_custom_properties: dict = None,
             use_all_materials: bool = False) -> List[Material]:
        """ This method loads all textures obtained from https://cc0textures.com, use the script
        (scripts/download_cc_textures.py) to download all the textures to your pc.

        All textures here support Physically based rendering (PBR), which makes the textures more realistic.

        All materials will have the custom property "is_cc_texture": True, which will make the selection later on easier.

        :param folder_path: The path to the downloaded cc0textures.
        :param used_assets: A list of all asset names, you want to use. The asset-name must not be typed in completely, only the
                            beginning the name starts with. By default all assets will be loaded, specified by an empty list.
        :param preload: If set true, only the material names are loaded and not the complete material.
        :param fill_used_empty_materials: If set true, the preloaded materials, which are used are now loaded completely.
        :param add_custom_properties:  A dictionary of materials and the respective properties.
        :param use_all_materials: If this is false only a selection of probably useful textures is used. This excludes \
                                  some see through texture and non tileable texture.
        :return a list of all loaded materials, if preload is active these materials do not contain any textures yet
                and have to be filled before rendering (by calling this function again, no need to save the prior
                returned list)
        """
        folder_path = Utility.resolve_path(folder_path)
        # this selected textures are probably useful for random selection
        probably_useful_texture = ["paving stones", "tiles", "wood", "fabric", "bricks", "metal", "wood floor",
                                   "ground", "rock", "concrete", "leather", "planks", "rocks", "gravel",
                                   "asphalt", "painted metal", "painted plaster", "marble", "carpet",
                                   "plastic", "roofing tiles", "bark", "metal plates", "wood siding",
                                   "terrazzo", "plaster", "paint", "corrugated steel", "painted wood", "lava"
                                   "cardboard", "clay", "diamond plate", "ice", "moss", "pipe", "candy",
                                   "chipboard", "rope", "sponge", "tactile paving", "paper", "cork",
                                   "wood chips"]
        if not use_all_materials and used_assets is None:
            used_assets = probably_useful_texture
        else:
            used_assets = [asset.lower() for asset in used_assets]

        if add_custom_properties is None:
            add_custom_properties = dict()

        if preload and fill_used_empty_materials:
            raise Exception("Preload and fill used empty materials can not be done at the same time, check config!")

        if os.path.exists(folder_path) and os.path.isdir(folder_path):
            materials = []
            for asset in os.listdir(folder_path):
                if used_assets:
                    skip_this_one = True
                    for used_asset in used_assets:
                        # lower is necessary here, as all used assets are made that that way
                        if asset.lower().startswith(used_asset.replace(" ", "")):
                            skip_this_one = False
                            break
                    if skip_this_one:
                        continue
                current_path = os.path.join(folder_path, asset)
                if os.path.isdir(current_path):
                    base_image_path = os.path.join(current_path, "{}_2K_Color.jpg".format(asset))
                    if not os.path.exists(base_image_path):
                        continue

                    # construct all image paths
                    ambient_occlusion_image_path = base_image_path.replace("Color", "AmbientOcclusion")
                    metallic_image_path = base_image_path.replace("Color", "Metalness")
                    roughness_image_path = base_image_path.replace("Color", "Roughness")
                    alpha_image_path = base_image_path.replace("Color", "Opacity")
                    normal_image_path = base_image_path.replace("Color", "Normal")
                    displacement_image_path = base_image_path.replace("Color", "Displacement")

                    # if the material was already created it only has to be searched
                    if fill_used_empty_materials:
                        new_mat = MaterialLoaderUtility.find_cc_material_by_name(asset, add_custom_properties)
                    else:
                        new_mat = MaterialLoaderUtility.create_new_cc_material(asset, add_custom_properties)

                    # if preload then the material is only created but not filled
                    if preload:
                        # Set alpha to 0 if the material has an alpha texture, so it can be detected e.q. in the material getter.
                        nodes = new_mat.node_tree.nodes
                        principled_bsdf = Utility.get_the_one_node_with_type(nodes, "BsdfPrincipled")
                        principled_bsdf.inputs["Alpha"].default_value = 0 if os.path.exists(alpha_image_path) else 1
                        # add it here for the preload case
                        materials.append(Material(new_mat))
                        continue
                    elif fill_used_empty_materials and not MaterialLoaderUtility.is_material_used(new_mat):
                        # now only the materials, which have been used should be filled
                        continue

                    # create material based on these image paths
                    CCMaterialLoader.create_material(new_mat, base_image_path, ambient_occlusion_image_path,
                                                     metallic_image_path, roughness_image_path, alpha_image_path,
                                                     normal_image_path, displacement_image_path)

                    materials.append(Material(new_mat))
            return materials
        else:
            raise Exception("The folder path does not exist: {}".format(folder_path))
Пример #18
0
    def _add_dust_to_material(self, material: Material, value: dict):
        """
        Adds a dust film to the material, where the strength determines how much dust is used.

        This will be added right before the output of the material.

        :param material: Used material
        :param value: dict with all used keys
        """

        # extract values from the config, like strength, texture_scale and used_dust_texture
        config = Config(value)
        strength = config.get_float("strength")
        texture_scale = config.get_float("texture_scale", 0.1)
        # if no texture is used, a random noise texture is generated
        texture_nodes = config.get_list("used_dust_texture", None)

        group_node = material.new_node("ShaderNodeGroup")
        group_node.width = 250
        group = bpy.data.node_groups.new(name="Dust Material", type="ShaderNodeTree")
        group_node.node_tree = group
        nodes, links = group.nodes, group.links

        # define start locations and differences, to make the debugging easier
        x_pos, x_diff = -(250 * 4), 250
        y_pos, y_diff = (x_diff * 1), x_diff

        # Extract the normal for the current material location
        geometry_node = nodes.new('ShaderNodeNewGeometry')
        geometry_node.location.x = x_pos + x_diff * 0
        geometry_node.location.y = y_pos
        # this node clips the values, to avoid negative values in the usage below
        clip_mix_node = nodes.new("ShaderNodeMixRGB")
        clip_mix_node.inputs["Fac"].default_value = 1.0
        clip_mix_node.use_clamp = True
        clip_mix_node.location.x = x_pos + x_diff * 1
        clip_mix_node.location.y = y_pos
        links.new(geometry_node.outputs["Normal"], clip_mix_node.inputs["Color2"])

        # use only the z component
        separate_z_normal = nodes.new("ShaderNodeSeparateRGB")
        separate_z_normal.location.x = x_pos + x_diff * 2
        separate_z_normal.location.y = y_pos
        links.new(clip_mix_node.outputs["Color"], separate_z_normal.inputs["Image"])

        # this layer weight adds a small fresnel around the object, which makes it more realistic
        layer_weight = nodes.new("ShaderNodeLayerWeight")
        layer_weight.location.x = x_pos + x_diff * 2
        layer_weight.location.y = y_pos - y_diff * 1
        layer_weight.inputs["Blend"].default_value = 0.5
        # combine it with the z component
        mix_with_layer_weight = nodes.new("ShaderNodeMixRGB")
        mix_with_layer_weight.location.x = x_pos + x_diff * 3
        mix_with_layer_weight.location.y = y_pos
        mix_with_layer_weight.inputs["Fac"].default_value = 0.2
        links.new(separate_z_normal.outputs["B"], mix_with_layer_weight.inputs["Color1"])
        links.new(layer_weight.outputs["Facing"], mix_with_layer_weight.inputs["Color2"])
        # add a gamma node, to scale the colors correctly
        gamma_node = nodes.new("ShaderNodeGamma")
        gamma_node.location.x = x_pos + x_diff * 4
        gamma_node.location.y = y_pos
        gamma_node.inputs["Gamma"].default_value = 2.2
        links.new(mix_with_layer_weight.outputs["Color"], gamma_node.inputs["Color"])

        # use an overlay node to combine it with the texture result
        overlay = nodes.new("ShaderNodeMixRGB")
        overlay.location.x = x_pos + x_diff * 5
        overlay.location.y = y_pos
        overlay.blend_type = "OVERLAY"
        overlay.inputs["Fac"].default_value = 1.0
        links.new(gamma_node.outputs["Color"], overlay.inputs["Color1"])

        # add a multiply node to scale down or up the strength of the dust
        multiply_node = nodes.new("ShaderNodeMath")
        multiply_node.location.x = x_pos + x_diff * 6
        multiply_node.location.y = y_pos
        multiply_node.inputs[1].default_value = strength
        multiply_node.operation = "MULTIPLY"
        links.new(overlay.outputs["Color"], multiply_node.inputs[0])

        # add texture coords to make the scaling of the dust texture possible
        texture_coords = nodes.new("ShaderNodeTexCoord")
        texture_coords.location.x = x_pos + x_diff * 0
        texture_coords.location.y = y_pos - y_diff * 2
        mapping_node = nodes.new("ShaderNodeMapping")
        mapping_node.location.x = x_pos + x_diff * 1
        mapping_node.location.y = y_pos - y_diff * 2
        mapping_node.vector_type = "TEXTURE"
        scale_value = texture_scale
        mapping_node.inputs["Scale"].default_value = [scale_value] * 3
        links.new(texture_coords.outputs["UV"], mapping_node.inputs["Vector"])
        # check if a texture should be used
        if texture_nodes is not None and texture_nodes:
            texture_node = nodes.new("ShaderNodeTexImage")
            texture_node.location.x = x_pos + x_diff * 2
            texture_node.location.y = y_pos - y_diff * 2
            texture_node.image = random.choice(texture_nodes).image
            links.new(mapping_node.outputs["Vector"], texture_node.inputs["Vector"])
            links.new(texture_node.outputs["Color"], overlay.inputs["Color2"])
        else:
            if not texture_nodes:
                warnings.warn("No texture was found, check the config. Random generated texture is used.")
            # if no texture is used, we great a random noise pattern, which is used instead
            noise_node = nodes.new("ShaderNodeTexNoise")
            noise_node.location.x = x_pos + x_diff * 2
            noise_node.location.y = y_pos - y_diff * 2
            # this determines the pattern of the dust flakes, a high scale makes them small enough to look like dust
            noise_node.inputs["Scale"].default_value = 250.0
            noise_node.inputs["Detail"].default_value = 0.0
            noise_node.inputs["Roughness"].default_value = 0.0
            noise_node.inputs["Distortion"].default_value = 1.9
            links.new(mapping_node.outputs["Vector"], noise_node.inputs["Vector"])
            # this noise_node produces RGB colors, we only need one value in this instance red
            separate_r_channel = nodes.new("ShaderNodeSeparateRGB")
            separate_r_channel.location.x = x_pos + x_diff * 3
            separate_r_channel.location.y = y_pos - y_diff * 2
            links.new(noise_node.outputs["Color"], separate_r_channel.inputs["Image"])
            # as the produced noise image has a nice fading to it, we use a color ramp to create dust flakes
            color_ramp = nodes.new("ShaderNodeValToRGB")
            color_ramp.location.x = x_pos + x_diff * 4
            color_ramp.location.y = y_pos - y_diff * 2
            color_ramp.color_ramp.elements[0].position = 0.4
            color_ramp.color_ramp.elements[0].color = [1, 1, 1, 1]
            color_ramp.color_ramp.elements[1].position = 0.46
            color_ramp.color_ramp.elements[1].color = [0, 0, 0, 1]
            links.new(separate_r_channel.outputs["R"], color_ramp.inputs["Fac"])
            links.new(color_ramp.outputs["Color"], overlay.inputs["Color2"])

        # combine the previous color with the new dust mode
        mix_shader = nodes.new("ShaderNodeMixShader")
        mix_shader.location = (x_pos + x_diff * 8, y_pos)
        links.new(multiply_node.outputs["Value"], mix_shader.inputs["Fac"])

        # add a bsdf node for the dust, this will be used to actually give the dust a color
        dust_color = nodes.new("ShaderNodeBsdfPrincipled")
        dust_color.location = (x_pos + x_diff * 6, y_pos - y_diff)
        # the used dust color is a grey with a tint in orange
        dust_color.inputs["Base Color"].default_value = [0.8, 0.773, 0.7, 1.0]
        dust_color.inputs["Roughness"].default_value = 1.0
        dust_color.inputs["Specular"].default_value = 0.0
        links.new(dust_color.outputs["BSDF"], mix_shader.inputs[2])

        # create the input and output nodes inside of the group
        group_output = nodes.new("NodeGroupOutput")
        group_output.location = (x_pos + x_diff * 9, y_pos)
        group_input = nodes.new("NodeGroupInput")
        group_input.location = (x_pos + x_diff * 7, y_pos - y_diff * 0.5)

        # create sockets for the outside of the group match them to the mix shader
        group.outputs.new(mix_shader.outputs[0].bl_idname, mix_shader.outputs[0].name)
        group.inputs.new(mix_shader.inputs[1].bl_idname, mix_shader.inputs[1].name)
        group.inputs.new(multiply_node.inputs[1].bl_idname, "Dust strength")
        group.inputs.new(mapping_node.inputs["Scale"].bl_idname, "Texture scale")

        # link the input and output to the mix shader
        links.new(group_input.outputs[0], mix_shader.inputs[1])
        links.new(mix_shader.outputs[0], group_output.inputs[0])
        links.new(group_input.outputs["Dust strength"], multiply_node.inputs[1])
        links.new(group_input.outputs["Texture scale"], mapping_node.inputs["Scale"])

        # remove the connection between the output and the last node and put the mix shader in between
        node_connected_to_the_output, material_output = material.get_node_connected_to_the_output_and_unlink_it()

        # place the group node above the material output
        group_node.location = (material_output.location.x - x_diff, material_output.location.y + y_diff)

        # connect the dust group
        material.link(node_connected_to_the_output.outputs[0], group_node.inputs[0])
        material.link(group_node.outputs[0], material_output.inputs["Surface"])

        # set the default values
        group_node.inputs["Dust strength"].default_value = strength
        group_node.inputs["Texture scale"].default_value = [texture_scale] * 3
Пример #19
0
    def _op_principled_shader_value(material: Material, shader_input_key: str,
                                    value: Any, operation: str):
        """
        Sets or adds the given value to the shader_input_key of the principled shader in the material

        :param material: Material to be modified.
        :param shader_input_key: Name of the shader's input.
        :param value: Value to set.
        """
        nodes = material.node_tree.nodes
        # principled_bsdf = material.get_the_one_node_with_type(nodes, "BsdfPrincipled")
        principled_bsdf = material.get_the_one_node_with_type("BsdfPrincipled")
        keyframes = False

        if (shader_input_key[-8:] == 'keyframe'):
            shader_input_key = shader_input_key[:-9]
            keyframes = True
        # shader_input_key_copy = shader_input_key.replace("_", " ").title()
        shader_input_key_copy = shader_input_key.replace(
            "_", " ").upper() if shader_input_key.replace(
                "_", " ").title() == "Ior" else shader_input_key.replace(
                    "_", " ").title()

        if principled_bsdf.inputs[shader_input_key_copy].links:
            material.links.remove(
                principled_bsdf.inputs[shader_input_key_copy].links[0])
        if shader_input_key_copy in principled_bsdf.inputs:
            if operation == "set":
                if (keyframes):
                    keyframe = 0
                    for single_value in value:
                        if bpy.context.scene.frame_end < keyframe + 1:
                            bpy.context.scene.frame_end = keyframe + 1
                        principled_bsdf.inputs[
                            shader_input_key_copy].default_value = single_value
                        principled_bsdf.inputs[
                            shader_input_key_copy].keyframe_insert(
                                data_path="default_value", frame=keyframe)
                        keyframe += 1
                else:
                    principled_bsdf.inputs[
                        shader_input_key_copy].default_value = value

            elif operation == "add":
                if isinstance(
                        principled_bsdf.inputs[shader_input_key_copy].
                        default_value, float):
                    principled_bsdf.inputs[
                        shader_input_key_copy].default_value += value
                else:
                    if len(principled_bsdf.inputs[shader_input_key_copy].
                           default_value) != len(value):
                        raise Exception(
                            f"The shapder input key '{shader_input_key_copy}' needs a value with "
                            f"{len(principled_bsdf.inputs[shader_input_key_copy].default_value)} "
                            f"dimensions, the used config value only has {len(value)} dimensions."
                        )
                    for i in range(
                            len(principled_bsdf.inputs[shader_input_key_copy].
                                default_value)):
                        principled_bsdf.inputs[
                            shader_input_key_copy].default_value[i] += value[i]
        else:
            raise Exception(
                "Shader input key '{}' is not a part of the shader.".format(
                    shader_input_key_copy))