# Copyright (C) 2021 Victor Soupday # This file is part of CC/iC Blender Tools # # CC/iC Blender Tools is free software: you can redistribute it and/or modify # it under the terms of the GNU General Public License as published by # the Free Software Foundation, either version 3 of the License, or # (at your option) any later version. # # CC/iC Blender Tools is distributed in the hope that it will be useful, # but WITHOUT ANY WARRANTY; without even the implied warranty of # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the # GNU General Public License for more details. # # You should have received a copy of the GNU General Public License # along with CC/iC Blender Tools. If not, see . import bpy from mathutils import Vector, Euler, Matrix, Quaternion, Color from . import lib, utils, vars cursor = Vector((0,0)) cursor_top = Vector((0,0)) max_cursor = Vector((0,0)) new_nodes = [] def clear_cursor(): cursor_top.x = 0 cursor_top.y = 0 cursor.x = 0 cursor.y = 0 max_cursor.x = 0 max_cursor.y = 0 new_nodes.clear() def reset_cursor(): cursor_top.y = max_cursor.y cursor_top.x = 0 cursor.x = 0 cursor.y = cursor_top.y def advance_cursor(scale = 1.0): cursor.y = cursor_top.y - cursor_top.x cursor.x += vars.GRID_SIZE * scale if (cursor.x > max_cursor.x): max_cursor.x = cursor.x def drop_cursor(scale = 1.0): cursor.y -= vars.GRID_SIZE * scale if cursor.y < max_cursor.y: max_cursor.y = cursor.y def step_cursor(scale = 1.0, drop = 0.25): cursor_top.x += vars.GRID_SIZE * drop cursor.y = cursor_top.y - cursor_top.x cursor.x += vars.GRID_SIZE * scale if (cursor.x > max_cursor.x): max_cursor.x = cursor.x def step_cursor_if(thing, scale = 1.0, drop = 0.25): if thing is not None: step_cursor(scale, drop) def move_new_nodes(dx, dy): width = max_cursor.x height = -max_cursor.y - vars.GRID_SIZE for node in new_nodes: node.location.x += (dx) - width node.location.y += (dy) + (height / 2) clear_cursor() def make_shader_node(nodes, type, scale = 1.0): shader_node = nodes.new(type) shader_node.location = cursor new_nodes.append(shader_node) drop_cursor(scale) return shader_node ## color_space: Non-Color, sRGB def make_image_node(nodes, image, name, scale = 1.0): if image is None: return None image_node = make_shader_node(nodes, "ShaderNodeTexImage", scale) image_node.image = image image_node.name = utils.unique_name(name) return image_node def make_separate_rgb_node(nodes, label, name): if utils.B330(): sep_node = make_shader_node(nodes, "ShaderNodeSeparateColor") else: sep_node = make_shader_node(nodes, "ShaderNodeSeparateRGB") sep_node.label = label sep_node.name = utils.unique_name(name) return sep_node def make_value_node(nodes, label, name, value = 0.0): value_node = make_shader_node(nodes, "ShaderNodeValue", 0.4) value_node.label = label value_node.name = utils.unique_name(name) set_node_output_value(value_node, "Value", value) return value_node def make_mixrgb_node(nodes, blend_type): mix_node = make_shader_node(nodes, "ShaderNodeMixRGB", 0.8) mix_node.blend_type = blend_type return mix_node def make_math_node(nodes, operation, value1 = 0.5, value2 = 0.5): math_node = make_shader_node(nodes, "ShaderNodeMath", 0.6) math_node.operation = operation math_node.inputs[0].default_value = value1 math_node.inputs[1].default_value = value2 return math_node def make_bump_node(nodes, strength, distance): bump_node : bpy.types.ShaderNodeBump = make_shader_node(nodes, "ShaderNodeBump") set_node_input_value(bump_node, "Strength", strength) set_node_input_value(bump_node, "Distance", distance) return bump_node def make_normal_map_node(nodes, strength): normal_map_node : bpy.types.ShaderNodeBump = make_shader_node(nodes, "ShaderNodeNormalMap") set_node_input_value(normal_map_node, "Strength", strength) return normal_map_node def make_rgb_node(nodes, label, value = [1.0, 1.0, 1.0, 1.0]): rgb_node = make_shader_node(nodes, "ShaderNodeRGB", 0.8) rgb_node.label = label set_node_output_value(rgb_node, "Color", value) return rgb_node def make_vectormath_node(nodes, operation): vm_node = make_shader_node(nodes, "ShaderNodeVectorMath", 0.6) vm_node.operation = operation return vm_node def make_node_group_node(nodes, group, label, name): group_node = make_shader_node(nodes, "ShaderNodeGroup") group_node.node_tree = group group_node.label = label group_node.width = 240 group_node.name = utils.unique_name("(" + name + ")") return group_node def make_gltf_settings_node(nodes): gltf_group : bpy.types.NodeGroup = None for group in bpy.data.node_groups: if utils.B400(): if group.name == "glTF Material Output": gltf_group = group else: if group.name == "glTF Settings": gltf_group = group if not gltf_group: if utils.B400(): gltf_group = bpy.data.node_groups.new("glTF Material Output", "ShaderNodeTree") gltf_group.interface.new_socket("Occlusion", in_out="INPUT", socket_type="NodeSocketColor") gltf_group.interface.new_socket("Thickness", in_out="INPUT", socket_type="NodeSocketFloat") gltf_group.interface.new_socket("Specular", in_out="INPUT", socket_type="NodeSocketFloat") gltf_group.interface.new_socket("Specular Color", in_out="INPUT", socket_type="NodeSocketColor") else: gltf_group = bpy.data.node_groups.new("glTF Settings", "ShaderNodeTree") gltf_group.inputs.new("NodeSocketColor", "Occlusion") gltf_group.inputs.new("NodeSocketFloat", "Thickness") gltf_group.inputs.new("NodeSocketFloat", "Specular") gltf_group.inputs.new("NodeSocketColor", "Specular Color") return make_node_group_node(nodes, gltf_group, "glTF Settings", "glTF Settings") ## Node Socket Functions # def safe_node_output_socket(node, socket_or_name_or_number): """Return the node's socket or named output socket.""" try: if type(socket_or_name_or_number) == str: return output_socket(node, socket_or_name_or_number) elif type(socket_or_name_or_number) == int: return node.outputs[socket_or_name_or_number] else: return socket_or_name_or_number except: return None def safe_node_input_socket(node, socket_or_name_or_number): """Return the node's socket or named input socket.""" try: if type(socket_or_name_or_number) == str: return input_socket(node, socket_or_name_or_number) elif type(socket_or_name_or_number) == int: return node.inputs[socket_or_name_or_number] else: return socket_or_name_or_number except: return None def safe_socket_name(socket_or_name): """Return the supplied name or socket's name.""" try: if type(socket_or_name) == str: return socket_or_name elif type(socket_or_name) == int: return int(socket_or_name) else: return socket_or_name.name except: return None def get_node_input_color(node : bpy.types.Node, socket, default: tuple) -> tuple: """Returns the node's socket or named input sockets default color value or if linked to, the connecting node's default output value.\n Returns the supplied default value if node/socket is invalid.""" socket = safe_node_input_socket(node, socket) if node and socket: try: if socket.is_linked: connecting_node, connecting_socket = get_node_and_socket_connected_to_input(node, socket) return get_node_output_color(connecting_node, connecting_socket, default) return extract_socket_color(socket.default_value, default) except: ... return default def get_node_input_vector(node : bpy.types.Node, socket, default: Vector) -> Vector: """Returns the node's socket or named input sockets default value or if linked to, the connecting node's default output value.\n Returns the supplied default value if node/socket is invalid.""" socket = safe_node_input_socket(node, socket) if node and socket: try: if socket.is_linked: connecting_node, connecting_socket = get_node_and_socket_connected_to_input(node, socket) return get_node_output_vector(connecting_node, connecting_socket, default) return extract_socket_vector(socket.default_value, default) except: ... return default def get_node_input_rotation(node : bpy.types.Node, socket, default: Euler) -> Euler: """Returns the node's socket or named input sockets default value or if linked to, the connecting node's default output value.\n Returns the supplied default value if node/socket is invalid.""" socket = safe_node_input_socket(node, socket) if node and socket: try: if socket.is_linked: connecting_node, connecting_socket = get_node_and_socket_connected_to_input(node, socket) return get_node_output_rotation(connecting_node, connecting_socket, default) return extract_socket_rotation(socket.default_value, default) except: ... return default def get_node_input_value(node : bpy.types.Node, socket, default: float) -> float: """Returns the node's socket or named input sockets default value or if linked to, the connecting node's default output value.\n Returns the supplied default value if node/socket is invalid.""" socket = safe_node_input_socket(node, socket) if node and socket: try: if socket.is_linked: connecting_node, connecting_socket = get_node_and_socket_connected_to_input(node, socket) return get_node_output_value(connecting_node, connecting_socket, default) return extract_socket_value(socket.default_value, default) except: ... return default def get_node_output_color(node, socket, default: tuple) -> tuple: """Returns the node's socket or named output sockets default color value.\n Returns the supplied default value if node/socket is invalid.""" socket = safe_node_output_socket(node, socket) if node and socket: try: return extract_socket_color(socket.default_value, default) except: ... return default def get_node_output_vector(node, socket, default: Vector) -> Vector: """Returns the node's socket or named output sockets default value.\n Returns the supplied default value if node/socket is invalid.""" socket = safe_node_output_socket(node, socket) if node and socket: try: return extract_socket_vector(socket.default_value, default) except: ... return default def get_node_output_rotation(node, socket, default: Euler) -> Euler: """Returns the node's socket or named output sockets default value.\n Returns the supplied default value if node/socket is invalid.""" socket = safe_node_output_socket(node, socket) if node and socket: try: return extract_socket_rotation(socket.default_value, default) except: ... return default def get_node_output_value(node, socket, default: float) -> float: """Returns the node's socket or named output sockets default value.\n Returns the supplied default value if node/socket is invalid.""" socket = safe_node_output_socket(node, socket) if node and socket: try: return extract_socket_value(socket.default_value, default) except: ... return default def set_node_input_value(node, socket, value: any): """Sets the node's socket or named input socket's default value.\n If the socket's value is multidimensional the value will be set in each dimension.""" socket = safe_node_input_socket(node, socket) if node and socket: try: socket.default_value = utils.match_dimensions(socket.default_value, value) except: utils.log_detail("Unable to set input: " + node.name + "[" + str(socket) + "]") def set_node_output_value(node, socket, value: any): """Sets the node's socket or named output socket's default value.\n If the socket's value is multidimensional the value will be set in each dimension.""" socket = safe_node_output_socket(node, socket) if node and socket: try: socket.default_value = utils.match_dimensions(socket.default_value, value) except: utils.log_detail("Unable to set output: " + node.name + "[" + str(socket) + "]") BLENDER_4_SOCKET_REDIRECT = { "BSDF_PRINCIPLED": { "Subsurface": "Subsurface Weight", "Specular": "Specular IOR Level", "Sheen": "Sheen Weight", "Emission": "Emission Color", "Transmission": "Transmission Weight", "Clearcoat": "Coat Weight", "Clearcoat Roughness": "Coat Roughness", "Clearcoat IOR": "Coat IOR", "Clearcoat Normal": "Coat Normal", } } def input_socket(node, socket_name: str): try: if utils.B400(): if type(socket_name) is str: if node and node.type in BLENDER_4_SOCKET_REDIRECT: mappings = BLENDER_4_SOCKET_REDIRECT[node.type] socket_name = safe_socket_name(socket_name) if socket_name in mappings: blender_4_socket = mappings[socket_name] if node.inputs and blender_4_socket in node.inputs: return node.inputs[blender_4_socket] if type(socket_name) == str or type(socket_name) == int: return node.inputs[socket_name] else: return socket_name except: return None def output_socket(node, socket_name: str): try: if utils.B400(): if type(socket_name) is str: if node and node.type in BLENDER_4_SOCKET_REDIRECT: mappings = BLENDER_4_SOCKET_REDIRECT[node.type] socket_name = safe_socket_name(socket_name) if socket_name in mappings: blender_4_socket = mappings[socket_name] if node.outputs and blender_4_socket in node.outputs: return node.outputs[blender_4_socket] if type(socket_name) == str or type(socket_name) == int: return node.outputs[socket_name] else: return socket_name except: return None def link_nodes(links, from_node, from_socket, to_node, to_socket): """Create's a link between the supplied from_node and to_node's sockets (or named sockets).""" if from_node and to_node: try: from_socket = safe_node_output_socket(from_node, from_socket) to_socket = safe_node_input_socket(to_node, to_socket) if from_socket and to_socket: links.new(from_socket, to_socket) except: utils.log_detail(f"Unable to link: {from_node.name} [{str(from_socket)}] to {to_node.name} [{str(to_socket)}]") def unlink_node_output(links, node, *sockets): """Removes the link from the socket (or the node's named output socket).""" for socket in sockets: socket = safe_node_output_socket(node, socket) if node and socket: try: for link in socket.links: if link is not None: links.remove(link) except: utils.log_info("Unable to remove links from: " + node.name + "[" + str(socket) + "]") def unlink_node_input(links, node, *sockets): """Removes the link from the socket (or the node's named output socket).""" for socket in sockets: socket = safe_node_input_socket(node, socket) if node and socket: try: for link in socket.links: if link is not None: links.remove(link) except: utils.log_info("Unable to remove links from: " + node.name + "[" + str(socket) + "]") def get_socket_connected_to_output(node, *sockets): """Returns the *first* linked socket connected from the supplied node's output socket (or named output socket).""" for socket in sockets: try: socket = safe_node_output_socket(node, socket) if socket: return socket.links[0].to_socket except: pass return None def get_socket_connected_to_input(node, *sockets): """Returns the linked socket connected to the supplied node's input socket (or named input socket).""" for socket in sockets: try: socket = safe_node_input_socket(node, socket) if socket: return socket.links[0].from_socket except: pass return None def get_node_connected_to_output(node, *sockets): """Returns the *first* linked node connected from the supplied node's input socket (or named input socket).""" for socket in sockets: try: socket = safe_node_output_socket(node, socket) if socket: return socket.links[0].to_node except: pass return None def get_node_connected_to_input(node, *sockets): """Returns the linked node connected to the supplied node's input socket (or named input socket).""" for socket in sockets: try: socket = safe_node_input_socket(node, socket) if socket: return socket.links[0].from_node except: pass return None def get_node_and_socket_connected_to_output(node, *sockets): """Returns the *first* linked node and socket connected from the supplied node's output socket (or named output socket).""" for socket in sockets: try: socket = safe_node_output_socket(node, socket) if socket: return socket.links[0].to_node, socket.links[0].to_socket except: pass return None, None def get_node_and_socket_connected_to_input(node, *sockets): """Returns the linked node and socket connected to the the supplied node's input socket (or named input socket).""" for socket in sockets: try: socket = safe_node_input_socket(node, socket) if socket: return socket.links[0].from_node, socket.links[0].from_socket except: pass return None, None def has_input(node, *sockets): for socket in sockets: try: socket = safe_node_input_socket(node, socket) if socket: return True except: pass return False def has_connected_input(node, *sockets): """Returns True if the node's input socket (or named input socket) is linked to from another node.""" for socket in sockets: try: socket = safe_node_input_socket(node, socket) if socket.is_linked: return True except: pass return False def has_connected_output(node, *sockets): """Returns True if the node's input socket (or named input socket) is linked to from another node.""" for socket in sockets: try: socket = safe_node_output_socket(node, socket) if socket.is_linked: return True except: pass return False def is_mixer_connected(node : bpy.types.Node, socket): socket = safe_node_input_socket(node, socket) try: mixer = get_node_connected_to_input(node, socket) if mixer and mixer.type == "GROUP": if vars.NODE_PREFIX in mixer.name and "rl_mixer" in mixer.name: return True except: pass return False def is_image_node_connected_to_node(node, image, done): """Returns True if there is a linked image node with the supplied image connecting the this node.""" for socket in node.inputs: if socket.is_linked: found = is_image_node_connected_to_socket(node, socket, image, done) if found: return True return False def is_image_node_connected_to_socket(node, socket, image, done): """Returns True if there is a linked image node with the supplied image connecting the this node and socket.""" connected_node = get_node_connected_to_input(node, socket) if not connected_node or connected_node in done: return False done.append(connected_node) if connected_node.type == "TEX_IMAGE" and connected_node.image == image: return True else: return is_image_node_connected_to_node(node, image, done) def get_node_by_id(nodes, id_string): """Find a node with a particular id string.""" for node in nodes: if vars.NODE_PREFIX in node.name and id_string in node.name: return node return None def get_node_by_id_and_type(nodes, id, type): """Find a node with a particular id string and node type.""" for node in nodes: if vars.NODE_PREFIX in node.name and id in node.name and node.type == type: return node return None def reset_shader(mat_cache, nodes, links, shader_label, shader_name, shader_group, mix_shader_group, custom_bsdf = None): prefs = vars.prefs() shader_id = "(" + str(shader_name) + ")" bsdf_id = "(" + str(shader_name) + "_BSDF)" mix_id = "(" + str(shader_name) + "_MIX)" wrinkle_id = "(rl_wrinkle_shader)" group_node: bpy.types.Node = None mix_node: bpy.types.Node = None bsdf_node: bpy.types.Node = None output_node: bpy.types.Node = None wrinkle_node: bpy.types.Node = None has_group_node = shader_group is not None and shader_group != "" has_bsdf = has_group_node has_mix_node = mix_shader_group is not None and mix_shader_group != "" links.clear() for n in nodes: if not custom_bsdf and n.type == "BSDF_PRINCIPLED" and has_bsdf and shader_name in n.name: if not bsdf_node: utils.log_info("Keeping old BSDF: " + n.name) bsdf_node = n else: nodes.remove(n) elif custom_bsdf and n.type == "GROUP" and custom_bsdf in n.node_tree.name and has_bsdf and shader_name in n.name: if not bsdf_node: utils.log_info("Keeping old custom BSDF: " + n.name) bsdf_node = n else: nodes.remove(n) elif n.type == "GROUP" and n.node_tree and shader_name in n.name and lib.is_version(n.node_tree): if wrinkle_id in n.node_tree.name: utils.log_info("Keeping old wrinkle shader group: " + n.name) wrinkle_node = n elif has_group_node and shader_group in n.node_tree.name: if not group_node: utils.log_info("Keeping old shader group: " + n.name) group_node = n else: nodes.remove(n) elif has_mix_node and mix_shader_group in n.node_tree.name: if not mix_node: utils.log_info("Keeping old mix shader group: " + n.name) mix_node = n else: nodes.remove(n) else: nodes.remove(n) elif n.type == "OUTPUT_MATERIAL": if output_node: nodes.remove(n) else: output_node = n elif n.type == "TEX_IMAGE": if vars.NODE_PREFIX in n.name: # keep images pass elif not mat_cache.user_added: # keep all images if it is a user added material nodes.remove(n) else: nodes.remove(n) if has_group_node and not group_node: group = lib.get_node_group(shader_group) group_node = nodes.new("ShaderNodeGroup") group_node.node_tree = group group_node.name = utils.unique_name(shader_id) group_node.label = shader_label group_node.width = 240 utils.log_info("Creating new shader group: " + group_node.name) if has_mix_node and not mix_node: group = lib.get_node_group(mix_shader_group) mix_node = nodes.new("ShaderNodeGroup") mix_node.node_tree = group mix_node.name = utils.unique_name(mix_id) mix_node.label = shader_label mix_node.width = 240 utils.log_info("Creating new mix shader group: " + mix_node.name) # if the mix node has no BSDF input, then it doesn't need the Principled BSDF to mix: if has_mix_node and has_bsdf: if "BSDF" not in mix_node.inputs: has_bsdf = False if bsdf_node: nodes.remove(bsdf_node) bsdf_node = None if has_bsdf and not bsdf_node: if custom_bsdf: template_group = lib.get_node_group(custom_bsdf) # single user copy of template group: group = template_group.copy() bsdf_node = nodes.new("ShaderNodeGroup") bsdf_node.node_tree = group bsdf_node.name = utils.unique_name(bsdf_id) bsdf_node.label = shader_label bsdf_node.width = 240 utils.log_info(f"Created new custom BSDF: {bsdf_node.name} ({custom_bsdf})") else: bsdf_node = nodes.new("ShaderNodeBsdfPrincipled") bsdf_node.name = utils.unique_name(bsdf_id) bsdf_node.label = shader_label bsdf_node.width = 240 utils.log_info(f"Created new BSDF: {bsdf_node.name}") if not output_node: output_node = nodes.new("ShaderNodeOutputMaterial") if has_bsdf: if has_group_node: bsdf_node.location = (200, 400) else: bsdf_node.location = (0,0) if has_group_node: group_node.location = (-400, 0) if has_mix_node: mix_node.location = (500, -500) output_node.location = (900, -400) blocked_bsdf_sockets = [] if mat_cache.get_render_target() != "CYCLES": blocked_bsdf_sockets.append("Subsurface Radius") blocked_bsdf_sockets.append("Subsurface Color") # connect all group_node outputs to BSDF inputs: if has_group_node and has_bsdf: for socket in group_node.outputs: if socket.name not in blocked_bsdf_sockets: to_socket = input_socket(bsdf_node, socket.name) link_nodes(links, group_node, socket.name, bsdf_node, to_socket) link_nodes(links, group_node, "Transmission Alpha", bsdf_node, "Alpha") if bsdf_node and has_connected_input(bsdf_node, "Clearcoat"): link_nodes(links, group_node, "Normal", bsdf_node, "Clearcoat Normal") if utils.B400(): set_node_input_value(bsdf_node, "Subsurface Scale", 1.0) set_node_input_value(bsdf_node, "Sheen Roughness", 0.05) if has_connected_input(bsdf_node, "Emission Color"): set_node_input_value(bsdf_node, "Emission Strength", 1.0) if mat_cache.get_render_target() != "CYCLES" and not utils.B400(): link_nodes(links, group_node, "Base Color", bsdf_node, "Subsurface Color") # connect group_node outputs to any mix_node inputs: if has_mix_node and has_group_node: for socket in mix_node.inputs: link_nodes(links, group_node, socket.name, mix_node, socket.name) # connect up the BSDF to the mix_node: if has_mix_node and has_bsdf: link_nodes(links, bsdf_node, "BSDF", mix_node, "BSDF") # connect the shader to the output if has_mix_node: link_nodes(links, mix_node, "BSDF", output_node, "Surface") elif has_bsdf: link_nodes(links, bsdf_node, "BSDF", output_node, "Surface") # connect any displacement and/or thickness to the output if has_group_node: link_nodes(links, group_node, "Displacement", output_node, "Displacement") link_nodes(links, group_node, "Thickness", output_node, "Thickness") # don't do anything with the old wrinkle shader node yet return bsdf_node, group_node def clean_unused_image_nodes(nodes): to_remove = [] for node in nodes: if node.type == "TEX_IMAGE": is_linked = False for output in node.outputs: if output.is_linked: is_linked = True if not is_linked: to_remove.append(node) for node in to_remove: utils.log_info("Removing unused image node: " + node.name) nodes.remove(node) def is_texture_pack_system(node): if (vars.PACK_DIFFUSEROUGHNESS_ID in node.name or vars.PACK_DIFFUSEROUGHNESSBLEND1_ID in node.name or vars.PACK_DIFFUSEROUGHNESSBLEND2_ID in node.name or vars.PACK_DIFFUSEROUGHNESSBLEND3_ID in node.name or vars.PACK_DIFFUSEALPHA_ID in node.name or vars.PACK_MRSO_ID in node.name or vars.PACK_SSTM_ID in node.name or vars.PACK_MSMNAO_ID in node.name or vars.PACK_WRINKLEROUGHNESS_ID in node.name or vars.PACK_ROOTID_ID in node.name or vars.PACK_SSTMMNM_ID in node.name or "PACK_SPLIT" in node.name): return True else: return False def find_node_by_keywords(nodes, *keywords): for node in nodes: match = True for keyword in keywords: if not keyword in node.name: match = False break if match: return node return None def find_node_by_type(nodes, type): for n in nodes: if n.type == type: return n return None def find_node_by_image(nodes, image): for n in nodes: if n.type == "TEX_IMAGE" and n.image == image: return n return None def find_node_by_type_and_keywords(nodes, type, *keywords): for node in nodes: if node.type == type: match = True for keyword in keywords: if not keyword in node.name: match = False break if match: return node return None def find_node_group_by_keywords(nodes, *keywords): for node in nodes: if node.type == "GROUP" and node.node_tree and node.node_tree.nodes: match = True for keyword in keywords: if not keyword in node.node_tree.name: match = False break if match: return node return None def get_image_node_mapping(image_node): """Returns the location offset, rotation and scale vectors of any attached mapping node to the image node.""" location = (0,0,0) rotation = (0,0,0) scale = (1,1,1) if image_node and image_node.type == "TEX_IMAGE": mapping_node = get_node_connected_to_input(image_node, "Vector") if mapping_node: if mapping_node.type == "MAPPING": location = get_node_input_vector(mapping_node, "Location", Vector((0,0,0))) rotation = get_node_input_rotation(mapping_node, "Rotation", Euler((0,0,0))) scale = get_node_input_vector(mapping_node, "Scale", Vector((1,1,1))) elif mapping_node.type == "GROUP": # custom mapping group location = get_node_input_vector(mapping_node, "Offset", Vector((0,0,0))) scale = get_node_input_vector(mapping_node, "Tiling", Vector((1,1,1))) return location, rotation, scale def store_texture_mapping(image_node, mat_cache, texture_type): if image_node and image_node.type == "TEX_IMAGE": location, rotation, scale = get_image_node_mapping(image_node) texture_path = bpy.path.abspath(image_node.image.filepath) embedded = image_node.image.packed_file is not None image = image_node.image mat_cache.set_texture_mapping(texture_type, texture_path, embedded, image, location, rotation, scale) utils.log_info("Storing texture Mapping for: " + mat_cache.material.name + " texture: " + texture_type) image_id = "(" + texture_type + ")" image_node.name = utils.unique_name(image_id) def get_shader_node(nodes): for n in nodes: if n.type == "GROUP" and "(rl_" in n.name and "_shader)" in n.name: name = n.node_tree.name if ((vars.NODE_PREFIX in name or utils.get_prop(n.node_tree, "RL_Node_Group")) and "_rl_" in name and "_shader_" in name): return n return None def get_shader_nodes(mat, shader_name = None): if mat and mat.node_tree: nodes = mat.node_tree.nodes if shader_name: shader_id = "(" + str(shader_name) + ")" bsdf_id = "(" + str(shader_name) + "_BSDF)" mix_id = "(" + str(shader_name) + "_MIX)" else: shader_id = "_shader)" bsdf_id = "_BSDF)" mix_id = "_MIX)" shader_node = bsdf_node = mix_node = None for node in nodes: if vars.NODE_PREFIX in node.name: if shader_id in node.name: shader_node = node elif bsdf_id in node.name: bsdf_node = node elif mix_id in node.name: mix_node = node return bsdf_node, shader_node, mix_node return None, None, None def get_bsdf_node(mat): if mat and mat.node_tree: nodes = mat.node_tree.nodes for node in nodes: if node.type == "BSDF_PRINCIPLED": return node for node in nodes: if node.type == "GROUP" and "_BSDF)" in node.name: return node return None def get_custom_bsdf_nodes(mat_or_node): bsdf_nodes = [] bsdf_node = None if type(mat_or_node) is bpy.types.Material: bsdf_node = get_bsdf_node(mat_or_node) else: bsdf_node = mat_or_node if bsdf_node: if bsdf_node.type == "GROUP": for node in bsdf_node.node_tree.nodes: if node.type == "BSDF_PRINCIPLED": bsdf_nodes.append(node) else: bsdf_nodes.append(bsdf_node) return bsdf_nodes def get_tiling_node(mat, shader_name, texture_type): if mat and mat.node_tree: nodes = mat.node_tree.nodes shader_id = "(tiling_" + shader_name + "_" + texture_type + "_mapping)" return get_node_by_id(nodes, shader_id) return None def get_tiling_node_from_nodes(nodes, shader_name, texture_type): shader_id = "(tiling_" + shader_name + "_" + texture_type + "_mapping)" return get_node_by_id(nodes, shader_id) def create_custom_image_node(nodes, node_name, image, location = (0, 0)): # find or create the bake image node image_node = find_node_by_type_and_keywords(nodes, "TEX_IMAGE", node_name) if not image_node: image_node = make_image_node(nodes, image, node_name) if image_node.image != image: image_node.image = image image_node.location = location return image_node def find_shader_texture(nodes, texture_type): id = "(" + texture_type + ")" for node in nodes: if node.type == "TEX_IMAGE" and vars.NODE_PREFIX in node.name and id in node.name: return node return None def get_tex_image_size(node): if node and node.image: return node.image.size[0], node.image.size[1] return 64, 64 def get_largest_image_size(*nodes): max_size = [0,0] for node in nodes: if node and node.image: size = get_tex_image_size(node) max_size[0] = max(max_size[0], size[0]) max_size[1] = max(max_size[1], size[1]) return max_size[0], max_size[1] # e.g. # Normal:Height # Normal:Color # Normal:Normal:Color def trace_input_sockets(node, socket_trace : str): if node and socket_trace: socket_names = socket_trace.split(":") trace_node = None trace_socket = None try: if socket_names: trace_node : bpy.types.Node = node for socket_name in socket_names: socket = input_socket(trace_node, socket_name) if socket and socket.is_linked: link = socket.links[0] trace_node = link.from_node trace_socket = link.from_socket else: trace_node = None trace_socket = None break except Exception as e: utils.log_error(f"Trace Input Sockets: {node} {socket_trace}", e) trace_node = None trace_socket = None return trace_node, trace_socket def trace_input_value(node: bpy.types.Node, socket_trace: str, default_value: float) -> float: if node and socket_trace: socket_names = socket_trace.split(":") trace_node = None trace_socket = None try: value_socket_name = socket_names[-1] socket_names = socket_names[:-1] trace_node: bpy.types.Node = node if socket_names: for socket_name in socket_names: socket = input_socket(trace_node, socket_name) if socket and socket.is_linked: link = socket.links[0] trace_node = link.from_node trace_socket = link.from_socket else: trace_node = None trace_socket = None break if trace_node: value_socket = input_socket(trace_node, value_socket_name) return get_node_input_value(trace_node, value_socket, default_value) except Exception as e: utils.log_error(f"Trace Input Value: {node} {socket_trace}", e) return default_value def extract_socket_value(value, default_value: float) -> float: """Socket values are float""" try: if value is None: return default_value T = type(value) if T is float or T is int: return value elif (T is bpy.types.bpy_prop_array or T is list or T is tuple): L = len(value) if L == 4: # RGBA color, return alpha multipled average return utils.lum(value) else: # vector, return magnitude return utils.mag(value) except Exception as e: utils.log_error(f"Extract Socket Value: {value}", e) return default_value def extract_socket_vector(value, default_value: Vector) -> Vector: """Socket locations / scale are Vector""" try: if value is None: return default_value T = type(value) if T is Vector: return value elif (T is bpy.types.bpy_prop_array or T is list or T is tuple): xyz = value[:3] if len(xyz) < 3: xyz += [0]*(3 - len(xyz)) return Vector(xyz) if T is float: return Vector((value, value, value)) except Exception as e: utils.log_error(f"Extract Socket Vector: {value}", e) return default_value def extract_socket_rotation(value, default_value: Euler) -> Euler: """Socket rotations are Euler""" try: if value is None: return default_value T = type(value) if T is Euler: return value if T is float or T is int: return Euler((value, value, value)) elif (T is bpy.types.bpy_prop_array or T is list or T is tuple or T is Vector): xyz = value[:3] if len(xyz) < 3: xyz += [0]*(3 - len(xyz)) return Euler(xyz) except Exception as e: utils.log_error(f"Extract Socket Vector: {value}", e) return default_value def extract_socket_color(value, default_value: tuple) -> tuple: """Socket colours are bpy_prop_array[4] - RGBA""" try: if value is None: return default_value T = type(value) if T is float or T is int: return (value, value, value, 1.0) elif (T is bpy.types.bpy_prop_array or T is list or T is tuple or T is Color): rgba = value[:4] if len(rgba) < 4: rgba += [0]*(4 - len(rgba)) rgba[3] = 1.0 return tuple(rgba) except Exception as e: utils.log_error(f"Extract Socket Color: {value}", e) return default_value def set_trace_input_value(node, socket_trace, value): if node and socket_trace: socket_names = socket_trace.split(":") trace_node = None trace_socket = None try: value_socket_name = socket_names[-1] socket_names = socket_names[:-1] trace_node : bpy.types.Node = node if socket_names: for socket_name in socket_names: socket = input_socket(trace_node, socket_name) if socket and socket.is_linked: link = socket.links[0] trace_node = link.from_node trace_socket = link.from_socket else: trace_node = None trace_socket = None break if trace_node: value_socket = input_socket(trace_node, value_socket_name) set_node_input_value(trace_node, value_socket, value) return True except Exception as e: utils.log_error(f"Set Trace Input Value: {node} {socket_trace} {value}", e) pass return False def furthest_from(n0, dir, *nodes): dir.normalize() most = 0 result = n0 for n in nodes: if n and n0: dn = (n.location - n0.location) proj = dir.dot(dn) if proj > most: most = proj result = n return result def closest_to(n0, dir, *nodes): dir.normalize() least = 9999 result = n0 for n in nodes: if n and n0: dn = (n.location - n0.location) proj = dir.dot(dn) if proj < least: least = proj result = n return result