Files
2026-03-17 15:34:28 -06:00

1253 lines
42 KiB
Python

# Copyright (C) 2021 Victor Soupday
# This file is part of CC/iC Blender Tools <https://github.com/soupday/cc_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 <https://www.gnu.org/licenses/>.
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