2696 lines
138 KiB
Python
2696 lines
138 KiB
Python
import mathutils
|
|
import bpy
|
|
import hashlib
|
|
|
|
bl_info = {
|
|
"name": "Material Batch Tools",
|
|
"description": "Batch tools for quickly modifying, copying, and pasting nodes on all materials in selected objects",
|
|
"author": "Theanine3D",
|
|
"version": (2, 2, 1),
|
|
"blender": (3, 0, 0),
|
|
"category": "Material",
|
|
"location": "Properties -> Material Properties",
|
|
"support": "COMMUNITY"
|
|
}
|
|
|
|
|
|
# PROPERTY DEFINITIONS
|
|
bake_node_preset = {
|
|
"image": "",
|
|
"interpolation": "Linear",
|
|
"projection": "FLAT",
|
|
"projection_blend": 0.0,
|
|
"extension": "REPEAT",
|
|
"name": "Bake Target Node"
|
|
}
|
|
|
|
node_unify_settings = {
|
|
"name": "",
|
|
"type": "",
|
|
"material": ""
|
|
}
|
|
|
|
|
|
class MatBatchProperties(bpy.types.PropertyGroup):
|
|
BakeTargetNodeColorEnable: bpy.props.BoolProperty(
|
|
name="Enable", description="Enable or disable the optional color decoration for the Bake Target Node", default=True)
|
|
BakeTargetNodeColor: bpy.props.FloatVectorProperty(
|
|
name="Color", subtype="COLOR", description="Color to use for the Bake Target Node. This is purely cosmetic - it just makes the node easier to find", default=(0.52, 0.145, 0.152), size=3, min=0, max=1)
|
|
UVMapNodeTarget: bpy.props.StringProperty(
|
|
name="UV Map", description="Name of the UV map to set in the UV Map node", default="UVMap", maxlen=64)
|
|
UVMapNodeExtensionFilter: bpy.props.StringProperty(
|
|
name="Filter", description="Only the Image Texture nodes that are set to this file format will be modified", default="PNG", maxlen=20)
|
|
UVSlotIndex: bpy.props.EnumProperty(
|
|
name="UV Slot", description="The UV Slot that will be modified by the buttons below", items=[("1", 'UV Slot 1', 'The first UV slot in the object mesh(es)', 1), ("2", 'UV Slot 2', 'The second UV slot in the object mesh(es)', 2)], default=1)
|
|
VCName: bpy.props.StringProperty(
|
|
name="Name", description="Name to set in Vertex Color slot 1", default="Col", maxlen=64)
|
|
AlphaBlendMode: bpy.props.EnumProperty(
|
|
name="Blend Mode", description="The Blend Mode and Shadow Mode to set in the material(s). If set to Alpha Blend, Shadow Mode will be set to Alpha Clip", items=[("OPAQUE", 'Opaque', 'No transparency', 0), ("CLIP", 'Alpha Clip', 'Pixels will be either 100 percent transparent or 100 percent opaque', 1), ("BLEND", 'Alpha Blend', 'Pixels will be anywhere between 0 to 100 percent transparent', 2)], default=0)
|
|
AlphaBlendFilter: bpy.props.EnumProperty(
|
|
name="Filter", description="Only materials that satisfy this filter will be modified", items=[("NOFILTER", 'None', 'No filter. All materials will be modified', 0), ("PRINCIPLEDNODE", 'Principled BSDF Alpha', 'There must be a Principled node in the material, and its "Alpha" input is either connected to another node or is less than 1.000', 1), ("TRANSPARENTNODE", 'Transparent BSDF', 'There must be at least one Transparent BSDF in the material', 2)], default=0)
|
|
AlphaThreshold: bpy.props.FloatProperty(
|
|
name="Clip Threshold", subtype="FACTOR", description="This setting is used only by Alpha Clip", default=0.5, min=0.0, max=1.0)
|
|
AlphaPrincipledRemove: bpy.props.BoolProperty(
|
|
name="Remove Principled BSDF Alpha", description="If this option is enabled, and the Blend Mode is set to Opaque, the Principled BSDF's 'Alpha' input will be disconnected, and its value will be set to 1.0", default=False)
|
|
SavedNodeName: bpy.props.StringProperty(
|
|
name="Copied Node", description="The name of the node from which settings were copied", default="", maxlen=200)
|
|
SavedNodeType: bpy.props.StringProperty(
|
|
name="Copied Node Type", description="The type of the node from which settings were copied", default="", maxlen=200)
|
|
UnifyFilterLabel: bpy.props.StringProperty(
|
|
name="Label Filter", description="If specified, the Unify button will only affect any nodes that have this custom label. Case sensitive! Leave blank if you want to alter ALL nodes of the same type as the template node", default="", maxlen=100)
|
|
SwitchShaderTarget: bpy.props.EnumProperty(
|
|
name="Shader", description="The shader to switch in all materials in all selected objects to. For example, if you select Principled, any Emission nodes will be switched to Principled", items=[("EMISSION", 'Emission', 'Fullbright / shadeless shader - not affected by scene lighting', 0), ("BSDF_PRINCIPLED", 'Principled BSDF', 'Standard shader in Blender, affected by scene lighting', 1)], default=0)
|
|
CopiedTexture: bpy.props.StringProperty(
|
|
name="Copied Texture Name", description="The name of the active image texture copied from a selected face", default="", maxlen=200)
|
|
Template: bpy.props.EnumProperty(
|
|
name="Template", description="The node graph template to apply to all materials in all selected objects", items=[("ECT", 'Emissive + Color + Texture', 'Blends vertex color onto texture, if one exists', 0),
|
|
("EC", 'Emissive + Color', 'Ignores image textures completely', 1),
|
|
("ACCT", 'Alpha Clip + Color + Texture', 'Transparency via alpha clip, and emission', 2),
|
|
("ACT", 'Additive + Color + Texture', 'Combines transparency and emission for an additive effect', 3),
|
|
("AC", 'Additive + Color', 'Combines transparency and emission for an additive effect', 4),
|
|
("PT", 'Principled + Texture', 'Principled shading, with texture', 5),
|
|
("PC", 'Principled + Color', 'Principled shading, with vertex color', 6),
|
|
("HDRT", 'HDR Lightmap', "Emissive but with an HDR lightmap applied for baked lighting. Your HDR's UV map must be named 'lightmap', and your HDR's filename must contain either 'light_', '.hdr' or '.exr' to be detected automatically", 7),
|
|
("PP", 'Mirror UV', "Mirroring / ping pong effect applied to any UV Maps, on both X and Y axis", 8),
|
|
("NO_PP", 'Unmirror UV', "Removes the mirror / ping pong effect from any UV Maps, on both X and Y axis", 9),
|
|
], default=0)
|
|
SkipTexture: bpy.props.StringProperty(
|
|
name="Skip Texture", description="Any texture containing this string in its filename will NOT be assigned in any image texture when applying a material template (optional - leave blank if unneeded)", default="", maxlen=200)
|
|
BackfaceCamera: bpy.props.BoolProperty(
|
|
name="Camera", description="Affects whether backface culling is enabled for the active camera. In Blender 4.1 or lower, this setting controls the legacy 'Backface Culling' setting", default=True)
|
|
BackfaceShadow: bpy.props.BoolProperty(
|
|
name="Shadow", description="Affects whether backface culling is enabled for shadows. Only supported in Blender 4.2 or higher", default=True)
|
|
BackfaceLightProbe: bpy.props.BoolProperty(
|
|
name="Light Probe Volume", description="Affects whether backface culling is enabled for light probe volume capture. Only supported in Blender 4.2 or higher", default=True)
|
|
IsolateCollection: bpy.props.BoolProperty(
|
|
name="Isolate to Collection", description="If enabled, any isolated meshes are moved to a separate collection for easier finding", default=True)
|
|
IsolateTrait: bpy.props.EnumProperty(
|
|
name="Trait", description="The trait to look for in materials, in order to isolate their assigned faces into a separate object", items=[("transparent", 'Transparent', "Material uses alpha transparency, via Transparent BSDF or Principled BSDF's alpha slot.", 0),
|
|
("emissive", 'Emissive', "Material uses Emission shader or Principled BSDF's Emission slot", 1),
|
|
("animated", 'Animated', "Material uses an Image Sequence node, or contains animation data from keyframes and/or drivers", 2),
|
|
], default=0)
|
|
|
|
|
|
# FUNCTION DEFINITIONS
|
|
|
|
def display_msg_box(message="", title="Info", icon='INFO'):
|
|
''' Open a pop-up message box to notify the user of something '''
|
|
''' Example: '''
|
|
''' display_msg_box("This is a message", "This is a custom title", "ERROR") '''
|
|
|
|
def draw(self, context):
|
|
lines = message.split("\n")
|
|
for line in lines:
|
|
self.layout.label(text=line)
|
|
print(line)
|
|
|
|
bpy.context.window_manager.popup_menu(draw, title=title, icon=icon)
|
|
|
|
def update_alpha_settings(mat, alpha_mode, shadow_mode, alpha_threshold):
|
|
if bpy.app.version >= (4, 2, 0):
|
|
|
|
if alpha_mode != "CLIP":
|
|
# Remove the Math "Greater Than" node if one exists
|
|
for node in bpy.data.materials[mat].node_tree.nodes:
|
|
if node.type == 'MATH' and node.operation == "GREATER_THAN":
|
|
bpy.data.materials[mat].node_tree.nodes.remove(node)
|
|
break
|
|
|
|
if alpha_mode == "BLEND":
|
|
alpha_mode = "BLENDED"
|
|
|
|
elif alpha_mode == "CLIP":
|
|
greaterthan_node = None
|
|
img_tex_node = None
|
|
principled_node = None
|
|
mix_shader_node = None
|
|
|
|
# Search for existing nodes first
|
|
for node in bpy.data.materials[mat].node_tree.nodes:
|
|
if node.type == 'MATH' and node.operation == "GREATER_THAN":
|
|
greaterthan_node = node
|
|
continue
|
|
elif node.type == 'TEX_IMAGE' and node.image:
|
|
img_tex_node = node
|
|
continue
|
|
elif node.type == 'MIX_SHADER':
|
|
mix_shader_node = node
|
|
continue
|
|
elif node.type == "BSDF_PRINCIPLED":
|
|
principled_node = node
|
|
continue
|
|
if img_tex_node is not None and (principled_node is not None or mix_shader_node is not None):
|
|
if greaterthan_node is None:
|
|
greaterthan_node = bpy.data.materials[mat].node_tree.nodes.new(type='ShaderNodeMath')
|
|
greaterthan_node.operation = "GREATER_THAN"
|
|
greaterthan_node.location = (img_tex_node.location.x + 100, img_tex_node.location.y - 286)
|
|
bpy.data.materials[mat].node_tree.links.new(img_tex_node.outputs[1], greaterthan_node.inputs[0])
|
|
if principled_node is not None:
|
|
bpy.data.materials[mat].node_tree.links.new(greaterthan_node.outputs[0], principled_node.inputs[4])
|
|
elif mix_shader_node is not None:
|
|
bpy.data.materials[mat].node_tree.links.new(greaterthan_node.outputs[0], mix_shader_node.inputs[0])
|
|
alpha_mode = "DITHERED"
|
|
else:
|
|
alpha_mode = "DITHERED"
|
|
|
|
bpy.data.materials[mat].surface_render_method = alpha_mode
|
|
else:
|
|
bpy.data.materials[mat].blend_method = alpha_mode
|
|
bpy.data.materials[mat].shadow_method = shadow_mode
|
|
bpy.data.materials[mat].alpha_threshold = alpha_threshold
|
|
|
|
def recursive_node_search(startnode, end_node_type):
|
|
'''Searches into a node's links for a specific node type. Search ends if a node of the specified type is found, or if .'''
|
|
endnode = None
|
|
connecting_nodes = list()
|
|
|
|
for x in range(0, 5, 1):
|
|
for i in startnode.inputs:
|
|
if len(startnode.inputs) >= (x + 1):
|
|
for l in startnode.inputs[x].links:
|
|
connecting_nodes.append(
|
|
startnode.inputs[x].links[x].from_node)
|
|
else:
|
|
break
|
|
|
|
while endnode == None:
|
|
for x in range(0, 5, 1):
|
|
for n in connecting_nodes:
|
|
for i in n.inputs:
|
|
if len(startnode.inputs) >= (x + 1):
|
|
if len(n.inputs[x].links) > 0:
|
|
for l in n.inputs[x].links:
|
|
if n.inputs[x].links[x].from_node.type == end_node_type:
|
|
endnode = n.inputs[x].links[x].from_node
|
|
return endnode
|
|
else:
|
|
connecting_nodes.append(
|
|
n.inputs[x].links[x].from_node)
|
|
else:
|
|
continue
|
|
|
|
connecting_nodes.remove(n)
|
|
|
|
if len(connecting_nodes) < 2:
|
|
if len(connecting_nodes) < 1:
|
|
return None
|
|
elif len(connecting_nodes[0].inputs.links) < 1:
|
|
return None
|
|
else:
|
|
continue
|
|
|
|
def check_for_selected(objectOnly=False):
|
|
list_of_mats = set()
|
|
|
|
# Check if any objects are selected.
|
|
if len(bpy.context.selected_objects) > 0:
|
|
|
|
# Check if we're only checking for selected objects, regardless if the objects have any materials
|
|
if objectOnly == False:
|
|
|
|
# For each selected object
|
|
for obj in bpy.context.selected_objects:
|
|
if obj.type == "MESH":
|
|
|
|
for mat in obj.material_slots.keys():
|
|
list_of_mats.add(mat)
|
|
|
|
if len(list_of_mats) > 0:
|
|
return list_of_mats
|
|
else:
|
|
display_msg_box(
|
|
"There are no valid materials in the selected objects", "Error", "ERROR")
|
|
return False
|
|
else:
|
|
return False
|
|
|
|
def is_node_connected(material, node_to_check):
|
|
''' Checks if a specified node is actually connected (indirectly or directly) to the final Material Output'''
|
|
|
|
output_node = None
|
|
# Find the Material Output node
|
|
for node in material.node_tree.nodes:
|
|
if node.type == 'OUTPUT_MATERIAL':
|
|
output_node = node
|
|
break
|
|
|
|
if not output_node:
|
|
return False
|
|
|
|
# Function to recursively check connections
|
|
def check_connections(current_node):
|
|
if current_node == node_to_check:
|
|
return True
|
|
for input_socket in current_node.inputs:
|
|
for link in input_socket.links:
|
|
source_node = link.from_node
|
|
if check_connections(source_node):
|
|
return True
|
|
return False
|
|
|
|
# Start from the output node and check connections
|
|
return check_connections(output_node)
|
|
|
|
def find_faces_with_material(mesh_obj, material_name):
|
|
if material_name not in mesh_obj.data.materials:
|
|
return []
|
|
|
|
mat_index = mesh_obj.data.materials.find(material_name)
|
|
faces_with_material = [poly.index for poly in mesh_obj.data.polygons if poly.material_index == mat_index]
|
|
return faces_with_material
|
|
|
|
def separate_faces(mesh_obj, face_indices):
|
|
bpy.ops.object.mode_set(mode='EDIT')
|
|
bpy.ops.mesh.reveal()
|
|
bpy.ops.mesh.select_all(action='DESELECT')
|
|
bpy.ops.mesh.select_mode(use_extend=False, use_expand=False, type='FACE')
|
|
bpy.ops.object.mode_set(mode='OBJECT')
|
|
|
|
for face_index in face_indices:
|
|
if face_index < len(mesh_obj.data.polygons):
|
|
mesh_obj.data.polygons[face_index].select = True
|
|
|
|
bpy.ops.object.mode_set(mode='EDIT')
|
|
bpy.ops.mesh.separate(type='SELECTED')
|
|
bpy.ops.object.mode_set(mode='OBJECT')
|
|
|
|
for obj in bpy.context.selected_objects:
|
|
bpy.context.view_layer.objects.active = obj
|
|
bpy.ops.object.material_slot_remove_unused()
|
|
|
|
mesh_obj.select_set(False)
|
|
bpy.context.view_layer.objects.active = bpy.context.selected_objects[0]
|
|
new_obj = bpy.context.active_object
|
|
|
|
return new_obj
|
|
|
|
|
|
# Bake Target copy operator
|
|
|
|
|
|
class CopyBakeTargetNode(bpy.types.Operator):
|
|
"""Copy the currently active Image Texture node and set it as a preset for baking."""
|
|
bl_idname = "material.copy_bake_target"
|
|
bl_label = "Copy"
|
|
bl_options = {'REGISTER'}
|
|
|
|
def execute(self, context):
|
|
|
|
# Check if there's actually an active object and active material
|
|
if bpy.context.active_object != None and bpy.context.active_object.active_material != None:
|
|
|
|
if len(bpy.context.active_object.material_slots) != 0:
|
|
|
|
if len(bpy.context.active_object.active_material.node_tree.nodes) != 0:
|
|
|
|
# Check if there's an active node
|
|
if bpy.context.active_object.active_material.node_tree.nodes.active != None:
|
|
|
|
# Check if the selected, active node is actually an Image Texture.
|
|
if bpy.context.active_object.active_material.node_tree.nodes.active.type == "TEX_IMAGE":
|
|
if bpy.context.active_object.active_material.node_tree.nodes.active.image != None:
|
|
bake_node_preset["image"] = bpy.context.active_object.active_material.node_tree.nodes.active.image.name
|
|
bake_node_preset["interpolation"] = bpy.context.active_object.active_material.node_tree.nodes.active.interpolation
|
|
bake_node_preset["projection"] = bpy.context.active_object.active_material.node_tree.nodes.active.projection
|
|
bake_node_preset["projection_blend"] = bpy.context.active_object.active_material.node_tree.nodes.active.projection_blend
|
|
bake_node_preset["extension"] = bpy.context.active_object.active_material.node_tree.nodes.active.extension
|
|
else:
|
|
display_msg_box(
|
|
"There is no active Image Texture node. Click on an Image Texture node to set it as active first", "Error", "ERROR")
|
|
else:
|
|
display_msg_box(
|
|
"The active material has no nodes. Select a valid material with at least one Image Texture node", "Error", "ERROR")
|
|
else:
|
|
display_msg_box(
|
|
"There are no valid materials in the active mesh object", "Error", "ERROR")
|
|
else:
|
|
display_msg_box(
|
|
"There is no active mesh object. Click on a mesh object to set it as active first", "Error", "ERROR")
|
|
|
|
return {'FINISHED'}
|
|
|
|
|
|
# Bake Target paste operator
|
|
|
|
class PasteBakeTargetNode(bpy.types.Operator):
|
|
"""Paste the Bake Target Node in all materials in selected objects, using the node settings previously copied with Copy button"""
|
|
bl_idname = "material.paste_bake_target"
|
|
bl_label = "Paste"
|
|
bl_options = {'REGISTER'}
|
|
|
|
def execute(self, context):
|
|
|
|
num_processed = 0
|
|
|
|
list_of_mats = check_for_selected()
|
|
|
|
# Check if any objects are selected.
|
|
if list_of_mats != False:
|
|
|
|
# Check if an image texture has actually been copied yet
|
|
if bake_node_preset["image"] != "":
|
|
|
|
# For each material in selected object
|
|
for mat in list_of_mats:
|
|
|
|
# Find Material Output node
|
|
reference_node = None
|
|
for node in bpy.data.materials[mat].node_tree.nodes:
|
|
if node.type == "OUTPUT_MATERIAL":
|
|
reference_node = node
|
|
break
|
|
|
|
# If no Material Output node exists, look for an alternative reference node instead
|
|
if reference_node is None:
|
|
for node in bpy.data.materials[mat].node_tree.nodes:
|
|
if node.type == "BSDF_PRINCIPLED" or node.type == "EMISSION":
|
|
reference_node = node
|
|
break
|
|
|
|
# If reference node was found:
|
|
if reference_node != None:
|
|
|
|
new_image_node = None
|
|
bake_target_exists = False
|
|
|
|
# Check if Bake Target Node already exists. If so, reset it.
|
|
for node in bpy.data.materials[mat].node_tree.nodes:
|
|
node.select = False
|
|
if "Bake Target Node" in node.name:
|
|
new_image_node = node
|
|
bake_target_exists = True
|
|
break
|
|
|
|
if not bake_target_exists:
|
|
# Create Image Texture node if Bake Target Node doesn't already exist
|
|
new_image_node = bpy.data.materials[mat].node_tree.nodes.new(
|
|
'ShaderNodeTexImage')
|
|
|
|
new_image_node.image = bpy.data.images[bake_node_preset["image"]]
|
|
new_image_node.location = mathutils.Vector(
|
|
((reference_node.location[0] + 180), (reference_node.location[1])))
|
|
new_image_node.interpolation = bake_node_preset["interpolation"]
|
|
new_image_node.projection = bake_node_preset["projection"]
|
|
new_image_node.projection_blend = bake_node_preset["projection_blend"]
|
|
new_image_node.extension = bake_node_preset["extension"]
|
|
new_image_node.color = bpy.context.scene.MatBatchProperties.BakeTargetNodeColor
|
|
new_image_node.use_custom_color = bpy.context.scene.MatBatchProperties.BakeTargetNodeColorEnable
|
|
|
|
new_image_node.name = "Bake Target Node"
|
|
new_image_node.label = "Bake Target"
|
|
new_image_node.select = True
|
|
bpy.data.materials[mat].node_tree.nodes.active = new_image_node
|
|
num_processed += 1
|
|
|
|
display_msg_box(
|
|
f'Created bake target in {num_processed} material(s).', 'Info', 'INFO')
|
|
|
|
else:
|
|
display_msg_box(
|
|
"There is no currently set Bake Target. Use the Copy button first to set one", "Error", "ERROR")
|
|
|
|
return {'FINISHED'}
|
|
|
|
|
|
# Bake Target delete operator
|
|
|
|
class DeleteBakeTargetNode(bpy.types.Operator):
|
|
"""Delete the Bake Target Node, if present, in all materials in selected objects"""
|
|
bl_idname = "material.delete_bake_target"
|
|
bl_label = "Delete"
|
|
bl_options = {'REGISTER'}
|
|
|
|
def execute(self, context):
|
|
num_processed = 0
|
|
list_of_mats = check_for_selected()
|
|
|
|
# Check if any objects are selected.
|
|
if list_of_mats != False:
|
|
|
|
if bake_node_preset["image"] != "":
|
|
|
|
# For each material in selected object
|
|
for mat in list_of_mats:
|
|
|
|
# Check if Bake Target Node already exists. If so, delete it.
|
|
for node in bpy.data.materials[mat].node_tree.nodes:
|
|
if "Bake Target Node" in node.name:
|
|
bpy.data.materials[mat].node_tree.nodes.remove(
|
|
node)
|
|
num_processed += 1
|
|
break
|
|
|
|
display_msg_box(
|
|
f'Deleted {num_processed} bake target node(s).', 'Info', 'INFO')
|
|
|
|
else:
|
|
display_msg_box(
|
|
"There is no currently set Bake Target. Use the Copy button first to set one", "Error", "ERROR")
|
|
|
|
return {'FINISHED'}
|
|
|
|
# Assign UV Map Node operator
|
|
|
|
|
|
class AssignUVMapNode(bpy.types.Operator):
|
|
"""Assign a UV Map node to any Image Texture that satisfies the entered Filter, in all materials in selected objects"""
|
|
bl_idname = "material.assign_uv_map_node"
|
|
bl_label = "Assign UV Map Node"
|
|
bl_options = {'REGISTER'}
|
|
|
|
def execute(self, context):
|
|
num_processed = 0
|
|
list_of_mats = check_for_selected()
|
|
|
|
# Check if any objects are selected.
|
|
if list_of_mats != False:
|
|
|
|
# For each material in selected object
|
|
for mat in list_of_mats:
|
|
|
|
nodetree = bpy.data.materials[mat].node_tree
|
|
links = bpy.data.materials[mat].node_tree.links
|
|
|
|
# Look for Image Texture nodes
|
|
for node in nodetree.nodes:
|
|
new_UV_node = None
|
|
reference_node = None
|
|
|
|
if node.type == "TEX_IMAGE" and node.image:
|
|
|
|
# Skip if it's a Bake Target node
|
|
if node.label == "Bake Target":
|
|
continue
|
|
|
|
# Check if Image Texture is in the user's entered format
|
|
if node.image.file_format == bpy.context.scene.MatBatchProperties.UVMapNodeExtensionFilter:
|
|
|
|
# Check if Image Texture already has a node connected to it
|
|
if node.inputs[0].links:
|
|
|
|
# If node connected to it is a UV Map node...
|
|
if node.inputs[0].links[0].from_node.type == "UVMAP":
|
|
|
|
# Delete the old UV Map node
|
|
nodetree.nodes.remove(
|
|
node.inputs[0].links[0].from_node)
|
|
reference_node = node
|
|
|
|
# If the Image Texture has some other kind of node connected... recursively search to find the closest UV Map node
|
|
else:
|
|
foundnode = recursive_node_search(
|
|
node, "UVMAP")
|
|
if foundnode:
|
|
reference_node = foundnode.outputs[0].links[0].to_node
|
|
nodetree.nodes.remove(
|
|
foundnode)
|
|
else:
|
|
reference_node = node
|
|
else:
|
|
reference_node = node
|
|
|
|
# Create new UV Map node
|
|
new_UV_node = nodetree.nodes.new(
|
|
"ShaderNodeUVMap")
|
|
new_UV_node.name = "Batch UV Map"
|
|
new_UV_node.uv_map = bpy.context.scene.MatBatchProperties.UVMapNodeTarget
|
|
new_UV_node.location = mathutils.Vector(
|
|
((reference_node.location[0] - 200), (reference_node.location[1] - 150)))
|
|
nodetree.links.new(
|
|
new_UV_node.outputs[0], reference_node.inputs[0])
|
|
num_processed += 1
|
|
continue
|
|
display_msg_box(
|
|
f'Created and assigned {num_processed} UV Map node(s).', 'Info', 'INFO')
|
|
|
|
return {'FINISHED'}
|
|
|
|
|
|
# Overwrite UV Slot Name operator
|
|
|
|
class OverwriteUVSlotName(bpy.types.Operator):
|
|
"""Using the specified UV Map name above, this button will overwrite the name of the UV Map in the specified UV slot, in all selected objects. If UV Map slot doesn't exist, a new UV Map will be created with that name"""
|
|
bl_idname = "object.overwrite_uv_slot_name"
|
|
bl_label = "Overwrite UV Slot Name"
|
|
bl_options = {'REGISTER'}
|
|
|
|
def execute(self, context):
|
|
num_processed = 0
|
|
# Check if any objects are selected
|
|
if check_for_selected(True) != False:
|
|
|
|
# For each selected object
|
|
for obj in bpy.context.selected_objects:
|
|
if obj.type == "MESH":
|
|
mesh = obj.data
|
|
uvslots = mesh.uv_layers
|
|
uvslot_index = int(
|
|
bpy.context.scene.MatBatchProperties.UVSlotIndex)
|
|
uvname = bpy.context.scene.MatBatchProperties.UVMapNodeTarget
|
|
|
|
counter = 0
|
|
for slot in uvslots:
|
|
if slot.name == uvname:
|
|
if counter != uvslot_index - 1:
|
|
slot.name = slot.name + ".001"
|
|
else:
|
|
counter += 1
|
|
|
|
if len(uvslots) == 0 and uvslot_index == 1:
|
|
uvslots.new(
|
|
name=uvname)
|
|
elif len(uvslots) == 1 and uvslot_index == 2:
|
|
uvslots.new(
|
|
name=uvname)
|
|
elif len(uvslots) == 0 and uvslot_index == 2:
|
|
uvslots.new(
|
|
name=uvname + ".001")
|
|
uvslots.new(
|
|
name=uvname)
|
|
elif uvslots[uvslot_index-1] != None:
|
|
uvslots[uvslot_index -
|
|
1].name = uvname
|
|
num_processed += 1
|
|
obj.data.update()
|
|
|
|
|
|
display_msg_box(
|
|
f'Renamed the UV map layer for {num_processed} object(s).', 'Info', 'INFO')
|
|
|
|
return {'FINISHED'}
|
|
|
|
|
|
# Set UV Slot as Active opterator
|
|
|
|
class SetUVSlotAsActive(bpy.types.Operator):
|
|
"""Sets the currently selected UV Slot above as the 'active' slot in all selected objects. Does not modify the UV map name"""
|
|
bl_idname = "object.set_uv_slot_as_active"
|
|
bl_label = "Set UV Slot as Active"
|
|
bl_options = {'REGISTER'}
|
|
|
|
def execute(self, context):
|
|
num_processed = 0
|
|
# Check if any objects are selected
|
|
if check_for_selected(True) != False:
|
|
|
|
# For each selected object
|
|
for obj in bpy.context.selected_objects:
|
|
if obj.type == "MESH":
|
|
mesh = obj.data
|
|
uvslots = mesh.uv_layers
|
|
uvslot_index = int(
|
|
bpy.context.scene.MatBatchProperties.UVSlotIndex)
|
|
if len(uvslots) > 0:
|
|
uvslots.active = uvslots[uvslot_index - 1]
|
|
num_processed += 1
|
|
obj.data.update()
|
|
|
|
|
|
display_msg_box(
|
|
f'Set the active UV slot for {num_processed} object(s).', 'Info', 'INFO')
|
|
|
|
return {'FINISHED'}
|
|
|
|
# Assign Vertex Color to Nodes operator
|
|
|
|
|
|
class AssignVCToNodes(bpy.types.Operator):
|
|
"""Assign the Vertex Color name above to all Color Attribute nodes, in all materials in selected objects"""
|
|
bl_idname = "material.assign_vc_to_nodes"
|
|
bl_label = "Assign Name to Color Nodes"
|
|
bl_options = {'REGISTER'}
|
|
|
|
def execute(self, context):
|
|
num_processed = 0
|
|
|
|
list_of_mats = check_for_selected()
|
|
|
|
# Check if any objects are selected.
|
|
if list_of_mats != False:
|
|
|
|
# For each selected object
|
|
for obj in bpy.context.selected_objects:
|
|
|
|
if obj.type == "MESH":
|
|
num_processed += 1
|
|
|
|
# For each material in selected object
|
|
for mat in list_of_mats:
|
|
|
|
for node in bpy.data.materials[mat].node_tree.nodes:
|
|
|
|
if node.type == "VERTEX_COLOR":
|
|
node.layer_name = bpy.context.scene.MatBatchProperties.VCName
|
|
elif node.type == "ATTRIBUTE":
|
|
node.attribute_name = bpy.context.scene.MatBatchProperties.VCName
|
|
obj.data.update()
|
|
|
|
display_msg_box(
|
|
f'Assigned vertex color layer in {num_processed} object(s).', 'Info', 'INFO')
|
|
|
|
return {'FINISHED'}
|
|
|
|
# Rename Vertex Color Slot operator
|
|
|
|
|
|
class RenameVertexColorSlot(bpy.types.Operator):
|
|
"""Rename the first Vertex Color slot in all selected objects, using the name specified above"""
|
|
bl_idname = "object.rename_vertex_color"
|
|
bl_label = "Rename Vertex Color Slot 1"
|
|
bl_options = {'REGISTER'}
|
|
|
|
def execute(self, context):
|
|
num_processed = 0
|
|
|
|
# Blender 3.2 renamed "vertex colors" to "color attributes," so let's check the version beforehand
|
|
useColorAttributes = (bpy.app.version >= (3, 2, 0))
|
|
|
|
# Check if any objects are selected
|
|
if check_for_selected(True) != False:
|
|
|
|
# For each selected object
|
|
for obj in bpy.context.selected_objects:
|
|
if obj.type == "MESH":
|
|
|
|
mesh = obj.data
|
|
if useColorAttributes:
|
|
vcslots = mesh.color_attributes
|
|
else:
|
|
vcslots = mesh.vertex_colors
|
|
vcname = bpy.context.scene.MatBatchProperties.VCName
|
|
|
|
if len(vcslots) > 0:
|
|
vcslots[0].name = vcname
|
|
else:
|
|
if useColorAttributes:
|
|
vcslots.new(name=vcname, type="FLOAT_COLOR",
|
|
domain="POINT")
|
|
# vcslots.new(name=vcname, type="BYTE_COLOR",
|
|
# domain="CORNER")
|
|
else:
|
|
vcslots.new(name=vcname)
|
|
|
|
num_processed += 1
|
|
obj.data.update()
|
|
|
|
display_msg_box(
|
|
f'Renamed {num_processed} vertex color slot(s).', 'Info', 'INFO')
|
|
return {'FINISHED'}
|
|
|
|
# Convert Vertex Color operator
|
|
|
|
class ConvertVertexColor(bpy.types.Operator):
|
|
"""Converts the data type of the first Color Attribute slot in all selected objects, between 'Face Corner Byte Color' and 'Vertex Color'"""
|
|
bl_idname = "object.convert_vertex_color"
|
|
bl_label = "Convert Vertex Color Slot 1"
|
|
bl_options = {'REGISTER'}
|
|
|
|
def execute(self, context):
|
|
|
|
# Blender 3.2 renamed "vertex colors" to "color attributes," so let's check the version beforehand
|
|
useColorAttributes = (bpy.app.version >= (3, 2, 0))
|
|
|
|
if not useColorAttributes:
|
|
display_msg_box(
|
|
f'This feature is only available in Blender 3.2 or higher.', 'Error', 'ERROR')
|
|
return {'FINISHED'}
|
|
|
|
num_processed = 0
|
|
|
|
# Check if any objects are selected
|
|
if check_for_selected(True) != False:
|
|
|
|
# For each selected object
|
|
for obj in bpy.context.selected_objects:
|
|
if obj.type == "MESH":
|
|
|
|
mesh = obj.data
|
|
vcslots = mesh.color_attributes
|
|
vcname = bpy.context.scene.MatBatchProperties.VCName
|
|
|
|
if len(vcslots) == 1:
|
|
if vcslots[0].data_type == "FLOAT_COLOR":
|
|
vcslots.remove(vcslots[0])
|
|
vcslots.new(name=vcname, type="BYTE_COLOR",
|
|
domain="CORNER")
|
|
elif vcslots[0].data_type == "BYTE_COLOR":
|
|
vcslots.remove(vcslots[0])
|
|
vcslots.new(name=vcname, type="FLOAT_COLOR",
|
|
domain="POINT")
|
|
num_processed += 1
|
|
obj.data.update()
|
|
|
|
display_msg_box(
|
|
f'Converted {num_processed} color attribute slot(s).', 'Info', 'INFO')
|
|
return {'FINISHED'}
|
|
|
|
|
|
# Set Blend Mode operator
|
|
|
|
class SetBlendMode(bpy.types.Operator):
|
|
"""Sets the currently selected Blend Mode above as the Blend & Shadow Mode in all materials, in all selected objects"""
|
|
bl_idname = "material.set_blend_mode"
|
|
bl_label = "Set Blend Mode"
|
|
bl_options = {'REGISTER'}
|
|
|
|
def execute(self, context):
|
|
alpha_mode = bpy.context.scene.MatBatchProperties.AlphaBlendMode
|
|
shadow_mode = bpy.context.scene.MatBatchProperties.AlphaBlendMode
|
|
filter_mode = bpy.context.scene.MatBatchProperties.AlphaBlendFilter
|
|
alpha_threshold = bpy.context.scene.MatBatchProperties.AlphaThreshold
|
|
principled_alpha_slot = 21 if bpy.app.version < (4, 0, 0) else 4
|
|
num_processed = 0
|
|
|
|
if alpha_mode == "BLEND":
|
|
shadow_mode = "CLIP"
|
|
|
|
list_of_mats = check_for_selected()
|
|
|
|
# Check if any objects are selected.
|
|
if list_of_mats != False:
|
|
|
|
# For each selected object
|
|
for obj in bpy.context.selected_objects:
|
|
if obj.type == "MESH":
|
|
|
|
# For each material in selected object
|
|
for mat in list_of_mats:
|
|
|
|
principled_nodes = []
|
|
for node in bpy.data.materials[mat].node_tree.nodes:
|
|
if node.type == "BSDF_PRINCIPLED":
|
|
principled_nodes.append(node)
|
|
|
|
# If user also wants to remove any alpha from the Principled node itself too
|
|
if bpy.context.scene.MatBatchProperties.AlphaPrincipledRemove == True and alpha_mode == "OPAQUE":
|
|
for node in principled_nodes:
|
|
if len(node.inputs[principled_alpha_slot].links) > 0:
|
|
bpy.data.materials[mat].node_tree.links.remove(
|
|
node.inputs[principled_alpha_slot].links[0])
|
|
node.inputs[principled_alpha_slot].default_value = 1.0
|
|
|
|
# Filter 1 - Principled BSDF with Alpha
|
|
if filter_mode == "PRINCIPLEDNODE":
|
|
for node in bpy.data.materials[mat].node_tree.nodes:
|
|
if node.type == "BSDF_PRINCIPLED":
|
|
if len(node.inputs[principled_alpha_slot].links) > 0:
|
|
update_alpha_settings(mat, alpha_mode, shadow_mode, alpha_threshold)
|
|
num_processed += 1
|
|
break
|
|
else:
|
|
if node.inputs[principled_alpha_slot].default_value < 1.0:
|
|
update_alpha_settings(mat, alpha_mode, shadow_mode, alpha_threshold)
|
|
num_processed += 1
|
|
break
|
|
|
|
# Filter 2 - Transparent BSDF
|
|
elif filter_mode == "TRANSPARENTNODE":
|
|
for node in bpy.data.materials[mat].node_tree.nodes:
|
|
update_alpha_settings(mat, alpha_mode, shadow_mode, alpha_threshold)
|
|
num_processed += 1
|
|
break
|
|
|
|
else:
|
|
update_alpha_settings(mat, alpha_mode, shadow_mode, alpha_threshold)
|
|
num_processed += 1
|
|
continue
|
|
obj.data.update()
|
|
|
|
display_msg_box(
|
|
f'Updated alpha settings for {str(num_processed)} material(s).', 'Info', 'INFO')
|
|
return {'FINISHED'}
|
|
|
|
|
|
# Copy Node Settings operator
|
|
|
|
class SetAsTemplateNode(bpy.types.Operator):
|
|
"""Stores the currently active, selected node as a template"""
|
|
bl_idname = "material.set_as_template_node"
|
|
bl_label = "Set as Template Node"
|
|
bl_options = {'REGISTER'}
|
|
|
|
def execute(self, context):
|
|
|
|
global node_unify_settings
|
|
|
|
node_unify_settings = {
|
|
"name": "",
|
|
"type": "",
|
|
"material": ""
|
|
}
|
|
|
|
# Check if there's an active object
|
|
if bpy.context.active_object != None:
|
|
|
|
# Check if there's an active node
|
|
if bpy.context.active_object.active_material.node_tree.nodes.active != None:
|
|
|
|
active_node = bpy.context.active_object.active_material.node_tree.nodes.active
|
|
|
|
node_unify_settings["name"] = active_node.name
|
|
node_unify_settings["type"] = active_node.type
|
|
node_unify_settings["material"] = bpy.context.active_object.active_material.name
|
|
bpy.context.scene.MatBatchProperties.SavedNodeName = active_node.name
|
|
bpy.context.scene.MatBatchProperties.SavedNodeType = active_node.bl_label
|
|
else:
|
|
display_msg_box(
|
|
"There is no active node. Click on a node to set one as active", "Error", "ERROR")
|
|
else:
|
|
display_msg_box(
|
|
"There is no active mesh object. Click on a mesh object to set one as active", "Error", "ERROR")
|
|
|
|
return {'FINISHED'}
|
|
|
|
|
|
# Unify Node Settings operator
|
|
|
|
class UnifyNodeSettings(bpy.types.Operator):
|
|
"""Searches for all nodes of the same type, in all materials on selected objects, and copies the template node's settings into those other nodes' settings. The original template node is not modified and must still exist"""
|
|
bl_idname = "material.unify_node_settings"
|
|
bl_label = "Unify Node Settings"
|
|
bl_options = {'REGISTER'}
|
|
|
|
def execute(self, context):
|
|
num_processed = 0
|
|
node_type = node_unify_settings["type"]
|
|
|
|
# Check if template node's material still exists:
|
|
if bpy.data.materials.get(node_unify_settings["material"]) != None:
|
|
|
|
# Check if template node still exists:
|
|
if bpy.data.materials[node_unify_settings["material"]].node_tree.nodes.get(node_unify_settings["name"]) != None:
|
|
|
|
template_node = bpy.data.materials[node_unify_settings["material"]
|
|
].node_tree.nodes[node_unify_settings["name"]]
|
|
|
|
list_of_mats = check_for_selected()
|
|
|
|
# Check if any objects are selected.
|
|
if list_of_mats != False:
|
|
|
|
# Check if there are any previously copied node settings
|
|
if node_unify_settings["name"] != "":
|
|
|
|
# For each selected object
|
|
for obj in bpy.context.selected_objects:
|
|
if obj.type == "MESH":
|
|
num_processed += 1
|
|
|
|
# For each material in selected object
|
|
for mat in list_of_mats:
|
|
|
|
valid_nodes = []
|
|
|
|
for node in bpy.data.materials[mat].node_tree.nodes:
|
|
|
|
# Check if node is of the saved type
|
|
if node.type == node_type:
|
|
|
|
# Check if a Label Filter was specified
|
|
if bpy.context.scene.MatBatchProperties.UnifyFilterLabel != "":
|
|
if node.label == bpy.context.scene.MatBatchProperties.UnifyFilterLabel:
|
|
valid_nodes.append(node)
|
|
else:
|
|
continue
|
|
|
|
else:
|
|
valid_nodes.append(node)
|
|
|
|
# Special operations for curve nodes - storing template curve data for later
|
|
template_curve_data = [[],[],[],[]]
|
|
template_curve_handle_types = [[],[],[],[]]
|
|
if 'CURVE' in node.type:
|
|
curve_index = 0
|
|
for curve in template_node.mapping.curves:
|
|
for point in curve.points:
|
|
template_curve_data[curve_index].append(tuple(point.location))
|
|
template_curve_handle_types[curve_index].append(point.handle_type)
|
|
curve_index += 1
|
|
else:
|
|
del template_curve_data
|
|
del template_curve_handle_types
|
|
|
|
# Special operations for color ramp nodes - storing template gradient data for later
|
|
template_ramp_data = []
|
|
if 'VALTORGB' in node.type:
|
|
for stop in template_node.color_ramp.elements:
|
|
template_ramp_data.append((stop.position, tuple(stop.color)))
|
|
else:
|
|
del template_ramp_data
|
|
|
|
for node in valid_nodes:
|
|
|
|
# Copy and paste inputs from template node
|
|
input_counter = 0
|
|
|
|
for i in node.inputs:
|
|
|
|
if hasattr(i, "default_value") and hasattr(template_node.inputs[input_counter], "default_value"):
|
|
|
|
i.default_value = template_node.inputs[
|
|
input_counter].default_value
|
|
input_counter += 1
|
|
|
|
# Copy and paste properties from template node, but exclude the properties contained in a "do not use" list
|
|
property_list = list(
|
|
template_node.bl_rna.properties.keys())
|
|
new_property_list = list()
|
|
|
|
do_not_use = ['rna_type', 'type', 'location', 'location_absolute', 'width', 'width_hidden', 'height', 'dimensions', 'name', 'label', 'inputs', 'outputs', 'internal_links', 'parent', 'use_custom_color', 'color', 'select', 'show_options',
|
|
'show_preview', 'hide', 'mute', 'show_texture', 'bl_idname', 'bl_label', 'bl_description', 'bl_icon', 'bl_static_type', 'bl_width_default', 'bl_width_min', 'bl_width_max', 'bl_height_default', 'bl_height_min', 'bl_height_max']
|
|
|
|
for prop in property_list:
|
|
if prop not in do_not_use:
|
|
if node.is_property_readonly(prop) == False:
|
|
new_property_list.append(
|
|
prop)
|
|
|
|
for prop in new_property_list:
|
|
setattr(
|
|
node, prop, eval(f"template_node.{prop}"))
|
|
|
|
# Special operations for curve nodes - copying template curve data over
|
|
if 'CURVE' in template_node.type and 'CURVE' in node.type:
|
|
curve_index = 0
|
|
for curve in node.mapping.curves:
|
|
|
|
# Clear the existing points first
|
|
point_count = len(curve.points)
|
|
for index in list(range(0,point_count)):
|
|
try:
|
|
curve.points.remove(curve.points[index])
|
|
except:
|
|
continue
|
|
|
|
# Check if the point counts are the same. If not, we need to add new points
|
|
point_count_difference = abs(len(curve.points) - len(template_curve_data[curve_index]))
|
|
if point_count_difference > 0:
|
|
for index in range(0, point_count_difference):
|
|
curve.points.new(1,1)
|
|
|
|
point_index = 0
|
|
for point in curve.points:
|
|
point.location = template_curve_data[curve_index][point_index]
|
|
point.handle_type = template_curve_handle_types[curve_index][point_index]
|
|
point_index += 1
|
|
|
|
curve_index += 1
|
|
node.mapping.use_clip = template_node.mapping.use_clip
|
|
node.mapping.clip_min_x = template_node.mapping.clip_min_x
|
|
node.mapping.clip_min_y = template_node.mapping.clip_min_y
|
|
node.mapping.clip_max_x = template_node.mapping.clip_max_x
|
|
node.mapping.clip_max_y = template_node.mapping.clip_max_y
|
|
node.mapping.update()
|
|
|
|
# Special operations for color ramp nodes - copying template gradient data over
|
|
if 'VALTORGB' in template_node.type and 'VALTORGB' in node.type:
|
|
# Clear the existing points first
|
|
stop_count = len(node.color_ramp.elements)
|
|
for index in list(range(0,stop_count)):
|
|
try:
|
|
node.color_ramp.elements.remove(node.color_ramp.elements[index])
|
|
except:
|
|
continue
|
|
|
|
# Check if the stop counts are the same. If not, we need to add new points.
|
|
stop_count_difference = abs(len(node.color_ramp.elements) - len(template_ramp_data))
|
|
if stop_count_difference > 0:
|
|
for index in range(0,stop_count_difference):
|
|
node.color_ramp.elements.new(1.0)
|
|
|
|
stop_index = 0
|
|
for stop in node.color_ramp.elements:
|
|
stop.position = template_ramp_data[stop_index][0]
|
|
stop.color = template_ramp_data[stop_index][1]
|
|
stop_index += 1
|
|
|
|
node.color_ramp.color_mode = template_node.color_ramp.color_mode
|
|
node.color_ramp.hue_interpolation = template_node.color_ramp.hue_interpolation
|
|
node.color_ramp.elements.update()
|
|
|
|
else:
|
|
display_msg_box(
|
|
"You haven't set a a template yet. Use the Set as Template button to set one.", "Error", "ERROR")
|
|
return {'FINISHED'}
|
|
else:
|
|
display_msg_box(
|
|
"The template node no longer exists. Use the Set as Template button set a new one", "Error", "ERROR")
|
|
return {'FINISHED'}
|
|
else:
|
|
display_msg_box(
|
|
"The template node's parent material no longer exists. Use the Set as Template button set a new one", "Error", "ERROR")
|
|
return {'FINISHED'}
|
|
|
|
display_msg_box(
|
|
f'Applied unified node settings in {num_processed} object(s).', 'Info', 'INFO')
|
|
return {'FINISHED'}
|
|
|
|
# Shader Switch operator
|
|
|
|
|
|
class SwitchShader(bpy.types.Operator):
|
|
"""Finds all Principled BSDF or Emission shader nodes, in all materials in all selected objects, and switches them to the shader selected above"""
|
|
bl_idname = "material.switch_shader"
|
|
bl_label = "Switch Shader"
|
|
bl_options = {'REGISTER'}
|
|
|
|
def execute(self, context):
|
|
|
|
num_processed = 0
|
|
list_of_mats = check_for_selected()
|
|
|
|
# Check if any objects are selected.
|
|
if list_of_mats != False:
|
|
|
|
old_shader_type = None
|
|
target_shader_type = bpy.context.scene.MatBatchProperties.SwitchShaderTarget
|
|
|
|
if target_shader_type == "EMISSION": # If user wants to switch to Emission
|
|
old_shader_type = "BSDF_PRINCIPLED"
|
|
else:
|
|
old_shader_type = "EMISSION"
|
|
|
|
# For each selected object
|
|
for obj in bpy.context.selected_objects:
|
|
if obj.type == "MESH":
|
|
|
|
# For each material in selected object
|
|
for mat in list_of_mats:
|
|
|
|
material = bpy.data.materials[mat]
|
|
|
|
# Find the other shader
|
|
old_shaders = []
|
|
for node in bpy.data.materials[mat].node_tree.nodes:
|
|
if node.type == old_shader_type:
|
|
old_shaders.append(node)
|
|
break
|
|
|
|
# If the old shader wasn't found, skip this material and continue to the next material
|
|
if len(old_shaders) == 0:
|
|
continue
|
|
|
|
# If opposite shader was found:
|
|
else:
|
|
|
|
for old_shader in old_shaders:
|
|
|
|
new_shader = None
|
|
input_node_socket = None
|
|
output_node_socket = None
|
|
|
|
if len(old_shader.inputs[0].links) > 0:
|
|
input_node_socket = old_shader.inputs[0].links[0].from_socket
|
|
if len(old_shader.outputs[0].links) > 0:
|
|
output_node_socket = old_shader.outputs[0].links[0].to_socket
|
|
|
|
# Create new shader
|
|
if target_shader_type == "BSDF_PRINCIPLED":
|
|
new_shader = bpy.data.materials[mat].node_tree.nodes.new(
|
|
"ShaderNodeBsdfPrincipled")
|
|
|
|
if target_shader_type == "EMISSION":
|
|
new_shader = bpy.data.materials[mat].node_tree.nodes.new(
|
|
"ShaderNodeEmission")
|
|
|
|
# Place the new shader in the old shader's location
|
|
new_shader.location = old_shader.location
|
|
if len(old_shader.inputs[0].links) > 0:
|
|
material.node_tree.links.new(
|
|
new_shader.inputs[0], input_node_socket)
|
|
if len(old_shader.outputs[0].links) > 0:
|
|
material.node_tree.links.new(
|
|
output_node_socket, new_shader.outputs[0])
|
|
material.node_tree.nodes.remove(old_shader)
|
|
num_processed += 1
|
|
|
|
display_msg_box(
|
|
f'Switched shader in {num_processed} material(s).', 'Info', 'INFO')
|
|
|
|
return {'FINISHED'}
|
|
|
|
# Apply Material Template operator
|
|
|
|
class ApplyMatTemplate(bpy.types.Operator):
|
|
"""Applies the selected material template, to all materials in all selected objects, while attempting to retain the albedo image texture if one exists"""
|
|
bl_idname = "material.apply_mat_template"
|
|
bl_label = "Apply Material Template"
|
|
bl_options = {'REGISTER'}
|
|
|
|
def execute(self, context):
|
|
|
|
num_processed = 0
|
|
list_of_mats = check_for_selected()
|
|
useColorAttributes = bpy.app.version >= (3, 2, 0)
|
|
mix_node_type = "ShaderNodeMixRGB" if bpy.app.version < (3, 4, 0) else "ShaderNodeMix"
|
|
principled_alpha_slot = 21 if bpy.app.version < (4, 0, 0) else 4
|
|
|
|
# Check if any objects are selected.
|
|
if list_of_mats != False:
|
|
|
|
target_template = bpy.context.scene.MatBatchProperties.Template
|
|
skip_texture = bpy.context.scene.MatBatchProperties.SkipTexture
|
|
|
|
# For each selected object
|
|
for obj in bpy.context.selected_objects:
|
|
if obj.type == "MESH":
|
|
|
|
# For each material in selected object
|
|
for mat in list_of_mats:
|
|
|
|
material = bpy.data.materials[mat]
|
|
material.use_nodes = True
|
|
|
|
if material and material.use_nodes:
|
|
|
|
match target_template:
|
|
|
|
case "ECT":
|
|
|
|
if bpy.app.version >= (4, 2, 0):
|
|
material.surface_render_method = 'DITHERED'
|
|
else:
|
|
material.blend_method = 'OPAQUE'
|
|
|
|
# Store the color of the existing Principled BSDF or Emissive node (if any)
|
|
stored_color = None
|
|
for node in material.node_tree.nodes:
|
|
if node.type == 'BSDF_PRINCIPLED' or node.type == 'EMISSION':
|
|
if is_node_connected(material, node):
|
|
stored_color = tuple(node.inputs[0].default_value)
|
|
break
|
|
|
|
# Store the image of the existing image texture node (if any)
|
|
stored_image = None
|
|
for node in material.node_tree.nodes:
|
|
if node.type == 'TEX_IMAGE' and node.image:
|
|
stored_image = node.image
|
|
break
|
|
|
|
# Clear existing nodes
|
|
material.node_tree.nodes.clear()
|
|
if stored_image != None:
|
|
if skip_texture != "":
|
|
if skip_texture in stored_image.filepath:
|
|
stored_image = None
|
|
|
|
# Create necessary nodes
|
|
uv_map_node = None
|
|
img_tex_node = None
|
|
mix_color_node = None
|
|
if stored_image != None:
|
|
uv_map_node = material.node_tree.nodes.new(type='ShaderNodeUVMap')
|
|
img_texture_node = material.node_tree.nodes.new(type='ShaderNodeTexImage')
|
|
mix_color_node = material.node_tree.nodes.new(type=mix_node_type)
|
|
if "MixRGB" not in mix_node_type:
|
|
mix_color_node.data_type = 'RGBA'
|
|
mix_color_node.blend_type = 'MULTIPLY'
|
|
mix_color_node.inputs[0].default_value = 1.0 # Set the factor to 1.0
|
|
img_texture_node.image = stored_image
|
|
if len(obj.data.uv_layers) > 0:
|
|
uv_map_node.uv_map = obj.data.uv_layers[0].name
|
|
color_attr_node = material.node_tree.nodes.new(type='ShaderNodeVertexColor')
|
|
emission_node = material.node_tree.nodes.new(type='ShaderNodeEmission')
|
|
material_output_node = material.node_tree.nodes.new(type='ShaderNodeOutputMaterial')
|
|
|
|
# Copy the original base color over
|
|
if stored_color != None:
|
|
emission_node.inputs[0].default_value = stored_color
|
|
|
|
# Arrange nodes for clarity
|
|
if stored_image != None:
|
|
img_texture_node.location = (-500, 0)
|
|
mix_color_node.location = (-200, 100)
|
|
uv_map_node.location = (-700, 0)
|
|
color_attr_node.location = (-400, 150)
|
|
emission_node.location = (0, 100)
|
|
material_output_node.location = (200, 100)
|
|
|
|
# Add correct Vertex Color name
|
|
if useColorAttributes:
|
|
if len(obj.data.color_attributes) > 0:
|
|
color_attr_node.layer_name = obj.data.color_attributes[0].name
|
|
else:
|
|
if len(obj.data.vertex_colors) > 0:
|
|
color_attr_node.layer_name = obj.data.vertex_colors[0].name
|
|
|
|
# Link nodes
|
|
links = material.node_tree.links
|
|
if stored_image != None:
|
|
if "MixRGB" not in mix_node_type:
|
|
links.new(img_texture_node.outputs[0], mix_color_node.inputs[7])
|
|
links.new(color_attr_node.outputs[0], mix_color_node.inputs[6])
|
|
links.new(mix_color_node.outputs[2], emission_node.inputs[0])
|
|
else:
|
|
links.new(img_texture_node.outputs[0], mix_color_node.inputs[2])
|
|
links.new(color_attr_node.outputs[0], mix_color_node.inputs[1])
|
|
links.new(mix_color_node.outputs[0], emission_node.inputs[0])
|
|
links.new(emission_node.outputs[0], material_output_node.inputs[0])
|
|
links.new(uv_map_node.outputs[0], img_texture_node.inputs[0])
|
|
else:
|
|
links.new(color_attr_node.outputs[0], emission_node.inputs[0])
|
|
links.new(emission_node.outputs[0], material_output_node.inputs[0])
|
|
|
|
case "EC":
|
|
|
|
if bpy.app.version >= (4, 2, 0):
|
|
material.surface_render_method = 'DITHERED'
|
|
else:
|
|
material.blend_method = 'OPAQUE'
|
|
|
|
# Store the color of the existing Principled BSDF or Emissive node (if any)
|
|
stored_color = None
|
|
for node in material.node_tree.nodes:
|
|
if node.type == 'BSDF_PRINCIPLED' or node.type == 'EMISSION':
|
|
if is_node_connected(material, node):
|
|
stored_color = tuple(node.inputs[0].default_value)
|
|
break
|
|
|
|
# Clear existing nodes
|
|
material.node_tree.nodes.clear()
|
|
|
|
# Create necessary nodes
|
|
color_attr_node = material.node_tree.nodes.new(type='ShaderNodeVertexColor')
|
|
emission_node = material.node_tree.nodes.new(type='ShaderNodeEmission')
|
|
material_output_node = material.node_tree.nodes.new(type='ShaderNodeOutputMaterial')
|
|
|
|
# Copy the original base color over
|
|
if stored_color != None:
|
|
emission_node.inputs[0].default_value = stored_color
|
|
|
|
# Arrange nodes for clarity
|
|
color_attr_node.location = (-200, 100)
|
|
emission_node.location = (0, 100)
|
|
material_output_node.location = (200, 100)
|
|
|
|
# Add correct Vertex Color name
|
|
if useColorAttributes:
|
|
if len(obj.data.color_attributes) > 0:
|
|
color_attr_node.layer_name = obj.data.color_attributes[0].name
|
|
else:
|
|
if len(obj.data.vertex_colors) > 0:
|
|
color_attr_node.layer_name = obj.data.vertex_colors[0].name
|
|
|
|
# Link nodes
|
|
links = material.node_tree.links
|
|
links.new(color_attr_node.outputs[0], emission_node.inputs[0])
|
|
links.new(emission_node.outputs[0], material_output_node.inputs[0])
|
|
|
|
case "ACCT":
|
|
|
|
if bpy.app.version >= (4, 2, 0):
|
|
material.surface_render_method = 'DITHERED'
|
|
else:
|
|
material.blend_method = 'CLIP'
|
|
|
|
# Store the color of the existing Principled BSDF or Emissive node (if any)
|
|
stored_color = None
|
|
for node in material.node_tree.nodes:
|
|
if node.type == 'BSDF_PRINCIPLED' or node.type == 'EMISSION':
|
|
if is_node_connected(material, node):
|
|
stored_color = tuple(node.inputs[0].default_value)
|
|
break
|
|
|
|
# Store the image of the existing image texture node (if any)
|
|
stored_image = None
|
|
for node in material.node_tree.nodes:
|
|
if node.type == 'TEX_IMAGE' and node.image:
|
|
stored_image = node.image
|
|
break
|
|
|
|
# Clear existing nodes
|
|
material.node_tree.nodes.clear()
|
|
if stored_image != None:
|
|
if skip_texture != "":
|
|
if skip_texture in stored_image.filepath:
|
|
stored_image = None
|
|
|
|
# Create necessary nodes
|
|
if stored_image is not None:
|
|
uv_map_node = material.node_tree.nodes.new(type='ShaderNodeUVMap')
|
|
img_texture_node = material.node_tree.nodes.new(type='ShaderNodeTexImage')
|
|
mix_color_node = material.node_tree.nodes.new(type=mix_node_type)
|
|
if "MixRGB" not in mix_node_type:
|
|
mix_color_node.data_type = 'RGBA'
|
|
mix_color_node.blend_type = 'MULTIPLY'
|
|
mix_color_node.inputs[0].default_value = 1.0 # Set the factor to 1.0
|
|
img_texture_node.image = stored_image
|
|
if len(obj.data.uv_layers) > 0:
|
|
uv_map_node.uv_map = obj.data.uv_layers[0].name
|
|
|
|
color_attr_node = material.node_tree.nodes.new(type='ShaderNodeVertexColor')
|
|
emission_node = material.node_tree.nodes.new(type='ShaderNodeEmission')
|
|
material_output_node = material.node_tree.nodes.new(type='ShaderNodeOutputMaterial')
|
|
mix_shader_node = material.node_tree.nodes.new(type='ShaderNodeMixShader')
|
|
transparent_node = material.node_tree.nodes.new(type='ShaderNodeBsdfTransparent')
|
|
|
|
material.alpha_threshold = 0.5
|
|
|
|
# Add correct Vertex Color name
|
|
if useColorAttributes:
|
|
if len(obj.data.color_attributes) > 0:
|
|
color_attr_node.layer_name = obj.data.color_attributes[0].name
|
|
else:
|
|
if len(obj.data.vertex_colors) > 0:
|
|
color_attr_node.layer_name = obj.data.vertex_colors[0].name
|
|
|
|
# Copy the original base color over
|
|
if stored_color != None:
|
|
emission_node.inputs[0].default_value = stored_color
|
|
|
|
# Arrange nodes for clarity
|
|
if stored_image is not None:
|
|
uv_map_node.location = (-700, 0)
|
|
img_texture_node.location = (-500, 0)
|
|
mix_color_node.location = (-200, 100)
|
|
|
|
color_attr_node.location = (-400, 150)
|
|
emission_node.location = (0, 100)
|
|
mix_shader_node.location = (200, 100)
|
|
transparent_node.location = (0, -100)
|
|
material_output_node.location = (400, 100)
|
|
|
|
# Link nodes
|
|
links = material.node_tree.links
|
|
if stored_image is not None:
|
|
links.new(uv_map_node.outputs[0], img_texture_node.inputs[0])
|
|
links.new(img_texture_node.outputs[0], mix_color_node.inputs[2])
|
|
|
|
if "MixRGB" not in mix_node_type:
|
|
links.new(img_texture_node.outputs[0], mix_color_node.inputs[7])
|
|
links.new(color_attr_node.outputs[0], mix_color_node.inputs[6])
|
|
links.new(mix_color_node.outputs[2], emission_node.inputs[0])
|
|
else:
|
|
links.new(img_texture_node.outputs[0], mix_color_node.inputs[2])
|
|
links.new(color_attr_node.outputs[0], mix_color_node.inputs[1])
|
|
links.new(mix_color_node.outputs[0], emission_node.inputs[0])
|
|
else:
|
|
links.new(color_attr_node.outputs[0], emission_node.inputs[0])
|
|
links.new(emission_node.outputs[0], material_output_node.inputs[0])
|
|
|
|
# Mix Shader links
|
|
links.new(emission_node.outputs[0], mix_shader_node.inputs[2])
|
|
links.new(transparent_node.outputs[0], mix_shader_node.inputs[1])
|
|
if stored_image is not None:
|
|
links.new(img_texture_node.outputs[1], mix_shader_node.inputs[0])
|
|
links.new(mix_shader_node.outputs[0], material_output_node.inputs[0])
|
|
|
|
# Blender 4.2 got rid of the alpha clip setting, so we use the Math node instead
|
|
if bpy.app.version >= (4, 2, 0):
|
|
material.surface_render_method = 'DITHERED'
|
|
greaterthan_node = material.node_tree.nodes.new(type='ShaderNodeMath')
|
|
greaterthan_node.operation = 'GREATER_THAN'
|
|
greaterthan_node.location = (-200,-140)
|
|
if stored_image is not None:
|
|
links.new(img_texture_node.outputs[1], greaterthan_node.inputs[0])
|
|
links.new(greaterthan_node.outputs[0], mix_shader_node.inputs[0])
|
|
else:
|
|
material.blend_method = "CLIP"
|
|
|
|
|
|
case "ACT":
|
|
|
|
material.blend_method = "BLEND"
|
|
|
|
# Store the color of the existing Principled BSDF or Emissive node (if any)
|
|
stored_color = None
|
|
for node in material.node_tree.nodes:
|
|
if node.type == 'BSDF_PRINCIPLED' or node.type == 'EMISSION':
|
|
if is_node_connected(material, node):
|
|
stored_color = tuple(node.inputs[0].default_value)
|
|
break
|
|
|
|
# Store the image of the existing image texture node (if any)
|
|
stored_image = None
|
|
for node in material.node_tree.nodes:
|
|
if node.type == 'TEX_IMAGE' and node.image:
|
|
stored_image = node.image
|
|
break
|
|
|
|
# Clear existing nodes
|
|
material.node_tree.nodes.clear()
|
|
if stored_image != None:
|
|
if skip_texture != "":
|
|
if skip_texture in stored_image.filepath:
|
|
stored_image = None
|
|
|
|
# Create necessary nodes
|
|
if stored_image is not None:
|
|
uv_map_node = material.node_tree.nodes.new(type='ShaderNodeUVMap')
|
|
img_texture_node = material.node_tree.nodes.new(type='ShaderNodeTexImage')
|
|
mix_color_node = material.node_tree.nodes.new(type=mix_node_type)
|
|
if "MixRGB" not in mix_node_type:
|
|
mix_color_node.data_type = 'RGBA'
|
|
mix_color_node.blend_type = 'MULTIPLY'
|
|
mix_color_node.inputs[0].default_value = 1.0 # Set the factor to 1.0
|
|
img_texture_node.image = stored_image
|
|
if len(obj.data.uv_layers) > 0:
|
|
uv_map_node.uv_map = obj.data.uv_layers[0].name
|
|
|
|
color_attr_node = material.node_tree.nodes.new(type='ShaderNodeVertexColor')
|
|
emission_node = material.node_tree.nodes.new(type='ShaderNodeEmission')
|
|
material_output_node = material.node_tree.nodes.new(type='ShaderNodeOutputMaterial')
|
|
add_shader_node = material.node_tree.nodes.new(type='ShaderNodeAddShader')
|
|
transparent_node = material.node_tree.nodes.new(type='ShaderNodeBsdfTransparent')
|
|
|
|
# Copy the original base color over
|
|
if stored_color != None:
|
|
emission_node.inputs[0].default_value = stored_color
|
|
|
|
# Add correct Vertex Color name
|
|
if useColorAttributes:
|
|
if len(obj.data.color_attributes) > 0:
|
|
color_attr_node.layer_name = obj.data.color_attributes[0].name
|
|
else:
|
|
if len(obj.data.vertex_colors) > 0:
|
|
color_attr_node.layer_name = obj.data.vertex_colors[0].name
|
|
|
|
# Arrange nodes for clarity
|
|
if stored_image is not None:
|
|
uv_map_node.location = (-700, 0)
|
|
img_texture_node.location = (-500, 0)
|
|
mix_color_node.location = (-200, 100)
|
|
|
|
color_attr_node.location = (-400, 150)
|
|
emission_node.location = (0, 100)
|
|
add_shader_node.location = (200, 100)
|
|
transparent_node.location = (0, -100)
|
|
material_output_node.location = (400, 100)
|
|
|
|
# Link nodes
|
|
links = material.node_tree.links
|
|
if stored_image is not None:
|
|
links.new(uv_map_node.outputs[0], img_texture_node.inputs[0])
|
|
links.new(img_texture_node.outputs[0], mix_color_node.inputs[2])
|
|
|
|
if "MixRGB" not in mix_node_type:
|
|
links.new(img_texture_node.outputs[0], mix_color_node.inputs[7])
|
|
links.new(color_attr_node.outputs[0], mix_color_node.inputs[6])
|
|
links.new(mix_color_node.outputs[2], emission_node.inputs[0])
|
|
else:
|
|
links.new(img_texture_node.outputs[0], mix_color_node.inputs[2])
|
|
links.new(color_attr_node.outputs[0], mix_color_node.inputs[1])
|
|
links.new(mix_color_node.outputs[0], emission_node.inputs[0])
|
|
else:
|
|
links.new(color_attr_node.outputs[0], emission_node.inputs[0])
|
|
links.new(emission_node.outputs[0], material_output_node.inputs[0])
|
|
|
|
# Additive links
|
|
links.new(emission_node.outputs[0], add_shader_node.inputs[0])
|
|
links.new(transparent_node.outputs[0], add_shader_node.inputs[1])
|
|
links.new(add_shader_node.outputs[0], material_output_node.inputs[0])
|
|
|
|
case "AC":
|
|
|
|
material.blend_method = "BLEND"
|
|
|
|
# Store the color of the existing Principled BSDF or Emissive node (if any)
|
|
stored_color = None
|
|
for node in material.node_tree.nodes:
|
|
if node.type == 'BSDF_PRINCIPLED' or node.type == 'EMISSION':
|
|
if is_node_connected(material, node):
|
|
stored_color = tuple(node.inputs[0].default_value)
|
|
break
|
|
|
|
# Clear existing nodes
|
|
material.node_tree.nodes.clear()
|
|
|
|
# Create necessary nodes
|
|
color_attr_node = material.node_tree.nodes.new(type='ShaderNodeVertexColor')
|
|
emission_node = material.node_tree.nodes.new(type='ShaderNodeEmission')
|
|
material_output_node = material.node_tree.nodes.new(type='ShaderNodeOutputMaterial')
|
|
add_shader_node = material.node_tree.nodes.new(type='ShaderNodeAddShader')
|
|
transparent_node = material.node_tree.nodes.new(type='ShaderNodeBsdfTransparent')
|
|
|
|
# Add correct Vertex Color name
|
|
if useColorAttributes:
|
|
if len(obj.data.color_attributes) > 0:
|
|
color_attr_node.layer_name = obj.data.color_attributes[0].name
|
|
else:
|
|
if len(obj.data.vertex_colors) > 0:
|
|
color_attr_node.layer_name = obj.data.vertex_colors[0].name
|
|
|
|
# Copy the original base color over
|
|
if stored_color != None:
|
|
emission_node.inputs[0].default_value = stored_color
|
|
|
|
# Arrange nodes for clarity
|
|
color_attr_node.location = (-200, 100)
|
|
emission_node.location = (0, 100)
|
|
add_shader_node.location = (200, 100)
|
|
transparent_node.location = (0, -100)
|
|
material_output_node.location = (400, 100)
|
|
|
|
# Link nodes
|
|
links = material.node_tree.links
|
|
links.new(color_attr_node.outputs[0], emission_node.inputs[0])
|
|
links.new(emission_node.outputs[0], material_output_node.inputs[0])
|
|
|
|
# Additive links
|
|
links.new(emission_node.outputs[0], add_shader_node.inputs[0])
|
|
links.new(transparent_node.outputs[0], add_shader_node.inputs[1])
|
|
links.new(add_shader_node.outputs[0], material_output_node.inputs[0])
|
|
|
|
case "PT":
|
|
|
|
if bpy.app.version >= (4, 2, 0):
|
|
material.surface_render_method = 'DITHERED'
|
|
else:
|
|
material.blend_method = 'OPAQUE'
|
|
uses_transparency = False
|
|
has_alpha_channel = True
|
|
|
|
# Store the color of the existing Principled BSDF or Emission node (if any)
|
|
stored_color = None
|
|
for node in material.node_tree.nodes:
|
|
if node.type == 'BSDF_PRINCIPLED' or node.type == 'EMISSION':
|
|
if is_node_connected(material, node):
|
|
stored_color = tuple(node.inputs[0].default_value)
|
|
break
|
|
|
|
# Store the image of the existing image texture node (if any)
|
|
stored_image = None
|
|
for node in material.node_tree.nodes:
|
|
if node.type == 'TEX_IMAGE' and node.image:
|
|
stored_image = node.image
|
|
|
|
# Check if the found texture (if any) was being used for transparency previously
|
|
for output in node.outputs:
|
|
if output.links:
|
|
for link in output.links:
|
|
if (link.to_node.type == "BSDF_PRINCIPLED" and link.to_socket.identifier == "Alpha") or (link.to_node.type == "MIX_SHADER" and link.to_socket.identifier == "Fac") or (link.to_node.type == "MATH" and link.to_node.operation == "GREATER_THAN"):
|
|
uses_transparency = True
|
|
has_alpha_channel = True if output.identifier == "Alpha" else False
|
|
break
|
|
break
|
|
|
|
nodes = material.node_tree.nodes
|
|
links = material.node_tree.links
|
|
|
|
nodes.clear()
|
|
|
|
# Create nodes: UV Map, Image Texture, Principled BSDF, Material Output
|
|
if stored_image != None:
|
|
uv_map_node = nodes.new(type='ShaderNodeUVMap')
|
|
img_tex_node = nodes.new(type='ShaderNodeTexImage')
|
|
if len(obj.data.uv_layers) > 0:
|
|
uv_map_node.uv_map = obj.data.uv_layers[0].name
|
|
img_tex_node.image = stored_image
|
|
principled_node = nodes.new(type='ShaderNodeBsdfPrincipled')
|
|
material_output_node = nodes.new(type='ShaderNodeOutputMaterial')
|
|
|
|
# Copy the original base color over
|
|
if stored_color != None:
|
|
principled_node.inputs[0].default_value = stored_color
|
|
|
|
# Set positions for the nodes
|
|
if stored_image != None:
|
|
uv_map_node.location = (-700, 0)
|
|
img_tex_node.location = (-500, 0)
|
|
principled_node.location = (-200, 0)
|
|
material_output_node.location = (100, 0)
|
|
|
|
# Create links between nodes
|
|
if stored_image != None:
|
|
links.new(uv_map_node.outputs[0], img_tex_node.inputs[0])
|
|
links.new(img_tex_node.outputs[0], principled_node.inputs[0])
|
|
links.new(principled_node.outputs[0], material_output_node.inputs[0])
|
|
|
|
if uses_transparency:
|
|
# Blender 4.0 moved the # of the Principled BSDF's Alpha input
|
|
links.new(img_tex_node.outputs[1 if has_alpha_channel else 0],principled_node.inputs[principled_alpha_slot])
|
|
|
|
if bpy.app.version >= (4, 2, 0):
|
|
material.surface_render_method = 'DITHERED'
|
|
greaterthan_node = material.node_tree.nodes.new(type='ShaderNodeMath')
|
|
greaterthan_node.operation = "GREATER_THAN"
|
|
greaterthan_node.location = (-377, -83)
|
|
img_tex_node.location = (-655, 0)
|
|
uv_map_node.location = (-854, 0)
|
|
links.new(img_tex_node.outputs[1], greaterthan_node.inputs[0])
|
|
links.new(greaterthan_node.outputs[0], principled_node.inputs[4])
|
|
else:
|
|
material.blend_method = "CLIP"
|
|
material.alpha_threshold = 0.5
|
|
|
|
case "PC":
|
|
if bpy.app.version >= (4, 2, 0):
|
|
material.surface_render_method = 'DITHERED'
|
|
else:
|
|
material.blend_method = 'OPAQUE'
|
|
|
|
# Store the color of the existing Principled BSDF or Emissive node (if any)
|
|
stored_color = None
|
|
for node in material.node_tree.nodes:
|
|
if node.type == 'BSDF_PRINCIPLED' or node.type == 'EMISSION':
|
|
if is_node_connected(material, node):
|
|
stored_color = tuple(node.inputs[0].default_value)
|
|
break
|
|
|
|
nodes = material.node_tree.nodes
|
|
links = material.node_tree.links
|
|
|
|
nodes.clear()
|
|
|
|
# Create nodes: Color Attribute, Principled BSDF, Material Output
|
|
color_attr_node = nodes.new(type='ShaderNodeVertexColor')
|
|
principled_node = nodes.new(type='ShaderNodeBsdfPrincipled')
|
|
material_output_node = nodes.new(type='ShaderNodeOutputMaterial')
|
|
|
|
# Copy the original base color over
|
|
if stored_color != None:
|
|
principled_node.inputs[0].default_value = stored_color
|
|
|
|
# Add correct Vertex Color name
|
|
if useColorAttributes:
|
|
if len(obj.data.color_attributes) > 0:
|
|
color_attr_node.layer_name = obj.data.color_attributes[0].name
|
|
else:
|
|
if len(obj.data.vertex_colors) > 0:
|
|
color_attr_node.layer_name = obj.data.vertex_colors[0].name
|
|
|
|
# Set positions for the nodes
|
|
color_attr_node.location = (-400, 0)
|
|
principled_node.location = (-200, 0)
|
|
material_output_node.location = (100, 0)
|
|
|
|
# Create links between nodes
|
|
links.new(color_attr_node.outputs[0], principled_node.inputs[0])
|
|
links.new(principled_node.outputs[0], material_output_node.inputs[0])
|
|
|
|
case "HDRT":
|
|
|
|
if bpy.app.version >= (4, 2, 0):
|
|
material.surface_render_method = 'BLENDED'
|
|
else:
|
|
material.blend_method = 'OPAQUE'
|
|
|
|
# Store the color of the existing Principled BSDF or Emissive node (if any)
|
|
stored_color = None
|
|
for node in material.node_tree.nodes:
|
|
if node.type == 'BSDF_PRINCIPLED' or node.type == 'EMISSION':
|
|
if is_node_connected(material, node):
|
|
stored_color = tuple(node.inputs[0].default_value)
|
|
break
|
|
|
|
# Store the image of the existing image texture node (if any)
|
|
stored_image = None
|
|
stored_hdr_image = None
|
|
|
|
for node in material.node_tree.nodes:
|
|
if node.type == 'TEX_IMAGE' and node.image:
|
|
# An extra check to make sure we're not using the HDR texture as the albedo
|
|
if not ('OPEN_EXR' in node.image.file_format or 'HDR' in node.image.file_format or 'lightmap' in node.image.name_full):
|
|
if is_node_connected(material, node):
|
|
stored_image = node.image
|
|
break
|
|
|
|
for node in material.node_tree.nodes:
|
|
if node.type == 'TEX_IMAGE' and node.image:
|
|
# Find the HDR texture in the material and store it, if one was already present
|
|
if 'OPEN_EXR' in node.image.file_format or 'HDR' in node.image.file_format or 'lightmap' in node.image.name_full:
|
|
stored_hdr_image = node.image
|
|
break
|
|
|
|
# Clear existing nodes and check if the designated "skipped texture" was stored
|
|
material.node_tree.nodes.clear()
|
|
if stored_image != None:
|
|
if skip_texture != "":
|
|
if skip_texture in stored_image.filepath:
|
|
stored_image = None
|
|
|
|
# Create necessary nodes
|
|
uv_map_node = None
|
|
uv_hdr_map_node = None
|
|
img_tex_node = None
|
|
mix_color_node = None
|
|
hdr_tex_node = material.node_tree.nodes.new(type='ShaderNodeTexImage')
|
|
uv_hdr_map_node = material.node_tree.nodes.new(type='ShaderNodeUVMap')
|
|
if stored_image != None:
|
|
uv_map_node = material.node_tree.nodes.new(type='ShaderNodeUVMap')
|
|
img_tex_node = material.node_tree.nodes.new(type='ShaderNodeTexImage')
|
|
mix_color_node = material.node_tree.nodes.new(type=mix_node_type)
|
|
if "MixRGB" not in mix_node_type:
|
|
mix_color_node.data_type = 'RGBA'
|
|
mix_color_node.blend_type = 'MULTIPLY'
|
|
if "MIX_RGB" in mix_color_node.type:
|
|
mix_color_node.use_clamp = False
|
|
else:
|
|
mix_color_node.clamp_factor = False
|
|
mix_color_node.clamp_result = False
|
|
mix_color_node.inputs[0].default_value = 1.0 # Set the factor to 1.0
|
|
img_tex_node.image = stored_image
|
|
if len(obj.data.uv_layers) > 0:
|
|
uv_map_node.uv_map = obj.data.uv_layers[0].name
|
|
emission_node = material.node_tree.nodes.new(type='ShaderNodeEmission')
|
|
material_output_node = material.node_tree.nodes.new(type='ShaderNodeOutputMaterial')
|
|
|
|
# Copy the original base color over
|
|
if stored_color != None:
|
|
emission_node.inputs[0].default_value = stored_color
|
|
|
|
# Arrange nodes for clarity
|
|
if stored_image != None:
|
|
img_tex_node.location = (-500, 0)
|
|
mix_color_node.location = (-200, 100)
|
|
uv_map_node.location = (-700, 0)
|
|
hdr_tex_node.location = (-500, 350)
|
|
uv_hdr_map_node.location = (hdr_tex_node.location.x - 200, hdr_tex_node.location.y)
|
|
emission_node.location = (0, 100)
|
|
material_output_node.location = (200, 100)
|
|
|
|
# Look for an HDR texture already stored in this Blender file, if one wasn't found earlier
|
|
if stored_hdr_image != None:
|
|
hdr_tex_node.image = stored_hdr_image
|
|
else:
|
|
for searched_img in bpy.data.images:
|
|
if 'OPEN_EXR' in searched_img.file_format or 'HDR' in searched_img.file_format or 'lightmap' in searched_img.name_full:
|
|
hdr_tex_node.image = searched_img
|
|
hdr_tex_node.label = "HDR Lightmap"
|
|
uv_hdr_map_node.uv_map = "lightmap"
|
|
|
|
# Link nodes
|
|
links = material.node_tree.links
|
|
|
|
print(f"\nStored image is: {stored_image}")
|
|
if stored_image != None:
|
|
if "MixRGB" not in mix_node_type:
|
|
links.new(img_tex_node.outputs[0], mix_color_node.inputs[7])
|
|
links.new(hdr_tex_node.outputs[0], mix_color_node.inputs[6])
|
|
links.new(mix_color_node.outputs[2], emission_node.inputs[0])
|
|
else:
|
|
links.new(img_tex_node.outputs[0], mix_color_node.inputs[2])
|
|
links.new(hdr_tex_node.outputs[0], mix_color_node.inputs[1])
|
|
links.new(mix_color_node.outputs[0], emission_node.inputs[0])
|
|
links.new(emission_node.outputs[0], material_output_node.inputs[0])
|
|
links.new(uv_map_node.outputs[0], img_tex_node.inputs[0])
|
|
else:
|
|
links.new(hdr_tex_node.outputs[0], emission_node.inputs[0])
|
|
links.new(emission_node.outputs[0], material_output_node.inputs[0])
|
|
links.new(uv_hdr_map_node.outputs[0], hdr_tex_node.inputs[0])
|
|
|
|
case "PP":
|
|
node_tree = material.node_tree
|
|
|
|
uv_map_nodes = [node for node in node_tree.nodes if node.type == 'UVMAP']
|
|
|
|
for uv_map_node in uv_map_nodes:
|
|
connections = [link.to_socket for link in uv_map_node.outputs[0].links]
|
|
|
|
# Create a Separate XYZ node
|
|
separate_xyz_node = node_tree.nodes.new(type='ShaderNodeSeparateXYZ')
|
|
separate_xyz_node.label = "Ping Pong Separate"
|
|
separate_xyz_node.location = uv_map_node.location.x, uv_map_node.location.y + 200
|
|
node_tree.links.new(uv_map_node.outputs[0], separate_xyz_node.inputs[0])
|
|
|
|
# Create the first Math node
|
|
math_node_1 = node_tree.nodes.new(type='ShaderNodeMath')
|
|
math_node_1.operation = 'PINGPONG'
|
|
math_node_1.label = "Ping Pong X"
|
|
math_node_1.inputs[1].default_value = 1.0
|
|
math_node_1.location = separate_xyz_node.location.x + 200, separate_xyz_node.location.y
|
|
node_tree.links.new(separate_xyz_node.outputs[0], math_node_1.inputs[0])
|
|
|
|
# Create the second Math node
|
|
math_node_2 = node_tree.nodes.new(type='ShaderNodeMath')
|
|
math_node_2.operation = 'PINGPONG'
|
|
math_node_2.label = "Ping Pong Y"
|
|
math_node_2.inputs[1].default_value = 1.0
|
|
math_node_2.location = separate_xyz_node.location.x + 200, separate_xyz_node.location.y - 100
|
|
node_tree.links.new(separate_xyz_node.outputs[1], math_node_2.inputs[0])
|
|
|
|
# Create the Combine XYZ node
|
|
combine_xyz_node = node_tree.nodes.new(type='ShaderNodeCombineXYZ')
|
|
combine_xyz_node.location = math_node_1.location.x + 200, (math_node_1.location.y + math_node_2.location.y) / 2
|
|
combine_xyz_node.label = "Ping Pong Combine"
|
|
combine_xyz_node.inputs[2].default_value = 0.0
|
|
node_tree.links.new(math_node_1.outputs['Value'], combine_xyz_node.inputs[0])
|
|
node_tree.links.new(math_node_2.outputs['Value'], combine_xyz_node.inputs[1])
|
|
|
|
# Reconnect the original connections to the Combine XYZ node
|
|
for socket in connections:
|
|
node_tree.links.new(combine_xyz_node.outputs['Vector'], socket)
|
|
|
|
case "NO_PP":
|
|
node_tree = material.node_tree
|
|
|
|
pp_separate_nodes = [node for node in node_tree.nodes if node.label == 'Ping Pong Separate']
|
|
|
|
for pp_separate_node in pp_separate_nodes:
|
|
in_node = pp_separate_node.inputs[0].links[0].from_node
|
|
pp_x_node = pp_separate_node.outputs[0].links[0].to_node
|
|
pp_y_node = pp_separate_node.outputs[1].links[0].to_node
|
|
pp_combine_node = pp_x_node.outputs[0].links[0].to_node
|
|
out_nodes = [link.to_node for link in pp_combine_node.outputs[0].links]
|
|
|
|
# Reconnect the original connections to the Combine XYZ node
|
|
for out_node in out_nodes:
|
|
node_tree.links.new(in_node.outputs[0], out_node.inputs[0])
|
|
|
|
# Remove Ping Pong nodes
|
|
node_tree.nodes.remove(pp_x_node)
|
|
node_tree.nodes.remove(pp_y_node)
|
|
node_tree.nodes.remove(pp_combine_node)
|
|
node_tree.nodes.remove(pp_separate_node)
|
|
|
|
num_processed += 1
|
|
obj.data.update()
|
|
|
|
display_msg_box(
|
|
f'Applied template to {num_processed} material(s).', 'Info', 'INFO')
|
|
|
|
return {'FINISHED'}
|
|
|
|
# Find Active Face Texture operator
|
|
|
|
class FindActiveFaceTexture(bpy.types.Operator):
|
|
"""Finds the diffuse texture assigned to the current active face, and selects it in the Image Editor"""
|
|
bl_idname = "image.find_active_diffuse"
|
|
bl_label = "Find Active Face Texture"
|
|
bl_options = {'REGISTER'}
|
|
|
|
def execute(self, context):
|
|
|
|
list_of_mats = check_for_selected()
|
|
|
|
# Check if any objects are selected.
|
|
if list_of_mats != False:
|
|
|
|
# Get the active object
|
|
obj = bpy.context.active_object
|
|
if obj.type == "MESH":
|
|
|
|
# Get active material
|
|
mat = obj.active_material
|
|
|
|
node_tree = mat.node_tree
|
|
|
|
# Find the diffuse image texture node
|
|
diffuse = None
|
|
|
|
for node in node_tree.nodes:
|
|
if node.type == "TEX_IMAGE":
|
|
if len(node.outputs[0].links) > 0:
|
|
|
|
for link in node.outputs[0].links:
|
|
|
|
# Check if Image Texture is connected to a "color" socket," or a Mix node's A and B sockets
|
|
if "Color" in link.to_socket.name or "A" in link.to_socket.name or "B" in link.to_socket.name:
|
|
diffuse = node
|
|
break
|
|
|
|
# Check if diffuse was found already, and if so, break out of the loop
|
|
if diffuse != None:
|
|
break
|
|
|
|
# If diffuse was found:
|
|
if diffuse != None:
|
|
|
|
# Find active image editor
|
|
for screen in bpy.context.screen.areas:
|
|
if screen.type == "IMAGE_EDITOR":
|
|
for space in screen.spaces:
|
|
if space.type == "IMAGE_EDITOR":
|
|
space.image = diffuse.image
|
|
else:
|
|
continue
|
|
|
|
else:
|
|
display_msg_box(
|
|
'No texture was found. Make sure the active object has at least 1 material, with at least 1 texture assigned to it. Then go into Edit mode and select a face, then try running the operation again', 'Error', 'ERROR')
|
|
|
|
return {'FINISHED'}
|
|
|
|
# Copy Active Face Texture operator
|
|
|
|
class CopyActiveFaceTexture(bpy.types.Operator):
|
|
"""Copies the diffuse texture assigned to the current active face, and stores it for pasting in another face's material"""
|
|
bl_idname = "image.copy_active_diffuse"
|
|
bl_label = "Copy Active Face Texture"
|
|
bl_options = {'REGISTER'}
|
|
|
|
def execute(self, context):
|
|
|
|
list_of_mats = check_for_selected()
|
|
|
|
# Check if any objects are selected.
|
|
if list_of_mats != False:
|
|
|
|
# Get the active object
|
|
obj = bpy.context.active_object
|
|
if obj.type == "MESH":
|
|
|
|
# Get active material
|
|
mat = obj.active_material
|
|
|
|
node_tree = mat.node_tree
|
|
|
|
# Find the diffuse image texture node
|
|
diffuse = None
|
|
|
|
for node in node_tree.nodes:
|
|
if node.type == "TEX_IMAGE":
|
|
if len(node.outputs[0].links) > 0:
|
|
|
|
for link in node.outputs[0].links:
|
|
|
|
# Check if Image Texture is connected to a "color" socket," or a Mix node's A and B sockets
|
|
if "Color" in link.to_socket.name or "A" in link.to_socket.name or "B" in link.to_socket.name:
|
|
|
|
# Check if there's an actual image texture loaded in the node
|
|
if node.image:
|
|
diffuse = node
|
|
break
|
|
|
|
# Check if diffuse was found already, and if so, break out of the loop
|
|
if diffuse != None:
|
|
break
|
|
|
|
# If diffuse was found:
|
|
if diffuse != None:
|
|
bpy.context.scene.MatBatchProperties.CopiedTexture = diffuse.image.name
|
|
else:
|
|
display_msg_box(
|
|
'No texture was found. Make sure the active object has at least 1 material, with at least 1 texture assigned to it. Then go into Edit mode and select a face, then try running the operation again', 'Error', 'ERROR')
|
|
|
|
return {'FINISHED'}
|
|
|
|
# Paste Active Face Texture operator
|
|
|
|
class PasteActiveFaceTexture(bpy.types.Operator):
|
|
"""Pastes the previously copied diffuse texture into the selected, active face's active material"""
|
|
bl_idname = "image.paste_active_diffuse"
|
|
bl_label = "Paste Active Face Texture"
|
|
bl_options = {'REGISTER'}
|
|
|
|
def execute(self, context):
|
|
|
|
list_of_mats = check_for_selected()
|
|
|
|
# Check if any objects are selected.
|
|
if list_of_mats != False:
|
|
|
|
# Get the active object
|
|
obj = bpy.context.active_object
|
|
if obj.type == "MESH":
|
|
|
|
# Get active material
|
|
mat = obj.active_material
|
|
|
|
node_tree = mat.node_tree
|
|
|
|
# Check if a texture was already copied
|
|
copied_tex = bpy.context.scene.MatBatchProperties.CopiedTexture
|
|
|
|
if copied_tex != "" and bpy.data.images[copied_tex]:
|
|
|
|
# Find the diffuse image texture node
|
|
diffuse = None
|
|
|
|
for node in node_tree.nodes:
|
|
if node.type == "TEX_IMAGE":
|
|
if len(node.outputs[0].links) > 0:
|
|
|
|
for link in node.outputs[0].links:
|
|
|
|
# Check if Image Texture is connected to a "color" socket," or a Mix node's A and B sockets
|
|
if "Color" in link.to_socket.name or "A" in link.to_socket.name or "B" in link.to_socket.name:
|
|
diffuse = node
|
|
break
|
|
|
|
# Check if diffuse was found already, and if so, break out of the loop
|
|
if diffuse != None:
|
|
break
|
|
|
|
# If diffuse was found:
|
|
if diffuse != None:
|
|
diffuse.image = bpy.data.images[copied_tex]
|
|
else:
|
|
display_msg_box(
|
|
'No image texture node was found. Make sure the active object has at least 1 material, with at least 1 image texture node in its node tree.', 'Error', 'ERROR')
|
|
|
|
return {'FINISHED'}
|
|
|
|
|
|
# Copy Texture to Material Name operator
|
|
|
|
|
|
class CopyTexToMatName(bpy.types.Operator):
|
|
"""Finds the diffuse texture in all materials, in all selected objects, and renames the material to the diffuse's texture name (minus the file extension). If the material contains multiple diffuse textures, all of them will be appended to the material name"""
|
|
bl_idname = "material.copy_tex_to_mat_name"
|
|
bl_label = "Copy Diffuse Texture to Material Name"
|
|
bl_options = {'REGISTER'}
|
|
|
|
def execute(self, context):
|
|
|
|
num_processed = 0
|
|
|
|
list_of_mats = check_for_selected()
|
|
mats_to_rename = dict()
|
|
|
|
# Check if any objects are selected.
|
|
if list_of_mats != False:
|
|
|
|
# For each selected object
|
|
for obj in bpy.context.selected_objects:
|
|
if obj.type == "MESH":
|
|
|
|
# For each material
|
|
for mat in list_of_mats:
|
|
diffuse_textures_found = set()
|
|
|
|
if mat in bpy.data.materials.keys():
|
|
node_tree = bpy.data.materials[mat].node_tree
|
|
|
|
# Find the diffuse image texture node
|
|
if node_tree != None:
|
|
for node in node_tree.nodes:
|
|
|
|
if node.type == "TEX_IMAGE" and is_node_connected(bpy.data.materials[mat], node):
|
|
|
|
# Check if a file is actually loaded in this image texture node
|
|
if node.image:
|
|
|
|
if len(node.outputs[0].links) > 0:
|
|
|
|
for link in node.outputs[0].links:
|
|
|
|
# Check if Image Texture is connected to a "color" socket," or a Mix node's A and B sockets
|
|
if "Color" in link.to_socket.name or "A" in link.to_socket.name or "B" in link.to_socket.name or link.to_socket.type == 'RGBA':
|
|
# Get the texture name, but without the file extension
|
|
diffuse_name = node.image.name.split(".", 1)[0]
|
|
diffuse_textures_found.add(diffuse_name)
|
|
continue
|
|
else:
|
|
continue
|
|
|
|
if len(diffuse_textures_found) != None:
|
|
mats_to_rename[mat] = diffuse_textures_found
|
|
|
|
if len(mats_to_rename.keys()) != 0:
|
|
|
|
# If diffuse was found:
|
|
for mat in mats_to_rename.keys():
|
|
finalized_name = ""
|
|
|
|
# Change the current material's name
|
|
for texture in mats_to_rename[mat]:
|
|
if finalized_name != "":
|
|
finalized_name = finalized_name + " "
|
|
finalized_name = finalized_name + texture
|
|
|
|
# Check if a material with that name already exists
|
|
if finalized_name in bpy.data.materials.keys():
|
|
if mat in obj.material_slots.keys():
|
|
|
|
# Compare the node count for the materials - make sure they have the same amount of nodes, to avoid mismatching any unique but very similar materials
|
|
if len(bpy.data.materials[mat].node_tree.nodes) != len(bpy.data.materials[finalized_name].node_tree.nodes):
|
|
finalized_name += " " + str(len(bpy.data.materials[mat].node_tree.nodes))
|
|
|
|
if finalized_name in bpy.data.materials.keys():
|
|
if mat in obj.material_slots.keys():
|
|
old_index = obj.material_slots[mat].slot_index
|
|
if obj.material_slots[old_index].material.name != finalized_name:
|
|
obj.material_slots[old_index].material = bpy.data.materials[finalized_name]
|
|
num_processed += 1
|
|
|
|
else:
|
|
if mat in bpy.data.materials.keys():
|
|
bpy.data.materials[mat].name = finalized_name
|
|
if mat in list_of_mats:
|
|
list_of_mats.remove(mat)
|
|
num_processed += 1
|
|
|
|
display_msg_box(
|
|
f'Renamed {num_processed} material(s).', 'Info', 'INFO')
|
|
else:
|
|
display_msg_box(
|
|
'No diffuse textures found.', 'Error', 'ERROR')
|
|
|
|
return {'FINISHED'}
|
|
|
|
|
|
# Isolate by Material Trait operator
|
|
|
|
class IsolateByMatTrait(bpy.types.Operator):
|
|
"""Searches any currently selected meshes for assigned materials with a specific trait, and isolates those polygons into a separate object (and optionally, a dedicated collection)"""
|
|
bl_idname = "material.isolate_by_trait"
|
|
bl_label = "Isolate by Material Trait"
|
|
bl_options = {'REGISTER'}
|
|
|
|
def execute(self, context):
|
|
|
|
list_of_mats = check_for_selected()
|
|
isolate_to_collection = bpy.context.scene.MatBatchProperties.IsolateCollection
|
|
principled_alpha_slot = 21 if bpy.app.version < (4, 0, 0) else 4
|
|
principled_emissive_color_slot = 19 if bpy.app.version < (4, 0, 0) else 27
|
|
principled_emissive_strength_slot = 20 if bpy.app.version < (4, 0, 0) else 28
|
|
trait = bpy.context.scene.MatBatchProperties.IsolateTrait
|
|
matching_materials = set()
|
|
separated_objs = set()
|
|
|
|
root_collection = None
|
|
|
|
# Check if any objects are selected.
|
|
if list_of_mats != False:
|
|
|
|
for obj in bpy.context.selected_objects:
|
|
if obj.type == "MESH":
|
|
|
|
for mat in list_of_mats:
|
|
|
|
material = bpy.data.materials[mat]
|
|
material.use_nodes = True
|
|
is_transparent = False
|
|
is_emissive = False
|
|
is_animated = False
|
|
|
|
if material and material.use_nodes:
|
|
for node in material.node_tree.nodes:
|
|
if is_node_connected(material, node):
|
|
|
|
if trait == "transparent":
|
|
|
|
# Transparency scenario 1 - Principled BSDF with alpha input
|
|
if node.type == "BSDF_PRINCIPLED":
|
|
if len(node.inputs[principled_alpha_slot].links) > 0 or node.inputs[principled_alpha_slot].default_value != 1:
|
|
is_transparent = True
|
|
break
|
|
|
|
# Transparency scenario 2 - Transparent BSDF
|
|
elif node.type == "BSDF_TRANSPARENT":
|
|
is_transparent = True
|
|
break
|
|
|
|
if trait == "emissive":
|
|
|
|
# Emissive scenario 1 - Principled BSDF with emissive input
|
|
if node.type == "BSDF_PRINCIPLED":
|
|
em_color_slot = node.inputs[principled_emissive_color_slot]
|
|
em_strength_slot = node.inputs[principled_emissive_strength_slot]
|
|
if em_strength_slot.default_value != 0.0 or len(em_strength_slot.links) > 0:
|
|
if len(em_color_slot.links) > 0:
|
|
is_emissive = True
|
|
break
|
|
elif list(em_color_slot.default_value) != [0.0,0.0,0.0,1.0] and list(em_color_slot.default_value) != [0.0,0.0,0.0,0.0]:
|
|
is_emissive = True
|
|
break
|
|
|
|
# Emissive scenario 2 - Emission shader
|
|
elif node.type == "EMISSION":
|
|
is_emissive = True
|
|
break
|
|
|
|
|
|
if trait == "animated":
|
|
|
|
# Animated scenario 1 - Animation data exists and isn't "none"
|
|
if material.node_tree.animation_data != None:
|
|
is_animated = True
|
|
break
|
|
|
|
# Animated scenario 2 - Image Sequence node
|
|
elif node.type == "TEX_IMAGE":
|
|
if node.image.source == 'SEQUENCE':
|
|
is_animated = True
|
|
break
|
|
|
|
if is_transparent and trait == "transparent":
|
|
matching_materials.add(material.name)
|
|
if is_emissive and trait == "emissive":
|
|
matching_materials.add(material.name)
|
|
if is_animated and trait == "animated":
|
|
matching_materials.add(material.name)
|
|
|
|
materials_matched_count = 0
|
|
if len(matching_materials) > 0:
|
|
|
|
# Search all selected mesh objects for faces that have this material assigned.
|
|
mesh_objects = [obj for obj in bpy.context.selected_objects if obj.type == "MESH" and not (obj.name.endswith("_" + trait))]
|
|
if len(mesh_objects) == 0:
|
|
display_msg_box(
|
|
f'Selected trait has already been fully isolated in the selected object(s).', 'Info', 'INFO')
|
|
return {'FINISHED'}
|
|
|
|
bpy.ops.object.select_all(action='DESELECT')
|
|
for obj in mesh_objects:
|
|
obj.hide_set(False)
|
|
obj.select_set(True)
|
|
bpy.context.view_layer.objects.active = obj
|
|
|
|
matching_faces = set()
|
|
for material in matching_materials:
|
|
material_matched = False
|
|
for face_index in find_faces_with_material(obj, material):
|
|
matching_faces.add(face_index)
|
|
material_matched = True
|
|
if material_matched:
|
|
materials_matched_count += 1
|
|
|
|
if len(matching_faces) > 0:
|
|
if len(obj.data.polygons) == len(matching_faces):
|
|
continue
|
|
separated_obj = separate_faces(obj, matching_faces)
|
|
separated_obj.name = obj.name + "_" + trait
|
|
|
|
separated_objs.add(separated_obj)
|
|
|
|
if isolate_to_collection:
|
|
if trait.capitalize() in bpy.data.collections.keys():
|
|
root_collection = bpy.data.collections[trait.capitalize()]
|
|
else:
|
|
root_collection = bpy.data.collections.new(trait.capitalize())
|
|
bpy.context.scene.collection.children.link(root_collection)
|
|
|
|
for obj in separated_objs:
|
|
# Unlink the new collision model from other collections
|
|
obj_collections = [
|
|
c for c in bpy.data.collections if obj.name in c.objects.keys()]
|
|
for c in obj_collections:
|
|
if obj.name in c.objects.keys():
|
|
c.objects.unlink(obj)
|
|
if obj.name in bpy.context.scene.collection.objects.keys():
|
|
bpy.context.scene.collection.objects.unlink(obj)
|
|
|
|
root_collection.objects.link(obj)
|
|
bpy.context.view_layer.objects.active = obj
|
|
obj.select_set(True)
|
|
|
|
display_msg_box(
|
|
f'Isolated {materials_matched_count} {trait} material(s) into {len(separated_objs)} separate object(s).', 'Info', 'INFO')
|
|
return {'FINISHED'}
|
|
|
|
# Update Backface Culling operator
|
|
|
|
class UpdateBackfaceCulling(bpy.types.Operator):
|
|
"""Updates the backface culling settings in all materials in all selected objects, based on the settings above"""
|
|
bl_idname = "material.update_backface_culling"
|
|
bl_label = "Update Backface Culling"
|
|
bl_options = {'REGISTER'}
|
|
|
|
def execute(self, context):
|
|
|
|
num_processed = 0
|
|
list_of_mats = check_for_selected()
|
|
|
|
# Check if any objects are selected.
|
|
if list_of_mats != False:
|
|
|
|
backface_culling_camera = bpy.context.scene.MatBatchProperties.BackfaceCamera
|
|
backface_culling_shadow = bpy.context.scene.MatBatchProperties.BackfaceShadow
|
|
backface_culling_lightprobe = bpy.context.scene.MatBatchProperties.BackfaceLightProbe
|
|
|
|
for mat in list_of_mats:
|
|
material = bpy.data.materials[mat]
|
|
material.use_backface_culling = backface_culling_camera
|
|
|
|
if bpy.app.version >= (4, 1, 0):
|
|
material.use_backface_culling_shadow = backface_culling_shadow
|
|
if bpy.app.version >= (4, 2, 0):
|
|
material.use_backface_culling_lightprobe_volume = backface_culling_lightprobe
|
|
|
|
num_processed += 1
|
|
|
|
display_msg_box(
|
|
f'Updated backface culling settings in {num_processed} material(s).', 'Info', 'INFO')
|
|
|
|
return {'FINISHED'}
|
|
|
|
|
|
# Rename All Textures by Hash
|
|
|
|
class RenameTexturesByHash(bpy.types.Operator):
|
|
"""Rename ALL textures in this Blender file by generating a unique MD5-generated hash for each texture"""
|
|
bl_idname = "material.rename_textures_by_hash"
|
|
bl_label = "Rename All Textures by Hash"
|
|
bl_options = {'REGISTER'}
|
|
|
|
def execute(self, context):
|
|
|
|
num_processed = 0
|
|
num_dupes_removed = 0
|
|
duplicates_to_remove = set()
|
|
original_images = set()
|
|
|
|
def generate_hash_from_image(image):
|
|
pixels = list(image.pixels) # Get image pixels as a flat list
|
|
pixel_data = ''.join([str(int(p * 255)) for p in pixels]) # Convert to string
|
|
hash_object = hashlib.md5(pixel_data.encode()) # Generate MD5 hash
|
|
return hash_object.hexdigest() # Return hexadecimal MD5 hash string
|
|
|
|
for image in bpy.data.images:
|
|
original_images.add(image)
|
|
|
|
for image in original_images:
|
|
hash_name = generate_hash_from_image(image)[:32]
|
|
if hash_name[:32] in bpy.data.images.keys():
|
|
duplicates_to_remove.add(hash_name)
|
|
image.name = hash_name
|
|
num_processed += 1
|
|
|
|
for material in bpy.data.materials:
|
|
if material.node_tree:
|
|
for node in material.node_tree.nodes:
|
|
if node.type == 'TEX_IMAGE' and node.image.name.split(".")[0] in duplicates_to_remove:
|
|
node.image = bpy.data.images[node.image.name.split(".")[0]]
|
|
else:
|
|
continue
|
|
|
|
# Remove any duplicate images
|
|
for image in bpy.data.images:
|
|
if "." in image.name:
|
|
bpy.data.images.remove(image)
|
|
num_dupes_removed += 1
|
|
|
|
if len(bpy.data.images) > 0:
|
|
display_msg_box(
|
|
f'Renamed {num_processed} texture(s).\nRemoved {str(num_dupes_removed)} duplicate textures.', 'Info', 'INFO')
|
|
else:
|
|
display_msg_box(
|
|
'No textures found.', 'Error', 'ERROR')
|
|
|
|
return {'FINISHED'}
|
|
|
|
# End classes
|
|
|
|
|
|
def menu_func(self, context):
|
|
self.layout.operator(PasteBakeTargetNode.bl_idname)
|
|
self.layout.operator(CopyBakeTargetNode.bl_idname)
|
|
self.layout.operator(DeleteBakeTargetNode.bl_idname)
|
|
self.layout.operator(AssignUVMapNode.bl_idname)
|
|
self.layout.operator(OverwriteUVSlotName.bl_idname)
|
|
self.layout.operator(SetUVSlotAsActive.bl_idname)
|
|
self.layout.operator(AssignVCToNodes.bl_idname)
|
|
self.layout.operator(SetBlendMode.bl_idname)
|
|
self.layout.operator(SetAsTemplateNode.bl_idname)
|
|
self.layout.operator(UnifyNodeSettings.bl_idname)
|
|
self.layout.operator(SwitchShader.bl_idname)
|
|
self.layout.operator(ApplyMatTemplate.bl_idname)
|
|
self.layout.operator(FindActiveFaceTexture.bl_idname)
|
|
self.layout.operator(CopyTexToMatName.bl_idname)
|
|
self.layout.operator(IsolateByMatTrait.bl_idname)
|
|
self.layout.operator(RenameTexturesByHash.bl_idname)
|
|
|
|
|
|
def imageeditor_menu_func(self, context):
|
|
self.layout.operator(FindActiveFaceTexture.bl_idname)
|
|
self.layout.operator(CopyActiveFaceTexture.bl_idname)
|
|
self.layout.operator(PasteActiveFaceTexture.bl_idname)
|
|
self.layout.operator(RenameTexturesByHash.bl_idname)
|
|
self.layout.operator(CopyTexToMatName.bl_idname)
|
|
|
|
# MATERIALS PANEL
|
|
|
|
|
|
class MaterialBatchToolsPanel(bpy.types.Panel):
|
|
bl_label = 'Material Batch Tools'
|
|
bl_idname = "MATERIAL_PT_matbatchtools"
|
|
bl_space_type = 'PROPERTIES'
|
|
bl_region_type = 'WINDOW'
|
|
bl_context = 'material'
|
|
|
|
@ classmethod
|
|
def poll(cls, context):
|
|
return (context.object != None)
|
|
|
|
def draw_header(self, context):
|
|
layout = self.layout
|
|
|
|
def draw(self, context):
|
|
layout = self.layout
|
|
layout.enabled = (len(bpy.context.selected_objects) > 0)
|
|
|
|
class MaterialBatchToolsSubPanel_Nodes(bpy.types.Panel):
|
|
bl_parent_id = "MATERIAL_PT_matbatchtools"
|
|
bl_label = 'Nodes'
|
|
bl_idname = "MATERIAL_PT_matbatchtools_nodes"
|
|
bl_space_type = 'PROPERTIES'
|
|
bl_region_type = 'WINDOW'
|
|
bl_options = {"DEFAULT_CLOSED"}
|
|
bl_context = 'material'
|
|
|
|
@ classmethod
|
|
def poll(cls, context):
|
|
return (context.object != None)
|
|
|
|
def draw_header(self, context):
|
|
layout = self.layout
|
|
|
|
def draw(self, context):
|
|
layout = self.layout
|
|
|
|
# Node Unify UI
|
|
boxUnify = layout.box()
|
|
boxUnify.label(text="Node Unify")
|
|
rowUnify1 = boxUnify.row()
|
|
rowUnify1.label(text="Saved Node: " +
|
|
bpy.context.scene.MatBatchProperties.SavedNodeType)
|
|
rowUnify2 = boxUnify.row()
|
|
rowUnify3 = boxUnify.row()
|
|
rowUnify4 = boxUnify.row()
|
|
|
|
rowUnify2.prop(
|
|
bpy.context.scene.MatBatchProperties, "UnifyFilterLabel")
|
|
rowUnify3.operator("material.set_as_template_node")
|
|
rowUnify4.operator("material.unify_node_settings")
|
|
rowUnify4.enabled = (
|
|
bpy.context.scene.MatBatchProperties.SavedNodeName != "")
|
|
|
|
layout.separator()
|
|
|
|
# Bake Target Node UI
|
|
boxBakeTarget = layout.box()
|
|
boxBakeTarget.label(text="Bake Target Node")
|
|
|
|
rowBakeTarget0 = boxBakeTarget.row()
|
|
rowBakeTarget1 = boxBakeTarget.row()
|
|
rowBakeTarget2 = boxBakeTarget.row()
|
|
|
|
rowBakeTarget0.operator("material.copy_bake_target")
|
|
rowBakeTarget0.operator("material.paste_bake_target")
|
|
rowBakeTarget1.operator("material.delete_bake_target")
|
|
rowBakeTarget2.prop(
|
|
bpy.context.scene.MatBatchProperties, "BakeTargetNodeColorEnable")
|
|
rowBakeTarget2.prop(
|
|
bpy.context.scene.MatBatchProperties, "BakeTargetNodeColor")
|
|
|
|
layout.separator()
|
|
|
|
# Material Template UI
|
|
boxTemplate = layout.box()
|
|
boxTemplate.label(text="Material Templates")
|
|
rowSwitchShader1 = boxTemplate.row()
|
|
rowSwitchShader2 = boxTemplate.row()
|
|
rowTemplate1 = boxTemplate.row()
|
|
rowTemplate2 = boxTemplate.row()
|
|
rowTemplate3 = boxTemplate.row()
|
|
|
|
rowSwitchShader1.prop(
|
|
bpy.context.scene.MatBatchProperties, "SwitchShaderTarget")
|
|
rowSwitchShader2.operator("material.switch_shader")
|
|
|
|
rowTemplate1.prop(bpy.context.scene.MatBatchProperties, "Template")
|
|
rowTemplate2.prop(bpy.context.scene.MatBatchProperties, "SkipTexture")
|
|
rowTemplate3.operator("material.apply_mat_template")
|
|
|
|
class MaterialBatchToolsSubPanel_Transparency(bpy.types.Panel):
|
|
bl_parent_id = "MATERIAL_PT_matbatchtools"
|
|
bl_label = 'Transparency & Backface Culling'
|
|
bl_idname = "MATERIAL_PT_matbatchtools_transparency"
|
|
bl_space_type = 'PROPERTIES'
|
|
bl_region_type = 'WINDOW'
|
|
bl_options = {"DEFAULT_CLOSED"}
|
|
bl_context = 'material'
|
|
|
|
@ classmethod
|
|
def poll(cls, context):
|
|
return (context.object != None)
|
|
|
|
def draw_header(self, context):
|
|
layout = self.layout
|
|
|
|
def draw(self, context):
|
|
layout = self.layout
|
|
|
|
# Transparency UI
|
|
boxTransparency = layout.box()
|
|
boxTransparency.label(text="Transparency")
|
|
rowTransparency1 = boxTransparency.row()
|
|
rowTransparency2 = boxTransparency.row()
|
|
rowTransparency3 = boxTransparency.row()
|
|
rowTransparency4 = boxTransparency.row()
|
|
rowTransparency5 = boxTransparency.row()
|
|
rowTransparency1.prop(
|
|
bpy.context.scene.MatBatchProperties, "AlphaBlendMode")
|
|
rowTransparency2.prop(bpy.context.scene.MatBatchProperties,
|
|
"AlphaBlendFilter")
|
|
rowTransparency3.prop(bpy.context.scene.MatBatchProperties,
|
|
"AlphaThreshold")
|
|
rowTransparency3.enabled = (
|
|
bpy.context.scene.MatBatchProperties.AlphaBlendMode == "CLIP")
|
|
rowTransparency4.prop(
|
|
bpy.context.scene.MatBatchProperties, "AlphaPrincipledRemove")
|
|
rowTransparency4.enabled = (
|
|
bpy.context.scene.MatBatchProperties.AlphaBlendMode == "OPAQUE")
|
|
rowTransparency5.operator("material.set_blend_mode")
|
|
|
|
# Backface Culling UI
|
|
boxBackfaceCulling = layout.box()
|
|
boxBackfaceCulling.label(text="Backface Culling")
|
|
rowBackfaceCulling1 = boxBackfaceCulling.row()
|
|
rowBackfaceCulling2 = boxBackfaceCulling.row()
|
|
rowBackfaceCulling3 = boxBackfaceCulling.row()
|
|
rowBackfaceCulling4 = boxBackfaceCulling.row()
|
|
rowBackfaceCulling1.prop(
|
|
bpy.context.scene.MatBatchProperties, "BackfaceCamera")
|
|
rowBackfaceCulling2.prop(bpy.context.scene.MatBatchProperties,
|
|
"BackfaceShadow")
|
|
rowBackfaceCulling3.prop(bpy.context.scene.MatBatchProperties,
|
|
"BackfaceLightProbe")
|
|
rowBackfaceCulling2.enabled = bpy.app.version >= (4, 2, 0)
|
|
rowBackfaceCulling3.enabled = bpy.app.version >= (4, 2, 0)
|
|
rowBackfaceCulling4.operator("material.update_backface_culling")
|
|
|
|
class MaterialBatchToolsSubPanel_UV_VC(bpy.types.Panel):
|
|
bl_parent_id = "MATERIAL_PT_matbatchtools"
|
|
bl_label = 'UV & Vertex Colors'
|
|
bl_idname = "MATERIAL_PT_matbatchtools_uv_vc"
|
|
bl_space_type = 'PROPERTIES'
|
|
bl_region_type = 'WINDOW'
|
|
bl_options = {"DEFAULT_CLOSED"}
|
|
bl_context = 'material'
|
|
|
|
@ classmethod
|
|
def poll(cls, context):
|
|
return (context.object != None)
|
|
|
|
def draw_header(self, context):
|
|
layout = self.layout
|
|
|
|
def draw(self, context):
|
|
layout = self.layout
|
|
|
|
# UV Map Node UI
|
|
boxUVMap1 = layout.box()
|
|
rowUVMap1 = boxUVMap1.row()
|
|
rowUVMap2 = boxUVMap1.row()
|
|
rowUVMap3 = boxUVMap1.row()
|
|
|
|
rowUVMap1.prop(bpy.context.scene.MatBatchProperties,
|
|
"UVMapNodeTarget")
|
|
rowUVMap2.prop(bpy.context.scene.MatBatchProperties,
|
|
"UVMapNodeExtensionFilter")
|
|
rowUVMap3.operator("material.assign_uv_map_node")
|
|
|
|
boxUVMap2 = boxUVMap1.box()
|
|
rowUVMap4 = boxUVMap2.row()
|
|
rowUVMap5 = boxUVMap2.row()
|
|
rowUVMap6 = boxUVMap2.row()
|
|
|
|
# UV Slot UI
|
|
rowUVMap4.prop(bpy.context.scene.MatBatchProperties, "UVSlotIndex")
|
|
rowUVMap5.operator("object.overwrite_uv_slot_name")
|
|
rowUVMap6.operator("object.set_uv_slot_as_active")\
|
|
|
|
layout.separator()
|
|
|
|
# Vertex Colors UI
|
|
boxVertexColors = layout.box()
|
|
boxVertexColors.label(text="Vertex Colors")
|
|
rowVertexColors1 = boxVertexColors.row()
|
|
rowVertexColors2 = boxVertexColors.row()
|
|
rowVertexColors3 = boxVertexColors.row()
|
|
rowVertexColors4 = boxVertexColors.row()
|
|
|
|
rowVertexColors1.prop(bpy.context.scene.MatBatchProperties, "VCName")
|
|
rowVertexColors2.operator("material.assign_vc_to_nodes")
|
|
rowVertexColors3.operator("object.rename_vertex_color")
|
|
rowVertexColors4.operator("object.convert_vertex_color")
|
|
|
|
class MaterialBatchToolsSubPanel_Isolate(bpy.types.Panel):
|
|
bl_parent_id = "MATERIAL_PT_matbatchtools"
|
|
bl_label = 'Isolate'
|
|
bl_idname = "MATERIAL_PT_matbatchtools_isolate"
|
|
bl_space_type = 'PROPERTIES'
|
|
bl_region_type = 'WINDOW'
|
|
bl_options = {"DEFAULT_CLOSED"}
|
|
bl_context = 'material'
|
|
|
|
@ classmethod
|
|
def poll(cls, context):
|
|
return (context.object != None)
|
|
|
|
def draw_header(self, context):
|
|
layout = self.layout
|
|
|
|
def draw(self, context):
|
|
layout = self.layout
|
|
|
|
# UV Map Node UI
|
|
boxIsolate = layout.box()
|
|
rowIsolate1 = boxIsolate.row()
|
|
rowIsolate2 = boxIsolate.row()
|
|
rowIsolate3 = boxIsolate.row()
|
|
|
|
rowIsolate1.prop(bpy.context.scene.MatBatchProperties, "IsolateCollection")
|
|
rowIsolate2.prop(bpy.context.scene.MatBatchProperties, "IsolateTrait")
|
|
rowIsolate3.operator("material.isolate_by_trait")
|
|
|
|
|
|
# End of classes
|
|
|
|
|
|
classes = (
|
|
MatBatchProperties,
|
|
CopyBakeTargetNode,
|
|
PasteBakeTargetNode,
|
|
DeleteBakeTargetNode,
|
|
AssignUVMapNode,
|
|
OverwriteUVSlotName,
|
|
SetUVSlotAsActive,
|
|
AssignVCToNodes,
|
|
RenameVertexColorSlot,
|
|
ConvertVertexColor,
|
|
SetBlendMode,
|
|
SetAsTemplateNode,
|
|
UnifyNodeSettings,
|
|
SwitchShader,
|
|
ApplyMatTemplate,
|
|
FindActiveFaceTexture,
|
|
CopyActiveFaceTexture,
|
|
PasteActiveFaceTexture,
|
|
CopyTexToMatName,
|
|
IsolateByMatTrait,
|
|
UpdateBackfaceCulling,
|
|
RenameTexturesByHash,
|
|
MaterialBatchToolsPanel,
|
|
MaterialBatchToolsSubPanel_Nodes,
|
|
MaterialBatchToolsSubPanel_UV_VC,
|
|
MaterialBatchToolsSubPanel_Transparency,
|
|
MaterialBatchToolsSubPanel_Isolate
|
|
)
|
|
|
|
|
|
def register():
|
|
for cls in classes:
|
|
bpy.utils.register_class(cls)
|
|
|
|
bpy.types.Scene.MatBatchProperties = bpy.props.PointerProperty(
|
|
type=MatBatchProperties)
|
|
|
|
bpy.types.IMAGE_MT_image.append(imageeditor_menu_func)
|
|
|
|
|
|
def unregister():
|
|
for cls in classes:
|
|
bpy.utils.unregister_class(cls)
|
|
|
|
del bpy.types.Scene.MatBatchProperties
|
|
|
|
bpy.types.IMAGE_MT_image.remove(imageeditor_menu_func)
|
|
|
|
|
|
if __name__ == "__main__":
|
|
register()
|