def run(self): """ Loads rocks.""" rocks_settings = self.config.get_list("batches", []) for subsec_num, subsec_settings in enumerate(rocks_settings): subsec_config = Config(subsec_settings) subsec_objects = RockEssentialsRockLoader.load_rocks( path=subsec_config.get_string("path"), subsec_num=subsec_num, objects=subsec_config.get_list("objects", []), sample_objects=subsec_config.get_bool("sample_objects", False), amount=subsec_config.get_int("amount") if subsec_config.has_param("amount") else None ) RockEssentialsRockLoader.set_rocks_properties( objects=subsec_objects, physics=subsec_config.get_bool("physics", False), render_levels=subsec_config.get_int("render_levels", 3), high_detail_mode=subsec_config.get_bool("high_detail_mode", False), scale=subsec_config.get_vector3d("scale", [1, 1, 1]), reflection_amount=subsec_config.get_float("reflection_amount") if subsec_config.has_param("reflection_amount") else None, reflection_roughness=subsec_config.get_float("reflection_roughness") if subsec_config.has_param("reflection_roughness") else None, hsv=subsec_config.get_list("HSV") if subsec_config.has_param("HSV") else None )
def build_provider_based_on_config(config): """ Builds up the provider using the parameters described in the given config. The given config should follow the following scheme: .. code-block:: yaml { "provider": "<name of provider class>" "parameters": { <provider parameters> } } :param config: A Configuration object or a dict containing the configuration data. :return: The constructed provider. """ if isinstance(config, dict): config = Config(config) parameters = {} for key in config.data.keys(): if key != 'provider': parameters[key] = config.data[key] if not config.has_param('provider'): raise Exception("Each provider needs a provider label, this one does not contain one: {}".format(config.data)) return Utility.build_provider(config.get_string("provider"), parameters)
def _cam2world_matrix_from_cam_extrinsics(self, config: Config) -> np.ndarray: """ Determines camera extrinsics by using the given config and returns them in form of a cam to world frame transformation matrix. :param config: The configuration object. :return: The 4x4 cam to world transformation matrix. """ if not config.has_param("cam2world_matrix"): position = MathUtility.change_coordinate_frame_of_point( config.get_vector3d("location", [0, 0, 0]), self.source_frame) # position = Vector((-0.01111459918320179, -0.051188092678785324, 0.19301876425743103)) rotation_format = config.get_string("rotation/format", "euler") value = config.get_vector3d("rotation/value", [0, 0, 0]) # Transform to blender coord frame value = MathUtility.change_coordinate_frame_of_point( value, self.source_frame) if rotation_format == "euler": # Rotation, specified as euler angles rotation_matrix = Euler(value, 'XYZ').to_matrix() elif rotation_format == "forward_vec": # Convert forward vector to euler angle (Assume Up = Z) rotation_matrix = CameraUtility.rotation_from_forward_vec( value) elif rotation_format == "look_at": # Convert forward vector to euler angle (Assume Up = Z) rotation_matrix = CameraUtility.rotation_from_forward_vec( value - position) else: raise Exception("No such rotation format:" + str(rotation_format)) if rotation_format == "look_at" or rotation_format == "forward_vec": inplane_rot = config.get_float("rotation/inplane_rot", 0.0) rotation_matrix = np.matmul( rotation_matrix, Euler((0.0, 0.0, inplane_rot)).to_matrix()) extra_rot = config.get_vector("rotation/extra_rot", mathutils.Vector([0., 0., 0.])) #extra_rot = Vector([0.3,-0.3,-0.7841]) rotation_matrix = rotation_matrix @ Euler( extra_rot).to_matrix() # cam2world_matrix = Matrix.Translation(Vector(position)) @ rotation_matrix.to_4x4() cam2world_matrix = MathUtility.build_transformation_mat( position, rotation_matrix) else: cam2world_matrix = np.array( config.get_list("cam2world_matrix")).reshape(4, 4).astype( np.float32) cam2world_matrix = MathUtility.change_target_coordinate_frame_of_transformation_matrix( cam2world_matrix, self.source_frame) return cam2world_matrix
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)
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])
def _set_cam_intrinsics(self, cam, config): """ Sets camera intrinsics from a source with following priority 1. from config function parameter if defined 2. from custom properties of cam if set in Loader 3. default config: resolution_x/y: 512 pixel_aspect_x: 1 clip_start: : 0.1 clip_end : 1000 fov : 0.691111 :param cam: The camera which contains only camera specific attributes. :param config: A configuration object with cam intrinsics. """ if config.is_empty(): return width = config.get_int("resolution_x", bpy.context.scene.render.resolution_x) height = config.get_int("resolution_y", bpy.context.scene.render.resolution_y) # Clipping clip_start = config.get_float("clip_start", cam.clip_start) clip_end = config.get_float("clip_end", cam.clip_end) if config.has_param("cam_K"): if config.has_param("fov"): print( 'WARNING: FOV defined in config is ignored. Mutually exclusive with cam_K' ) if config.has_param("pixel_aspect_x"): print( 'WARNING: pixel_aspect_x defined in config is ignored. Mutually exclusive with cam_K' ) cam_K = np.array(config.get_list("cam_K")).reshape(3, 3).astype( np.float32) CameraUtility.set_intrinsics_from_K_matrix(cam_K, width, height, clip_start, clip_end) else: # Set FOV fov = config.get_float("fov", cam.angle) # Set Pixel Aspect Ratio pixel_aspect_x = config.get_float( "pixel_aspect_x", bpy.context.scene.render.pixel_aspect_x) pixel_aspect_y = config.get_float( "pixel_aspect_y", bpy.context.scene.render.pixel_aspect_y) # Set camera shift shift_x = config.get_float("shift_x", cam.shift_x) shift_y = config.get_float("shift_y", cam.shift_y) CameraUtility.set_intrinsics_from_blender_params(fov, width, height, clip_start, clip_end, pixel_aspect_x, pixel_aspect_y, shift_x, shift_y, lens_unit="FOV") CameraUtility.set_stereo_parameters( config.get_string("stereo_convergence_mode", cam.stereo.convergence_mode), config.get_float("convergence_distance", cam.stereo.convergence_distance), config.get_float("interocular_distance", cam.stereo.interocular_distance)) if config.has_param("depth_of_field"): depth_of_field_config = Config( config.get_raw_dict("depth_of_field")) fstop_value = depth_of_field_config.get_float("fstop", 2.4) aperture_blades = depth_of_field_config.get_int( "aperture_blades", 0) aperture_ratio = depth_of_field_config.get_float( "aperture_ratio", 1.0) aperture_rotation = depth_of_field_config.get_float( "aperture_rotation_in_rad", 0.0) if depth_of_field_config.has_param( "depth_of_field_dist") and depth_of_field_config.has_param( "focal_object"): raise RuntimeError( "You can only use either depth_of_field_dist or a focal_object but not both!" ) if depth_of_field_config.has_param("depth_of_field_dist"): depth_of_field_dist = depth_of_field_config.get_float( "depth_of_field_dist") CameraUtility.add_depth_of_field(cam, None, fstop_value, aperture_blades, aperture_rotation, aperture_ratio, depth_of_field_dist) elif depth_of_field_config.has_param("focal_object"): focal_object = depth_of_field_config.get_list("focal_object") if len(focal_object) != 1: raise RuntimeError( f"There has to be exactly one focal object, use 'random_samples: 1' or change " f"the selector. Found {len(focal_object)}.") CameraUtility.add_depth_of_field(cam, focal_object[0], fstop_value, aperture_blades, aperture_rotation, aperture_ratio) else: raise RuntimeError( "The depth_of_field dict must contain either a focal_object definition or " "a depth_of_field_dist")
def perform_and_condition_check(self, and_condition, objects): """ Checks all objects in the scene if all given conditions are true for an object, it is added to the list. :param and_condition: Given conditions. Type: dict. :param objects: Objects, that are already in the return list. Type: list. :return: Objects that fulfilled given conditions. Type: list. """ new_objects = [] # through every object for obj in bpy.context.scene.objects: # if object is in list, skip it if obj in objects: continue select_object = True # run over all conditions and check if any one of them holds, if one does not work -> go to next obj for key, value in and_condition.items(): # check if the key is a requested custom property requested_custom_property = False requested_custom_function = False if key.startswith('cp_'): requested_custom_property = True key = key[3:] if key.startswith('cf_'): requested_custom_function = True key = key[3:] # check if an attribute with this name exists and the key was not a requested custom property if hasattr(obj, key) and not requested_custom_property: # check if the type of the value of attribute matches desired if isinstance(getattr(obj, key), type(value)): new_value = value # if not, try to enforce some mathutils-specific type else: if isinstance(getattr(obj, key), mathutils.Vector): new_value = mathutils.Vector(value) elif isinstance(getattr(obj, key), mathutils.Euler): new_value = mathutils.Euler(value) elif isinstance(getattr(obj, key), mathutils.Color): new_value = mathutils.Color(value) # raise an exception if it is none of them else: raise Exception( "Types are not matching: %s and %s !" % (type(getattr(obj, key)), type(value))) # or check for equality if not ((isinstance(getattr(obj, key), str) and re.fullmatch(value, getattr(obj, key)) is not None) or getattr(obj, key) == new_value): select_object = False break # check if a custom property with this name exists elif key in obj and requested_custom_property: # check if the type of the value of such custom property matches desired if isinstance(obj[key], type(value)) or (isinstance( obj[key], int) and isinstance(value, bool)): # if is a string and if the whole string matches the given pattern if not ((isinstance(obj[key], str) and re.fullmatch(value, obj[key]) is not None) or obj[key] == value): select_object = False break # raise an exception if not else: raise Exception( "Types are not matching: {} and {} for key: {}". format(type(obj[key]), type(value), key)) elif requested_custom_function and any( [key == "inside", key == "outside"]): conditions = Config(value) if conditions.has_param("min") and conditions.has_param( "max"): if any( conditions.has_param(key) for key in [ "x_min", "x_max", "y_min", "y_max", "z_min", "z_max" ]): raise RuntimeError( "An inside/outside condition cannot mix the min/max vector syntax with " "the x_min/x_max/y_min/... syntax.") bb_min = conditions.get_vector3d("min") bb_max = conditions.get_vector3d("max") is_inside = all(bb_min[i] < obj.location[i] < bb_max[i] for i in range(3)) else: if any( conditions.has_param(key) for key in ["min", "max"]): raise RuntimeError( "An inside/outside condition cannot mix the min/max syntax with " "the x_min/x_max/y_min/... syntax.") is_inside = True for axis_index in range(3): axis_name = "xyz"[axis_index] for direction in ["min", "max"]: key_name = "{}_{}".format(axis_name, direction) if key_name in value: real_position = obj.location[axis_index] border_position = float(value[key_name]) if (direction == "max" and real_position > border_position ) or (direction == "min" and real_position < border_position): is_inside = False if (key == "inside" and not is_inside) or (key == "outside" and is_inside): select_object = False break else: select_object = False break if select_object: new_objects.append(obj) return new_objects
def perform_and_condition_check(self, and_condition, objects): """ Checks all objects in the scene if all given conditions are true for an object, it is added to the list. :param and_condition: Given conditions. Type: dict. :param objects: Objects, that are already in the return list. Type: list. :return: Objects that fulfilled given conditions. Type: list. """ # run over all conditions and check if any one of them holds, if one does not work -> go to next obj for key, value in and_condition.items(): # check if the key is a requested custom property requested_custom_property = False requested_custom_function = False if key.startswith('cp_'): requested_custom_property = True key = key[3:] if key.startswith('cf_'): requested_custom_function = True key = key[3:] if not requested_custom_property and not requested_custom_function: # Filter by normal attributes objects = Filter.by_attr(objects, key, value, regex=True) elif requested_custom_property: # Filter by custom property objects = Filter.by_cp(objects, key, value, regex=True) elif requested_custom_function: # Build boundaries of interval conditions = Config(value) if conditions.has_param("min") and conditions.has_param("max"): if any( conditions.has_param(key) for key in ["x_min", "x_max", "y_min", "y_max", "z_min", "z_max" ]): raise RuntimeError( "An inside/outside condition cannot mix the min/max vector syntax with " "the x_min/x_max/y_min/... syntax.") bb_min = conditions.get_vector3d("min") bb_max = conditions.get_vector3d("max") else: if any( conditions.has_param(key) for key in ["min", "max"]): raise RuntimeError( "An inside/outside condition cannot mix the min/max syntax with " "the x_min/x_max/y_min/... syntax.") # Set the interval +/- inf per default, so they will be ignored if they are not set bb_min = mathutils.Vector((-np.inf, -np.inf, -np.inf)) bb_max = mathutils.Vector((np.inf, np.inf, np.inf)) # Set boundaries given by config for axis_index in range(3): axis_name = "xyz"[axis_index] for direction in ["min", "max"]: key_name = "{}_{}".format(axis_name, direction) if key_name in value: if direction == "min": bb_min[axis_index] = float(value[key_name]) else: bb_max[axis_index] = float(value[key_name]) if key == "inside": objects = Filter.by_attr_in_interval( objects, "location", bb_min, bb_max) elif key == "outside": objects = Filter.by_attr_outside_interval( objects, "location", bb_min, bb_max) else: raise Exception("No such custom function: " + str(key)) return objects
def _add_dust_to_material(self, material: bpy.types.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 = None if config.has_param("used_dust_texture"): texture_nodes = config.get_list("used_dust_texture") group_node = material.node_tree.nodes.new("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 = Utility.get_node_connected_to_the_output_and_unlink_it(material) # 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.node_tree.links.new(node_connected_to_the_output.outputs[0], group_node.inputs[0]) material.node_tree.links.new(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