Files
ProjectStructure_HOME/replace_cel_with_bsdf.py
2025-09-09 15:47:16 -06:00

375 lines
16 KiB
Python

import bpy
import re
import os
def link_bsdf_materials():
"""Link all materials from the BSDF library file"""
library_path = r"A:\1 Amazon_Active_Projects\1 BlenderAssets\Amazon\MATERIALS_BSDF_pallette_v1.0.blend"
if not os.path.exists(library_path):
print(f"Warning: Library file not found at {library_path}")
return []
print(f"Linking materials from: {library_path}")
# Get list of materials before linking
materials_before = set(bpy.data.materials.keys())
# Link all materials from the library file
with bpy.data.libraries.load(library_path, link=True) as (data_from, data_to):
# Link all materials
data_to.materials = data_from.materials
# Get list of newly linked materials
materials_after = set(bpy.data.materials.keys())
newly_linked = materials_after - materials_before
print(f"Linked {len(newly_linked)} materials from library")
for mat_name in sorted(newly_linked):
print(f" - {mat_name}")
return list(newly_linked)
def remap_appended_to_linked():
"""Remap any appended BSDF materials to their linked counterparts"""
print("\nChecking for appended BSDF materials to remap to linked versions...")
materials = bpy.data.materials
remapping_count = 0
# Group materials by base name (without library suffix)
material_groups = {}
for mat in materials:
# Check if it's a BSDF material (from any source)
if mat.name.startswith("BSDF_") or "BSDF_" in mat.name:
# Extract base name (remove library reference if present)
base_name = mat.name.split(".blend")[0] if ".blend" in mat.name else mat.name
base_name = base_name.split(".")[0] if "." in base_name else base_name
if base_name not in material_groups:
material_groups[base_name] = []
material_groups[base_name].append(mat)
# For each group, prefer linked materials over appended ones
for base_name, mats in material_groups.items():
if len(mats) > 1:
# Sort to prefer linked materials (they have library references)
linked_mats = [m for m in mats if m.library is not None]
appended_mats = [m for m in mats if m.library is None]
if linked_mats and appended_mats:
# Use the first linked material as target
target_material = linked_mats[0]
# Remap all appended materials to the linked one
for appended_mat in appended_mats:
if appended_mat.users > 0:
print(f"Remapping appended {appended_mat.name} ({appended_mat.users} users) to linked {target_material.name}")
appended_mat.user_remap(target_material)
remapping_count += 1
# Remove the unused appended material
if appended_mat.users == 0:
print(f"Removing unused appended material: {appended_mat.name}")
bpy.data.materials.remove(appended_mat)
# Also check for any BSDF materials that might be from old paths or different files
# and try to find matching linked materials
for mat in materials:
if mat.library is None and (mat.name.startswith("BSDF_") or "BSDF_" in mat.name):
# This is an appended BSDF material - look for a linked version
base_name = mat.name.split(".blend")[0] if ".blend" in mat.name else mat.name
base_name = base_name.split(".")[0] if "." in base_name else base_name
# Look for any linked material with the same base name
for linked_mat in materials:
if (linked_mat.library is not None and
linked_mat.name.startswith("BSDF_") and
(linked_mat.name == base_name or
linked_mat.name.startswith(base_name + ".") or
linked_mat.name == mat.name)):
if mat.users > 0:
print(f"Remapping old BSDF {mat.name} ({mat.users} users) to linked {linked_mat.name}")
mat.user_remap(linked_mat)
remapping_count += 1
# Remove the unused material
if mat.users == 0:
print(f"Removing unused old BSDF material: {mat.name}")
bpy.data.materials.remove(mat)
break
print(f"Remapped {remapping_count} appended/old BSDF materials to linked versions")
return remapping_count
def remap_missing_datablocks():
"""Remap materials that have missing/broken library links"""
print("\nChecking for missing datablocks to remap...")
materials = bpy.data.materials
remapping_count = 0
# Find materials with missing library links
missing_materials = []
for mat in materials:
if mat.library is not None and mat.library.filepath and not os.path.exists(bpy.path.abspath(mat.library.filepath)):
missing_materials.append(mat)
print(f"Found missing datablock: {mat.name} (from {mat.library.filepath})")
if not missing_materials:
print("No missing datablocks found.")
return 0
# For each missing material, try to find a replacement
for missing_mat in missing_materials:
base_name = missing_mat.name.split(".blend")[0] if ".blend" in missing_mat.name else missing_mat.name
base_name = base_name.split(".")[0] if "." in base_name else base_name
# Look for a replacement material
replacement_found = False
# First, try to find a linked material with the same name from the current library
for mat in materials:
if (mat.library is not None and
mat.library.filepath and
os.path.exists(bpy.path.abspath(mat.library.filepath)) and
mat.name == missing_mat.name):
if missing_mat.users > 0:
print(f"Remapping missing {missing_mat.name} ({missing_mat.users} users) to valid linked {mat.name}")
missing_mat.user_remap(mat)
remapping_count += 1
replacement_found = True
break
# If no exact match, try to find a BSDF material with similar name
if not replacement_found and (missing_mat.name.startswith("BSDF_") or "BSDF_" in missing_mat.name):
for mat in materials:
if (mat.library is not None and
mat.library.filepath and
os.path.exists(bpy.path.abspath(mat.library.filepath)) and
mat.name.startswith("BSDF_") and
(mat.name == base_name or
mat.name.startswith(base_name + ".") or
base_name in mat.name)):
if missing_mat.users > 0:
print(f"Remapping missing BSDF {missing_mat.name} ({missing_mat.users} users) to valid linked {mat.name}")
missing_mat.user_remap(mat)
remapping_count += 1
replacement_found = True
break
# If still no replacement, try to find any valid linked material with the same base name
if not replacement_found:
for mat in materials:
if (mat.library is not None and
mat.library.filepath and
os.path.exists(bpy.path.abspath(mat.library.filepath)) and
mat.name == base_name):
if missing_mat.users > 0:
print(f"Remapping missing {missing_mat.name} ({missing_mat.users} users) to valid linked {mat.name}")
missing_mat.user_remap(mat)
remapping_count += 1
replacement_found = True
break
if not replacement_found:
print(f"Warning: No replacement found for missing material {missing_mat.name}")
print(f"Remapped {remapping_count} missing datablocks to valid linked materials")
return remapping_count
def replace_cel_materials():
"""Replace all CEL materials with their BSDF counterparts using Blender's user remapping"""
# First, link BSDF materials from library
linked_materials = link_bsdf_materials()
# Then, remap any missing datablocks
missing_remaps = remap_missing_datablocks()
# Then, remap any appended BSDF materials to linked versions
appended_remaps = remap_appended_to_linked()
print(f"\n=== STARTING MATERIAL REPLACEMENT ===")
# Custom material mappings (source -> target)
custom_mappings = {
"bag BLACK (squid ink)": "BSDF_black_SQUID-INK",
"bag WHITE": "BSDF_WHITE",
"Wheel-White": "BSDF_WHITE",
"Bag Colors": "BSDF_Bag Colors",
"cardboard": "Package_Cardboard",
"blue (triton)": "BSDF_blue-2_TRITON",
"gray (snow)": "BSDF_gray-6_SNOW",
"gray (storm)": "BSDF_gray-2_STORM",
"gray (summit)": "BSDF_gray-5_SUMMIT",
"light blue (prime)": "BSDF_blue-4_PRIME",
"yellow (summer)": "BSDF_orange-5_SUMMER",
"Accessory_CEL_gray-6_SNOW": "BSDF_gray-6_SNOW",
"Accessory_CEL_SquidInk": "BSDF_black_SQUID-INK",
"FingerScanner": "BSDF_black_SQUID-INK",
"cel BLACK (squid ink)": "BSDF_black_SQUID-INK",
"cel WHITE": "BSDF_WHITE",
"gray (stone)": "BSDF_gray-3_STONE",
"green (oxygen)": "BSDF_green-3_OXYGEN",
"orange (smile)": "BSDF_orange-3_SMILE",
"orange (blaze)": "BSDF_orange-1_BLAZE"
}
# Get all materials in the scene
materials = bpy.data.materials
# Dictionary to store source -> target material mapping
material_mapping = {}
# Replace all CEL materials with their BSDF counterparts, ignoring numeric suffixes
cel_pattern = re.compile(r"^(CEL_.+?)(\.\d{3})?$")
bsdf_pattern = re.compile(r"^(BSDF_.+?)(\.\d{3})?$")
# Build a mapping from base BSDF name to BSDF material (without suffix)
bsdf_base_map = {bsdf_pattern.match(mat.name).group(1): mat for mat in materials if bsdf_pattern.match(mat.name)}
# Build a mapping from exact material names to materials
exact_material_map = {mat.name: mat for mat in materials}
# Helpers to normalize names (case-insensitive, ignore numeric suffixes and library suffix)
def normalize_base(name):
base_name = name.split(".blend")[0] if ".blend" in name else name
match = re.match(r"^(.*?)(\.\d{3})?$", base_name)
base_name = match.group(1) if match else base_name
return base_name.strip().casefold()
# Map normalized base name -> list of materials
materials_by_base = {}
for mat in materials:
base = normalize_base(mat.name)
materials_by_base.setdefault(base, []).append(mat)
# Normalize BSDF base name map for robust target lookups
bsdf_base_map_normalized = {normalize_base(base): mat for base, mat in bsdf_base_map.items()}
replacements_made = 0
missing_targets = []
# Process custom mappings first (case/suffix-insensitive)
for source_name, target_name in custom_mappings.items():
# Gather source candidates by exact name or base name (handles .001 etc.)
if source_name in exact_material_map:
source_candidates = [exact_material_map[source_name]]
else:
source_candidates = materials_by_base.get(normalize_base(source_name), [])
if not source_candidates:
print(f"Warning: Source material '{source_name}' not found")
continue
# Resolve target BSDF by exact or base name
target_material = exact_material_map.get(target_name)
if target_material is None:
target_material = bsdf_base_map_normalized.get(normalize_base(target_name))
if target_material is None:
# Final fallback: any BSDF whose base equals target base
target_base_norm = normalize_base(target_name)
for mat in materials:
m = bsdf_pattern.match(mat.name)
if m and normalize_base(m.group(1)) == target_base_norm:
target_material = mat
break
if target_material is None:
missing_targets.append(f"{source_name} -> {target_name}")
print(f"Warning: Target material '{target_name}' not found for custom mapping '{source_name}'")
continue
for src_mat in source_candidates:
material_mapping[src_mat] = target_material
print(f"Found custom mapping: {src_mat.name} -> {target_material.name}")
# Find all CEL materials and their BSDF counterparts
for mat in materials:
cel_match = cel_pattern.match(mat.name)
if cel_match:
base_cel = cel_match.group(1)
base_bsdf = base_cel.replace("CEL_", "BSDF_", 1)
if base_bsdf in bsdf_base_map:
material_mapping[mat] = bsdf_base_map[base_bsdf]
print(f"Found CEL mapping: {mat.name} -> {bsdf_base_map[base_bsdf].name}")
else:
missing_targets.append(f"{mat.name} -> {base_bsdf}")
print(f"Warning: No BSDF counterpart found for {mat.name}")
# Use Blender's built-in user remapping to replace ALL users
for source_material, target_material in material_mapping.items():
print(f"Remapping all users of {source_material.name} to {target_material.name}")
# Get user count before remapping
users_before = source_material.users
# Remap all users of the source material to the target material
# This catches ALL users including geometry node instances, drivers, etc.
source_material.user_remap(target_material)
# Get user count after remapping
users_after = source_material.users
replacements_this_material = users_before - users_after
replacements_made += replacements_this_material
print(f" Users before: {users_before}, after: {users_after}")
print(f" Remapped {replacements_this_material} users")
# Optional: Remove unused source materials after remapping
print(f"\nCleaning up unused source materials...")
removed_materials = 0
for source_material in material_mapping.keys():
if source_material.users == 0:
print(f"Removing unused material: {source_material.name}")
bpy.data.materials.remove(source_material)
removed_materials += 1
else:
print(f"Warning: {source_material.name} still has {source_material.users} users after remapping")
# Summary
print(f"\n=== REPLACEMENT SUMMARY ===")
print(f"Materials linked from library: {len(linked_materials)}")
print(f"Missing datablocks remapped: {missing_remaps}")
print(f"Appended->Linked remappings: {appended_remaps}")
print(f"Total materials mapped: {len(material_mapping)}")
print(f"Successful mappings: {len(material_mapping)}")
print(f"Total user remappings: {replacements_made}")
print(f"Source materials removed: {removed_materials}")
if missing_targets:
print(f"\nMissing target materials for:")
for mapping in missing_targets:
print(f" - {mapping}")
return len(material_mapping), replacements_made
# Run the replacement
if __name__ == "__main__":
replace_cel_materials()
print("\nRemaining CEL materials in file:")
cel_count = 0
for mat in bpy.data.materials:
if mat.name.startswith("CEL_"):
print(f" {mat.name} ({mat.users} users)")
cel_count += 1
if cel_count == 0:
print(" None - all CEL materials have been replaced!")
print("\nBSDF materials in file:")
for mat in bpy.data.materials:
if mat.name.startswith("BSDF_"):
print(f" {mat.name} ({mat.users} users)")