342 lines
16 KiB
Python
342 lines
16 KiB
Python
import bpy
|
|
from bpy.types import Operator
|
|
from bpy.props import StringProperty
|
|
from pathlib import Path
|
|
|
|
# Assuming the utility nodegroups file is located relative to the addon
|
|
# This path might need adjustment depending on final addon distribution
|
|
UTILITY_NODEGROUPS_FILE = Path(__file__).parent / "blender_files" / "utility_nodegroups.blend"
|
|
MATERIAL_MERGE_NODEGROUP_NAME = "MaterialMerge"
|
|
HANDLER_NODEGROUP_NAME = "PBR_Handler" # Assumption from plan
|
|
BSDF_NODEGROUP_NAME = "PBR_BSDF" # Assumption from plan
|
|
|
|
# Helper function to copy nodes and identify outputs
|
|
def copy_material_nodes(source_mat, target_tree, location_offset=(0, 0)):
|
|
"""
|
|
Copies nodes from source_mat's node tree to target_tree, applying an offset.
|
|
Identifies and returns the copied nodes corresponding to the final BSDF and Displacement outputs.
|
|
|
|
Returns:
|
|
tuple: (copied_node_map, copied_final_bsdf_node, copied_final_disp_node)
|
|
Returns (None, None, None) on failure.
|
|
"""
|
|
if not source_mat or not source_mat.node_tree:
|
|
print(f"Error: Source material '{source_mat.name if source_mat else 'None'}' has no node tree.")
|
|
return None, None, None
|
|
|
|
source_tree = source_mat.node_tree
|
|
copied_node_map = {} # Map original node to copied node
|
|
copied_final_bsdf_node = None
|
|
copied_final_disp_node = None
|
|
|
|
# --- Identify Final Output Nodes in Source Tree ---
|
|
# This logic needs to handle both base materials and already-merged materials
|
|
source_final_bsdf_node = None
|
|
source_final_disp_node = None
|
|
|
|
# Try finding a top-level MaterialMerge node first (for recursive merging)
|
|
top_merge_node = None
|
|
for node in source_tree.nodes:
|
|
if node.type == 'GROUP' and node.node_tree and node.node_tree.name == MATERIAL_MERGE_NODEGROUP_NAME:
|
|
# Check if it's connected to the Material Output (likely the top one)
|
|
for link in source_tree.links:
|
|
if link.from_node == node and link.to_node.type == 'OUTPUT_MATERIAL':
|
|
top_merge_node = node
|
|
break
|
|
if top_merge_node:
|
|
break
|
|
|
|
if top_merge_node:
|
|
print(f" Identified top-level '{MATERIAL_MERGE_NODEGROUP_NAME}' in '{source_mat.name}'. Using its outputs.")
|
|
source_final_bsdf_node = top_merge_node
|
|
source_final_disp_node = top_merge_node # Both outputs come from the merge node
|
|
# Ensure the sockets exist before proceeding
|
|
if 'BSDF' not in source_final_bsdf_node.outputs or 'Displacement' not in source_final_disp_node.outputs:
|
|
print(f" Error: Identified merge node in '{source_mat.name}' lacks required BSDF/Displacement outputs.")
|
|
return None, None, None
|
|
else:
|
|
# If no top-level merge node, assume it's a base material
|
|
print(f" No top-level merge node found in '{source_mat.name}'. Assuming base material structure.")
|
|
source_final_bsdf_node = source_tree.nodes.get(BSDF_NODEGROUP_NAME)
|
|
source_final_disp_node = source_tree.nodes.get(HANDLER_NODEGROUP_NAME) # Displacement from Handler
|
|
if not source_final_bsdf_node:
|
|
print(f" Error: Could not find base BSDF node '{BSDF_NODEGROUP_NAME}' in '{source_mat.name}'.")
|
|
return None, None, None
|
|
if not source_final_disp_node:
|
|
print(f" Error: Could not find base Handler node '{HANDLER_NODEGROUP_NAME}' in '{source_mat.name}'.")
|
|
return None, None, None
|
|
# Ensure sockets exist
|
|
if 'BSDF' not in source_final_bsdf_node.outputs:
|
|
print(f" Error: Identified BSDF node '{BSDF_NODEGROUP_NAME}' lacks BSDF output.")
|
|
return None, None, None
|
|
if 'Displacement' not in source_final_disp_node.outputs:
|
|
print(f" Error: Identified Handler node '{HANDLER_NODEGROUP_NAME}' lacks Displacement output.")
|
|
return None, None, None
|
|
|
|
|
|
# --- Copy Nodes ---
|
|
print(f" Copying nodes from '{source_mat.name}'...")
|
|
for original_node in source_tree.nodes:
|
|
if original_node.type == 'OUTPUT_MATERIAL':
|
|
continue # Skip the material output node
|
|
|
|
new_node = target_tree.nodes.new(type=original_node.bl_idname)
|
|
# Copy properties (basic example, might need more specific handling)
|
|
for prop in original_node.bl_rna.properties:
|
|
if not prop.is_readonly and prop.identifier != "rna_type":
|
|
try:
|
|
setattr(new_node, prop.identifier, getattr(original_node, prop.identifier))
|
|
except AttributeError:
|
|
pass # Some properties might not be directly settable
|
|
|
|
# Copy specific node group if it's a group node
|
|
if original_node.type == 'GROUP' and original_node.node_tree:
|
|
new_node.node_tree = original_node.node_tree # Link the same node group
|
|
|
|
new_node.location = (original_node.location.x + location_offset[0],
|
|
original_node.location.y + location_offset[1])
|
|
new_node.width = original_node.width
|
|
new_node.label = original_node.label
|
|
new_node.name = original_node.name # Keep original name if possible (Blender might rename on conflict)
|
|
|
|
copied_node_map[original_node] = new_node
|
|
|
|
# Store the *copied* versions of the identified final output nodes
|
|
if original_node == source_final_bsdf_node:
|
|
copied_final_bsdf_node = new_node
|
|
if original_node == source_final_disp_node:
|
|
# If source was merge node, both point to the same copied node
|
|
# If source was base material, this points to the copied handler
|
|
copied_final_disp_node = new_node
|
|
|
|
# --- Copy Links ---
|
|
print(f" Copying links for '{source_mat.name}'...")
|
|
for original_link in source_tree.links:
|
|
original_from_node = original_link.from_node
|
|
original_to_node = original_link.to_node
|
|
|
|
# Check if both ends of the link were copied (i.e., not connected to Material Output)
|
|
if original_from_node in copied_node_map and original_to_node in copied_node_map:
|
|
new_from_node = copied_node_map[original_from_node]
|
|
new_to_node = copied_node_map[original_to_node]
|
|
|
|
# Find matching sockets by name (more robust than index)
|
|
try:
|
|
from_socket_name = original_link.from_socket.name
|
|
to_socket_name = original_link.to_socket.name
|
|
new_from_socket = new_from_node.outputs.get(from_socket_name)
|
|
new_to_socket = new_to_node.inputs.get(to_socket_name)
|
|
|
|
if new_from_socket and new_to_socket:
|
|
target_tree.links.new(new_from_socket, new_to_socket)
|
|
else:
|
|
print(f" Warning: Could not find matching sockets for link between '{original_from_node.name}' and '{original_to_node.name}' (Sockets: '{from_socket_name}', '{to_socket_name}')")
|
|
except Exception as e:
|
|
print(f" Error creating link between copied nodes '{new_from_node.name}' and '{new_to_node.name}': {e}")
|
|
|
|
|
|
if not copied_final_bsdf_node:
|
|
print(f" Error: Failed to find the copied version of the final BSDF node for '{source_mat.name}'.")
|
|
return None, None, None
|
|
if not copied_final_disp_node:
|
|
print(f" Error: Failed to find the copied version of the final Displacement node for '{source_mat.name}'.")
|
|
return None, None, None
|
|
|
|
print(f" Finished copying '{source_mat.name}'.")
|
|
return copied_node_map, copied_final_bsdf_node, copied_final_disp_node
|
|
|
|
|
|
class MATERIAL_OT_merge_materials(Operator):
|
|
"""Merge two selected Asset Processor materials"""
|
|
bl_idname = "material.merge_materials"
|
|
bl_label = "Merge Selected Materials"
|
|
bl_options = {'REGISTER', 'UNDO'}
|
|
|
|
# Properties to hold the names of the selected materials
|
|
# These will be set by the UI panel
|
|
material_a_name: StringProperty(
|
|
name="Material A",
|
|
description="First material to merge"
|
|
)
|
|
material_b_name: StringProperty(
|
|
name="Material B",
|
|
description="Second material to merge"
|
|
)
|
|
|
|
def execute(self, context):
|
|
mat_a = bpy.data.materials.get(self.material_a_name)
|
|
mat_b = bpy.data.materials.get(self.material_b_name)
|
|
|
|
if not mat_a or not mat_b:
|
|
self.report({'ERROR'}, "Please select two valid materials to merge.")
|
|
return {'CANCELLED'}
|
|
|
|
if mat_a == mat_b:
|
|
self.report({'ERROR'}, "Cannot merge a material with itself.")
|
|
return {'CANCELLED'}
|
|
|
|
# --- Core Merging Logic (Based on Plan) ---
|
|
|
|
# 1. Create new material
|
|
new_mat_name = f"MAT_Merged_{mat_a.name}_{mat_b.name}"
|
|
if new_mat_name in bpy.data.materials:
|
|
# Handle potential naming conflicts, maybe append a number
|
|
new_mat_name = f"{new_mat_name}.001" # Simple increment for now
|
|
# A more robust approach would check for existing names and find the next available number
|
|
# For prototype, this simple approach is acceptable.
|
|
|
|
new_mat = bpy.data.materials.new(name=new_mat_name)
|
|
new_mat.use_nodes = True
|
|
new_node_tree = new_mat.node_tree
|
|
|
|
# Clear default nodes (Principled BSDF and Material Output)
|
|
for node in new_node_tree.nodes:
|
|
new_node_tree.nodes.remove(node)
|
|
|
|
# Add Material Output node
|
|
output_node = new_node_tree.nodes.new(type='ShaderNodeOutputMaterial')
|
|
output_node.location = (400, 0) # Basic positioning
|
|
|
|
# 2. Copy nodes from source materials
|
|
print("Copying nodes for Material A...")
|
|
copied_map_a, copied_bsdf_a, copied_disp_a = copy_material_nodes(mat_a, new_node_tree, location_offset=(0, 0))
|
|
if not copied_bsdf_a or not copied_disp_a:
|
|
self.report({'ERROR'}, f"Failed to copy nodes or identify outputs for material '{mat_a.name}'. Check console for details.")
|
|
bpy.data.materials.remove(new_mat) # Clean up
|
|
return {'CANCELLED'}
|
|
|
|
print("Copying nodes for Material B...")
|
|
# Calculate offset for Material B based on Material A's nodes (simple approach)
|
|
offset_x = 0
|
|
if copied_map_a:
|
|
max_x = max((n.location.x + n.width for n in copied_map_a.values()), default=0)
|
|
min_x = min((n.location.x for n in copied_map_a.values()), default=0)
|
|
offset_x = max_x - min_x + 100 # Add some spacing
|
|
|
|
copied_map_b, copied_bsdf_b, copied_disp_b = copy_material_nodes(mat_b, new_node_tree, location_offset=(offset_x, 0))
|
|
if not copied_bsdf_b or not copied_disp_b:
|
|
self.report({'ERROR'}, f"Failed to copy nodes or identify outputs for material '{mat_b.name}'. Check console for details.")
|
|
bpy.data.materials.remove(new_mat) # Clean up
|
|
return {'CANCELLED'}
|
|
|
|
|
|
# 3. Link/Append MaterialMerge node group
|
|
merge_node = None
|
|
if not UTILITY_NODEGROUPS_FILE.is_file():
|
|
self.report({'ERROR'}, f"Utility nodegroups file not found: {UTILITY_NODEGROUPS_FILE}")
|
|
# TODO: Clean up newly created material if there's an error
|
|
return {'CANCELLED'}
|
|
|
|
# Check if the group is already in the current file
|
|
merge_group = bpy.data.node_groups.get(MATERIAL_MERGE_NODEGROUP_NAME)
|
|
|
|
if not merge_group:
|
|
# Attempt to link the node group
|
|
try:
|
|
with bpy.data.libraries.load(str(UTILITY_NODEGROUPS_FILE), link=True) as (data_from, data_to):
|
|
if MATERIAL_MERGE_NODEGROUP_NAME in data_from.node_groups:
|
|
data_to.node_groups = [MATERIAL_MERGE_NODEGROUP_NAME]
|
|
else:
|
|
self.report({'ERROR'}, f"Node group '{MATERIAL_MERGE_NODEGROUP_NAME}' not found in '{UTILITY_NODEGROUPS_FILE.name}'.")
|
|
# TODO: Clean up newly created material if there's an error
|
|
return {'CANCELLED'}
|
|
|
|
merge_group = bpy.data.node_groups.get(MATERIAL_MERGE_NODEGROUP_NAME)
|
|
if not merge_group:
|
|
self.report({'ERROR'}, f"Failed to link node group '{MATERIAL_MERGE_NODEGROUP_NAME}'.")
|
|
# TODO: Clean up newly created material if there's an error
|
|
return {'CANCELLED'}
|
|
|
|
except Exception as e:
|
|
self.report({'ERROR'}, f"Error linking '{MATERIAL_MERGE_NODEGROUP_NAME}' from '{UTILITY_NODEGROUPS_FILE.name}': {e}")
|
|
# TODO: Clean up newly created material if there's an error
|
|
return {'CANCELLED'}
|
|
|
|
# Add the linked/appended group to the new material's node tree
|
|
merge_node = new_node_tree.nodes.new(type='ShaderNodeGroup')
|
|
merge_node.node_tree = merge_group
|
|
merge_node.label = MATERIAL_MERGE_NODEGROUP_NAME # Set label for clarity
|
|
merge_node.location = (200, 0) # Basic positioning
|
|
|
|
|
|
# 4. Make Connections
|
|
links = new_node_tree.links
|
|
|
|
# Connect BSDFs to Merge node
|
|
# NOTE: Using original nodes here as placeholder. Needs to use *copied* nodes.
|
|
# NOTE: Using *copied* nodes now.
|
|
# Ensure the sockets exist before linking
|
|
bsdf_output_socket_a = copied_bsdf_a.outputs.get('BSDF')
|
|
shader_input_socket_a = merge_node.inputs.get('Shader A')
|
|
bsdf_output_socket_b = copied_bsdf_b.outputs.get('BSDF')
|
|
shader_input_socket_b = merge_node.inputs.get('Shader B')
|
|
|
|
if not all([bsdf_output_socket_a, shader_input_socket_a, bsdf_output_socket_b, shader_input_socket_b]):
|
|
self.report({'ERROR'}, "Could not find required BSDF/Shader sockets for linking.")
|
|
bpy.data.materials.remove(new_mat) # Clean up
|
|
return {'CANCELLED'}
|
|
|
|
link_bsdf_a = links.new(bsdf_output_socket_a, shader_input_socket_a)
|
|
link_bsdf_b = links.new(bsdf_output_socket_b, shader_input_socket_b)
|
|
|
|
# Connect Displacements to Merge node
|
|
# NOTE: Using original nodes here as placeholder. Needs to use *copied* nodes.
|
|
# NOTE: Using *copied* nodes now.
|
|
# Ensure the sockets exist before linking
|
|
disp_output_socket_a = copied_disp_a.outputs.get('Displacement')
|
|
disp_input_socket_a = merge_node.inputs.get('Displacement A')
|
|
disp_output_socket_b = copied_disp_b.outputs.get('Displacement')
|
|
disp_input_socket_b = merge_node.inputs.get('Displacement B')
|
|
|
|
if not all([disp_output_socket_a, disp_input_socket_a, disp_output_socket_b, disp_input_socket_b]):
|
|
self.report({'ERROR'}, "Could not find required Displacement sockets for linking.")
|
|
bpy.data.materials.remove(new_mat) # Clean up
|
|
return {'CANCELLED'}
|
|
|
|
link_disp_a = links.new(disp_output_socket_a, disp_input_socket_a)
|
|
link_disp_b = links.new(disp_output_socket_b, disp_input_socket_b)
|
|
|
|
# Connect Merge node outputs to Material Output
|
|
# Ensure the sockets exist before linking
|
|
merge_bsdf_output = merge_node.outputs.get('BSDF')
|
|
output_surface_input = output_node.inputs.get('Surface')
|
|
merge_disp_output = merge_node.outputs.get('Displacement')
|
|
output_disp_input = output_node.inputs.get('Displacement')
|
|
|
|
if not all([merge_bsdf_output, output_surface_input, merge_disp_output, output_disp_input]):
|
|
self.report({'ERROR'}, "Could not find required Merge/Output sockets for linking.")
|
|
bpy.data.materials.remove(new_mat) # Clean up
|
|
return {'CANCELLED'}
|
|
|
|
link_merge_bsdf = links.new(merge_bsdf_output, output_surface_input)
|
|
link_merge_disp = links.new(merge_disp_output, output_disp_input)
|
|
|
|
|
|
# 5. Layout (Optional)
|
|
# TODO: Implement better node layout
|
|
|
|
# Update node tree to apply changes
|
|
new_node_tree.nodes.update()
|
|
|
|
self.report({'INFO'}, f"Successfully merged '{mat_a.name}' and '{mat_b.name}' into '{new_mat.name}'")
|
|
|
|
return {'FINISHED'}
|
|
|
|
# Optional: Add invoke method if needed for more complex setup before execute
|
|
# Commented-out code moved to Deprecated/Old-Code/blender_addon_material_merger_operator_py_invoke_method_line_326.py
|
|
|
|
def register():
|
|
bpy.utils.register_class(MATERIAL_OT_merge_materials)
|
|
print("MATERIAL_OT_merge_materials registered")
|
|
|
|
def unregister():
|
|
bpy.utils.unregister_class(MATERIAL_OT_merge_materials)
|
|
print("MATERIAL_OT_merge_materials unregistered")
|
|
|
|
if __name__ == "__main__":
|
|
# This block is for running the script directly in Blender's text editor
|
|
# It's useful for testing the operator logic without installing the addon
|
|
register()
|
|
|
|
# Example usage (replace with actual material names in your file)
|
|
# bpy.ops.material.merge_materials(material_a_name="MAT_Wood01", material_b_name="MAT_SandBeach01") |