0
$\begingroup$

As you can see, there are lots of materials in my obj file. And 90 % of material slots are from duplication of mesh which generates material duplication with names of 001 002 003.. etc. So all materials with the same name(ex) material_01.006, material_01.001) are actually the same materials with the same textures but only have each different meshes.

What I wanna do is merge all these same texture materials and eliminate duplicate material and their slots. Is there any way to do that automatically? I can do it manually, but it takes too much time to do.

screenshot

More specifically, I'd like to merge two different material slots which have each different faces but have references to the same textures. In my case, every time I separate selected faces from the object, it duplicates material with the name added 001,002,003.. like this.

Let's say there are material slots A, B, and A.001. A and A.001 material slots have different faces but both assigned materials use the same texture.

In this case, if I change A.001 material slot's material from A.001 to A, then material slots are like this - A, B, A. And if I eliminate the third material slot - (which I assigned its material from A.001 to A), B will take all its faces, and not the first A material has all A and A.001 faces.

$\endgroup$
6
  • $\begingroup$ There was a similar question recently. Does this help you? blender.stackexchange.com/q/277488/107598 $\endgroup$
    – Blunder
    Commented Oct 31, 2022 at 11:50
  • $\begingroup$ @Blunder Hello. Thank you for your comment but it didnt work for me. My issue is not related with renaming, it is related with different materials removal that have reference of same texture files $\endgroup$
    – muuute
    Commented Nov 1, 2022 at 0:31
  • $\begingroup$ @Blunder More specifically, I'd like to merge two different material slots which have each different faces but have reference of same textures. In my case, everytime i seperate selected faces from object, it duplicate material with name added 001,002,003.. like this. Let's say there are material slots A , B and A.001. A and A.001 material slots have different faces but both assigned material use same texture. $\endgroup$
    – muuute
    Commented Nov 1, 2022 at 1:54
  • $\begingroup$ @Blunder In this case, if i change A.001 material slot's material from A.001 to A, then material slots are like this - A, B, A. And if i eliminate third material slot - (which I assigned its material from A.001 to A), B will take all its faces not first A material has all A and A.001 faces. $\endgroup$
    – muuute
    Commented Nov 1, 2022 at 1:54
  • $\begingroup$ Thanks for the example and clarification. I've added this info to your question so it gets not lost. The script of the linked question discards materials that use the same image texture and renames them (can be turned off) - but it does not clean up slots that have assigned the same material (A, B, A -> A, B). Sorry, I missed that requirement. This makes it a bit more complicated because you need to assign the vertices a "new" material (the base material, source of all duplicates). I haven't scripted that yet. $\endgroup$
    – Blunder
    Commented Nov 1, 2022 at 2:35

1 Answer 1

1
$\begingroup$

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

  • "wood", "metal"

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()')

$\endgroup$
1
  • $\begingroup$ I just tried with blender 3.2. Your code reduced duplicated materials, but order of material slots gone wrong. But I think it is not problem of your code, but problem of my materials cause there are too many materials. anyway thank you. I really appreciate your kindness $\endgroup$
    – muuute
    Commented Nov 2, 2022 at 2:07

You must log in to answer this question.

Not the answer you're looking for? Browse other questions tagged .