The following script removes empty material slots, duplicated materials, and material copies from the selected mesh objects. For example an object with the following material slots
- "wood.001", "metal", empty slot, "metal", "wood", "metal.004", "wood"
will get the following materials assigned
A material is considered a copy if it has a period and number appended to its name, e.g. wood.002 or concrete.034. The script does not check if the materials are actually the same, i.e. if they have the same node tree and the same settings for the nodes. It only checks the name. If there is no "base material" that has no number in the name then the first copy will be used to replace all other copies. Example: wood.005, wood.001, wood.003 will be merged into wood.005. While wood.005, wood, wood.003 will become simply wood.
Faces that have been assigned a material to be removed will receive the new base material.
Merged and now unused materials will be removed from the object's material slots.
Note: Removing slots is an expensive, time-consuming operation (because of the view_layer.update() in bpy.ops). If you have hundreds of objects with many materials to be removed then the script will run for several minutes. Check the output in the System Console.
Requires Blender 3.2 because of the new API context_temp_override. For older versions use line 74 (remove '#') and comment out lines 70 to 73 (add '#').
Don't forget to make a backup of your blend file before you run the script!
# clean material slots
import bpy
import re
import cProfile
# 'wood.002' -> True
def ends_with_dot_and_3_digits(name):
return bool(re.search(r'\.\d\d\d$', name))
# 'wood.002' -> 'wood'
def basename(name):
return name.rsplit('.', 1)[0]
def faces_assigned_to_materials(obj):
# Initialize dictionary of all materials applied to object with empty lists
# which will contain indices of faces on which these materials are applied
material_polys = { slot.material.name : [] for slot in obj.material_slots if not slot.material is None}
for idx, face in enumerate( obj.data.polygons ):
material_polys[ obj.material_slots[ face.material_index ].name ].append( idx )
return material_polys
def clean_materials():
print("--- start --")
# get names of all materials that have NO number suffix (such as wood.000, metal.001, glass.002, etc)
#base_material_names = { mat.name for mat in bpy.data.materials if not ends_with_dot_and_3_digits(mat.name) }
# remove material copies and clean up the material slots of all objects
for obj in bpy.context.selected_objects:
if obj.type != 'MESH':
continue
material_slot_indices = {}
material_polys = faces_assigned_to_materials(obj)
slots_to_remove = []
for sidx, slot in enumerate(obj.material_slots):
# skip empty slots and mark them to be removed
if slot.material is None:
slots_to_remove.append(sidx)
continue
# find base material and its slot (if precessed already)
mat_basename = basename(slot.name)
base_slot_idx = material_slot_indices.get(mat_basename)
if base_slot_idx is None:
# found a new material in the current slot
material_slot_indices[mat_basename] = sidx
new_mat = bpy.data.materials.get(mat_basename)
if new_mat:
slot.material = new_mat
print(f'INFO: object {obj.name}, slot {sidx}: replaced "{slot.material.name}" with "{new_mat.name}"')
else:
print(f'WARN: object {obj.name}, slot {sidx}:: cannot find base material for "{slot.name}"')
else:
# found a copy material. Assign the base material slot (slot_idx) to the faces to replace the materal slot
face_indices = material_polys[slot.name]
for fidx in face_indices:
obj.data.polygons[fidx].material_index = base_slot_idx
print(f'INFO: object {obj.name}, slot {sidx}:: assigned {len(face_indices)} faces of slot "{slot.name}" (idx:{sidx}) to base slot index {base_slot_idx}')
slots_to_remove.append(sidx)
print(f'INFO: object {obj.name}, slot {sidx}: this slot with material "{slot.material.name}" will be removed')
# remove material slots that are now empty for the current object
for sidx in slots_to_remove:
obj.active_material_index = sidx
override = bpy.context.copy()
override["object"] = obj
with bpy.context.temp_override(**override): # Blender 3.2+
bpy.ops.object.material_slot_remove()
#bpy.ops.object.material_slot_remove({'object': obj}) # Blender 3.1
print("--- done --")
cProfile.run('clean_materials()')