Removing Specific Material Slots

I’m writing an operator to remove specific material slots that have “MC_” in the material slot’s name. However, I can’t figure out how to query the slot’s index to make it active so it can be removed. Hope that makes sense…see the code below:

class OBJECT_OT_matClear(bpy.types.Operator):
    bl_idname = "object.clear_mats"
    bl_label = "Clear Created"
    bl_description = "Clear created materials"
    
    def execute(self, context):
        allOb = bpy.data.objects
        mats = bpy.data.materials
        scn = bpy.context.scene
        game = bpy.types.SceneGameData
        view = bpy.types.SpaceView3D
        selMatCap = scn.mat_matcap
        
        for ob in allOb:
            for slot in ob.material_slots:
                if ("MC_" in slot.name):
                    print (slot)

                    #this removes all slots
                    for i in range(len(ob.material_slots)):
                        ob.active_material_index = i - 1
                        bpy.ops.object.material_slot_remove()

Any help is much appreciated!

here’s a script i wrote to remove unused materials from object mat slots:


import bpy

def main():
    if len(bpy.data.objects) < 1:
        print("No objects")
        return

    if bpy.context.object is None:
        print("No context, new active: %s" % bpy.data.objects[0].name)
        bpy.context.scene.objects.active = bpy.data.objects[0]


    bpy.ops.object.mode_set(mode='OBJECT', toggle=False)

    for ob in bpy.data.objects:
           if ob.type != 'MESH' or not ob.select or ob.hide:
               continue

           for i, l in enumerate(bpy.context.scene.layers):
               if ob.layers[i] and ob.layers[i] == l:
                   break
           else:
               continue

           mat_slots = {}
           for p in ob.data.polygons:
               mat_slots[p.material_index] = 1

           mat_slots = mat_slots.keys()

           for i in reversed(range(len(ob.material_slots))):
               if i not in mat_slots:
                   bpy.context.scene.objects.active = ob
                   ob.active_material_index = i
                   bpy.ops.object.material_slot_remove()
main() 

bpy.context.scene.objects.active = ob makes ob the active object and material_slot_remove() will use it

Note: the for loop through material slots has to go from last to first, it wouldn’t work the other way around as the slot numbers change when removing a material (you could keep track of it with an extra variable to access subsequent slots correctly afterwards, but reversing the iteration is easier)

Awesome - thanks CoDEmanX! I can’t say I completely understand what your script is doing at the moment, but I’ll be studying it while I’m at work

It’s best to not use bpy.ops for this kind of stuff call object.data.materials.pop(index) on the ones you want to get rid of.

@Uncle Entity: Admittedly I’m not the most experienced developer…can you elaborate on what you mean by calling object.data.materials.pop(index) on the ones I want to get rid of?

pop(index) gets rid of the material but the material slots stay.

yep, the intention for my script was to clean up material slots. I did not know about pop(), but i wouldn’t use it anyway as it just sets the material to None = slots stay unclean

Would setting the “update_data” option to True do the trick: http://www.blender.org/documentation/blender_python_api_2_63_release/bpy.types.IDMaterials.html?highlight=materials.pop#bpy.types.IDMaterials.pop ?

Yes, it works. :slight_smile:

Nevertheless, with whatever little experience that I have, CodemanX’s version was easier to implement in this script of mine. It removes beautifully the spurious materials left after a Separate > By loose parts. See the function removeSpuriousMaterials() at the top. It’s CodemanX’s in disguise.

I commented out the first few lines because separateObjects() already takes care of setting up the context.

The script is just a way to edit many objects at the same time and retrieve them afterward with their names, materials and vertex_groups intact. Forgive this user his pretense at coding: I know next to nothing about it.



import bpy

def removeSpuriousMaterials():
#     if len(bpy.data.objects) < 1:
#         print("No objects")
#         return
#
#     if bpy.context.object is None:
#         print("No context, new active: %s" % bpy.data.objects[0].name)
#         bpy.context.scene.objects.active = bpy.data.objects[0]
#

     #bpy.ops.object.mode_set(mode='OBJECT', toggle=False)

     for ob in bpy.data.objects:
            if ob.type != 'MESH' or not ob.select or ob.hide:
                continue

            for i, l in enumerate(bpy.context.scene.layers):
                if ob.layers[i] and ob.layers[i] == l:
                    break
            else:
                continue

            mat_slots = {}
            for p in ob.data.polygons:
                mat_slots[p.material_index] = 1

            mat_slots = mat_slots.keys()

            for i in reversed(range(len(ob.material_slots))):
                if i not in mat_slots:
                    bpy.context.scene.objects.active = ob
                    ob.active_material_index = i
                    bpy.ops.object.material_slot_remove()
                    

def filterForMeshes():

    selectedMeshes=[]    
    #make a list of all that is selected
    selection=bpy.context.selected_objects

    #test each object to see if they are meshes
    for i in range(0,len(selection)):
        #if they are, append to selectedMeshes
        if selection[i].type == 'MESH':
            selectedMeshes.append(selection[i])

    return selectedMeshes

def makeVGs(selectedMeshes):
    #for each of the selected meshes in turn
    for i in range(0,len(selectedMeshes)):
        # make it active
        bpy.context.scene.objects.active=selectedMeshes[i]
        # go into edit mode
        bpy.ops.object.mode_set(mode='EDIT')
        vg=bpy.context.object.vertex_groups
        # add the name of the object as a prefix to existing vertex groups
        for n in range(0,len(vg)):
            vg[n].name= selectedMeshes[i].name +"_"+ vg[n].name
        # select all vertices
        bpy.ops.mesh.select_all(action='SELECT')
        # create a new vertex group with the 'All_' prefix
        bpy.context.object.vertex_groups.new('All_'+selectedMeshes[i].name)
        # make that group active
        bpy.ops.object.vertex_group_set_active(group='All_'+selectedMeshes[i].name)
        # assign the selected vertices to the new vg
        bpy.ops.object.vertex_group_assign()
        bpy.ops.object.mode_set(mode='OBJECT')
        

def separateObjects():
    # be sure to be in edit mode
    bpy.ops.object.mode_set(mode='EDIT')
    # deselect all vertices
    bpy.ops.mesh.select_all(action='DESELECT')
    
    # To separate each object:
    
    # find all vertex groups
    VGs=list(bpy.context.active_object.vertex_groups)
    
    # find VGs which start by 'All_'
    all=[]
    for vg in range(0,len(VGs)):
        vgName=VGs[vg].name
        if vgName[0:4]=='All_':
            all.append(VGs[vg])
            
    # Select each 'All_' VGs, separate, name, repeat
    
    # take note of all the objects in the scene:
    objects=list(bpy.data.objects)
    for vg in range(0,len(all)):
        bpy.ops.object.vertex_group_set_active(group=all[vg].name)
        # select its assigned vertices.
        bpy.ops.object.vertex_group_select()
        # complete selection in case object was edited
        bpy.ops.mesh.select_linked(limit=False)
        # separate the object
        bpy.ops.mesh.separate(type='SELECTED')
        # in Object mode...
        bpy.ops.object.mode_set(mode='OBJECT')
        # ... find the new object
        objectsPlusOne=list(bpy.data.objects)
        diff_list = [item for item in objectsPlusOne if not item in objects]
        # name new object
        diff_list[0].name=all[vg].name[4:]
        # take note of active object
        tempActive=bpy.context.scene.objects.active
        # set new object as active
        bpy.context.scene.objects.active = diff_list[0]
        # gather VGs of new object in a list
        vgInNewObject=list(bpy.context.active_object.vertex_groups)
        # remove all vgs not related to the object
        for i in range(0,len(vgInNewObject)):
            if diff_list[0].name not in vgInNewObject[i].name:
                bpy.ops.object.vertex_group_set_active(group=vgInNewObject[i].name)
                bpy.ops.object.vertex_group_remove(all=False)
            elif vgInNewObject[i].name[0:4] == 'All_':
                bpy.ops.object.vertex_group_set_active(group=vgInNewObject[i].name)
                bpy.ops.object.vertex_group_remove(all=False)                
        # make the rest of the joined object active
        bpy.context.scene.objects.active=tempActive
        # set 'objects' to 'ObjectsPlusOne
        objects = objectsPlusOne
        # back in Edit mode
        bpy.ops.object.mode_set(mode='EDIT')

    return


 
class AFO_OFAPanel(bpy.types.Panel): 
    bl_space_type = "VIEW_3D"
    bl_region_type = "TOOLS"
    bl_context = "objectmode"
    bl_label = "ManyMusketeers"
 
    def draw(self, context) :
        TheRow = self.layout.row(align = True)
        TheRow.operator("mesh.object_multi_edit", text = "All For One")
        TheRow2 = self.layout.column(align = True)
        TheRow2.operator("mesh.object_multi_separate", text = "One For All")
    #end draw
 
#end OFA_AFO_MakerPanel
 
class MeshObjectMultiEdit(bpy.types.Operator) :
    bl_idname = "mesh.object_multi_edit"
    bl_label = "Edit Multi"
    bl_options = {"UNDO"}
     
    def invoke(self, context, event) :
        meshesToEdit=filterForMeshes()
        makeVGs(meshesToEdit)
        try:
            bpy.ops.object.join()
        except RuntimeError:
            print("There is no mesh selected")
        try:
            bpy.context.selected_objects[0].name=bpy.context.selected_objects[0].users_group[0].name+"_AOF"
        except:
            self.report({'INFO'}, "Object could not be renamed: not part of a group")
        return {"FINISHED"}
    #end invoke
 
class MeshObjectSeparateMulti(bpy.types.Operator) :
    bl_idname = "mesh.object_multi_separate"
    bl_label = "Multi Separate"
    bl_options = {"UNDO"}
     
    def invoke(self, context, event) :
        separateObjects()
        bpy.ops.object.mode_set(mode='OBJECT')
        bpy.ops.object.select_all(action='DESELECT')
        bpy.context.active_object.select=True
        bpy.ops.object.delete()
        bpy.ops.object.select_all(action='SELECT')
        bpy.ops.object.origin_set(type='ORIGIN_CURSOR')
        removeSpuriousMaterials()
        return {"FINISHED"}
    #end invoke
 
#end

bpy.utils.register_class(MeshObjectSeparateMulti)

bpy.utils.register_class(MeshObjectMultiEdit)

bpy.utils.register_class(AFO_OFAPanel)


Good find Traveller!

Removing all materials becomes this easy:

>>> mats = bpy.context.object.data.materials
>>> while mats:
...     mats.pop(0, True)

about my code to remove unused materials:

>>> if ob.type != ‘MESH’ or not ob.select or ob.hide:
you only need to check for mesh type, leave out the other two checks if you wanna remove materials even from unselected and hidden meshes

           for i, l in enumerate(bpy.context.scene.layers):
               if ob.layers[i] and ob.layers[i] == l:
                   break
           else:
               continue

this makes it remove materials from meshes currently visible (selected layers), leave it out if you want

thanks guys for the help! I’m not exactly looking to remove unused material slots, but instead to remove material slots associated with a specific material. Like, I can easily find the slot’s name but can’t find the association of the slot name to the material index. Anyhoo, I’m thinking the answer probably lies in the code snippets you’ve posted so that’s what I’ll continue to study - Thanks again!

Easy :slight_smile:
Removes None and materials with “.00” in name:


import bpy

def main():
    if len(bpy.data.objects) < 1:
        print("No objects")
        return

    # Changing material_index requires object mode
    if bpy.context.object is None:
        print("No context, new active: %s" % bpy.data.objects[0].name)
        bpy.context.scene.objects.active = bpy.data.objects[0]

    bpy.ops.object.mode_set(mode='OBJECT', toggle=False)

    for ob in bpy.data.objects:
        if ob.type != 'MESH':
            continue
        
        for i, mat in reversed(list(enumerate(ob.data.materials))):
            if mat is None or ".00" in mat.name:
                ob.data.materials.pop(i, update_data=True)
                
        # Fix material assignments, indices do not update after popping!
        for p in ob.data.polygons:
            if p.material_index >= len(ob.data.materials):
                p.material_index = -1
                
    # Hack: Force update of material slot view
    bpy.context.scene.frame_set(bpy.context.scene.frame_current)
    
main()

note that material_index is kept even if the material with this index was removed. I added a simple check for valid indices, but for some reason some faces may occur black until edit mode entered…

perhaps, the operator is the better choice in the end ?!

The material_index is used in other places in blender so if you have a fairly complex setup you might tend to break things if you just willy-nilly move around the materials.

Also, if you find a case where you change something in python and the UI doesn’t update then this is a bug – was meaning to look into the materials.pop() code but got distracted by other things yesterday. Actually thought I fixed that before but apparently not…

Brilliant! Thanks everybody - you guys RAWK :ba: