1
$\begingroup$

Looking for a way to copy materials from one object to another. so I have an object that I imported. a daz character to be specific. I have made shader for all of the materials. This character is now setup and ready to go. Now I have to import him multiple times into blender in different poses etc. so i am looking for a way to apply all the materials I have already made to the new import. Since the new import has the same material names as the old blender appends everything .001 so I now have torso and torso.001 as available materials. So Im looking for a script which will go through all the material slots on the character and set the active material from the .001 to the original one. can anyone help me out here?

$\endgroup$
  • 3
    $\begingroup$ CTRL+L > Materials (provided they already have the same number of material slots) $\endgroup$ – Duarte Farrajota Ramos Mar 23 '18 at 16:52
  • $\begingroup$ That works great thanks! But is there a way to make a script that does it based on material names? Because when I import diff generation characters, the material slots change. They have the same number of slots, and the same material names, but the slots are not in the same order. So there the torso ends up getting applied to the arms. So looking for a script that looks at the material name, and replaces the name.001 with name. $\endgroup$ – steven850 Mar 23 '18 at 17:08
  • 3
    $\begingroup$ Search is your friend blender.stackexchange.com/search?q=replace+material+script $\endgroup$ – Duarte Farrajota Ramos Mar 23 '18 at 17:12
  • $\begingroup$ I have tried everything I could find in the search. Either there from older versions and wont run at all, or they just copy slot to slot, same as Ctrl+L. $\endgroup$ – steven850 Mar 23 '18 at 17:50
1
$\begingroup$

I use this as an add-on to deal with duplicated materials, node groups and textures ending in .001, .002, .003... after copying objects from one file and pasting them to another.

You can save it to .py file and install it as an add-on or just run it in the Text Editor and you will have a menu that you can call with ctrl+alt+shift+z with options to remove duplicate materials. It also removes duplicate textures, and duplicate node groups in materials as well(this can cause trouble if you have different textures with the same name or different node groups with default names, please see the info in the Info Panel after you use the functions and make sure you are OK with the textures and node groups replaced) It also checks for differences in the node trees of the materials so if the materials are different it can leave them alone as it was made to deal with duplicated materials that are the same after copy-pasting objects from one .blend file to another.

bl_info = {
    "name": "MZ Addon",
    "author": "Martynas Žiemys",
    "version": (0, 4),
    "blender": (2, 78, 0),
    "location": "alt+ctrl+z",
    "description": "Operators",
    "warning": "",
    "wiki_url": "",
    "category": "Operators",
    }

import bpy
selection = list()


def materials_same(mat1,mat2):
    #Check if nodes Match
    if mat1.use_nodes and mat2.use_nodes:
        if len(mat1.node_tree.nodes) != len(mat2.node_tree.nodes):
            return False
        for everynode in mat1.node_tree.nodes:
            if not(everynode.name in mat2.node_tree.nodes):
                return False             
        #if nodes match carry on to closer inspection      
        for everynode in mat1.node_tree.nodes:
            for everyinput in everynode.inputs:
                if hasattr(everyinput, 'default_value'):
                    if hasattr(everyinput.default_value, "__len__") :
                        for i in range(len(everyinput.default_value)):
                            if everyinput.default_value[i] != mat2.node_tree.nodes[everynode.name].inputs[everyinput.name].default_value[i]:
                                return False
                    elif everyinput.default_value != mat2.node_tree.nodes[everynode.name].inputs[everyinput.name].default_value:
                        return False
            #lets check links
            n=0
            if len(mat1.node_tree.links) != len(mat2.node_tree.links):
                return False
            else:
                for everylink in mat1.node_tree.links:  
                    if (mat1.node_tree.links[n].from_socket.name != mat2.node_tree.links[n].from_socket.name
                        or mat1.node_tree.links[n].to_node.name != mat2.node_tree.links[n].to_node.name
                        or mat1.node_tree.links[n].to_socket.name != mat2.node_tree.links[n].to_socket.name):
                        return False
                n=n+1 
    if mat1.use_nodes != mat2.use_nodes:
        return False

    print('Material \"' + str(mat1.name) + '\" is the same as \"' + str(mat2.name) + '\"')           
    return True

################ Operators ################################################

class MZRemoveDuplicates(bpy.types.Operator):
    """Remove Duplicate Materials, Textures and Node Groups That End in .001, .002,..."""
    bl_idname = "material.mz_remove_duplicates"
    bl_label = "Remove Duplicate Materials, Textures and Node Groups"
    bl_options = {"REGISTER", "UNDO"}
    ignore_changes = bpy.props.BoolProperty(
        name = "Ignore Material Cahnges",
        description = "Ignore difference in materials and replace all matching names with the first one",
        default = False
    )
    @classmethod
    def poll(cls, context):
        return context.object is not None
    def execute(self, context):
        mats_removed = 0
        mats_unnamed = 0 
        node_grp_removed = 0
        texture_nodes_removed = 0
        for every_object in bpy.data.objects:
            for every_slot in every_object.material_slots:
                part = every_slot.name.rpartition('.')
                if every_slot.name[-3:].isdigit():
                    if not(every_slot.name[:-4] in bpy.data.materials):
                        bpy.data.materials[every_slot.name].name = every_slot.name[:-4]
                        continue
                    if materials_same(bpy.data.materials[every_slot.name], bpy.data.materials[every_slot.name[:-4]]) or self.ignore_changes:
                        every_slot.material = bpy.data.materials.get(every_slot.name[:-4])
                        mats_removed += 1
                    else:
                        every_slot.material.name = str(every_slot.name[:-4]) + ' unnamed!'
                        mats_unnamed += 1
        for mat in bpy.data.materials:
            if mat.node_tree :
                for n in mat.node_tree.nodes:
                    if n.type == 'TEX_IMAGE':
                        if n.image == None:
                            print(mat.name,'has an image node with no image')
                        elif n.image.name[-3:].isdigit():
                            if not(n.image.name[:-4] in bpy.data.images):   # in case there is no more original without numbers
                                  n.image.name = n.image.name[:-4]          # rename the first one
                                  continue                                  # and carry on to the next
                            n.image = bpy.data.images[n.image.name[:-4]]
                            texture_nodes_removed += 1

        for every_node_group in bpy.data.node_groups:
            print(every_node_group.name)
            if every_node_group.name[-3:].isdigit():
                if not(every_node_group.name[:-4] in bpy.data.node_groups): # in case there is no more original without numbers
                    every_node_group.name = every_node_group.name[:-4]      # rename the first one  
                    continue                                                # and carry on to the next
                bpy.data.node_groups[every_node_group.name].user_remap(bpy.data.node_groups[every_node_group.name[:-4]])
                node_grp_removed += 1
        if mats_removed + mats_unnamed + node_grp_removed + texture_nodes_removed == 0:
            self.report({'INFO'}, 'No duplicating materials, node groups or textures found.')
        else:    
            self.report({'INFO'}, str(mats_removed) + ' material(s) replaced. ' + str(mats_unnamed) + ' different material(s) had the same name. ' + str(node_grp_removed) + ' node group(s) replaced. ' + str(texture_nodes_removed) + ' matching texture file(s) replaced.')
        return {'FINISHED'}

################ UI ################################################

class MZ_menu(bpy.types.Menu):
    bl_label = "MZ Menu"
    bl_idname = "MZ_menu"
    def draw(self, context):
        layout = self.layout
        layout.operator("material.mz_remove_duplicates", text = 'Remove Duplicate Materials, Textures and Node Groups').ignore_changes=False 
        layout.operator("material.mz_remove_duplicates", text = "Remove Duplicates - Ignore Material Difference").ignore_changes=True

def draw_item(self, context):
    layout = self.layout
    layout.menu(MZ_menu.bl_idname)

########### REGISTRATION ###############################

def register_MZkeymaps():
    global MZ_keymaps
    MZ_keymaps = []
    new_keymap = bpy.context.window_manager.keyconfigs.addon.keymaps.new(name = "Window",space_type='EMPTY', region_type='WINDOW')
    new_keymap_item = new_keymap.keymap_items.new("wm.call_menu", type = "Z",alt=True, ctrl=True, shift=True, value = "PRESS")
    new_keymap_item.properties.name = "MZ_menu"
    MZ_keymaps.append(new_keymap)

def unregister_MZkeymaps():
    register_MZkeymaps()
    for everykeymap in MZ_keymaps:
        bpy.context.window_manager.keyconfigs.addon.keymaps.remove(everykeymap)
        del MZ_keymaps[:]

def register():
    register_MZkeymaps()
    bpy.utils.register_class(MZRemoveDuplicates)
    bpy.utils.register_class(MZ_menu)



def unregister():
    unregister_MZkeymaps()
    bpy.utils.unregister_class(MZRemoveDuplicates)
    bpy.utils.unregister_class(MZ_menu)


if __name__ == "__main__":
    register()

Sorry about it not following pep8 guides. It's quite messy, but it saves a lot of time for me, so I hope it's useful.

$\endgroup$
  • $\begingroup$ FWIW not a fan of using every as a name for an iterator. A variable named every_object conjures up a collection with every object, like every_object = bpy.data.objects[:]., whereas each iteration the variable every_object is only one object. Wouldn't use for each_object in some_collection: either. Why not something simple like for ob in some_object_collection.? $\endgroup$ – batFINGER Sep 26 '18 at 13:21
  • $\begingroup$ Yeah you may be correct there :D . It is just easier for myself to follow the code this way. Its personal. Like I said, I apologize it is messy. I did not write it with sharing in mind at the time. I am sure it could be improved and make more sense not only with variable names. The terribly long lines hurt my eyes as well now that I look at it. But it works. I hope it might be useful for somebody. $\endgroup$ – Martynas Žiemys Sep 26 '18 at 13:47
1
$\begingroup$

This is what I came up with. Select the object with the "wrong" materials first (like 'torso.001',...) and the object with the "original" materials last (like 'torso',...). Then run the script. It compares all material names and if one of the "wrong" materials starts with the name of one of the "right" materials, it will be replaced by this mat. (So the material 'torso.001' would be replaced by the material 'torso').

The script does not update the 3dView, so be sure to do some action (like zoom in, etc.) to update the viewport after you run the script!

Also this might not work correctly, if some names are combined of other names. E.g. if you have two materials 'torsoUpper' and 'torso'. If that is a problem, I can edit the script.

Hope this helps.

import bpy

c = bpy.context

# Select multiple objects. 
# The last selected should be the object with the "original" colors
# The script loops through all materials of the other objects and compares
# them to the materials of the "original" object. If their names start with the same
# letters, the material is replaced with the original.
# HINT: The script does not update the 3dView!

print("---RUNNING SCRIPT---")
src = c.active_object

for i in c.selected_objects:
    if (i == src):
        continue
    for src_id in range(len(src.data.materials)):
        for replace_id in range(len(i.data.materials)):
            src_name = src.data.materials[src_id].name
            rep_name = i.data.materials[replace_id].name

            if (rep_name.startswith(src_name)):
                i.data.materials[replace_id] = src.data.materials[src_id]
                print("Replaced "+rep_name+" by "+src_name)
                break
$\endgroup$
  • 1
    $\begingroup$ Recommend using context.active_object for src rather than the last member of the selected objects list. This is usual blender MO to change settings of selected objects with those of the active. @steven850 if the answer works for you can accept and upvote. $\endgroup$ – batFINGER Sep 26 '18 at 13:04
  • $\begingroup$ @batFINGER Thank you, I edited the script. $\endgroup$ – Ectras Sep 27 '18 at 14:25

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service, privacy policy and cookie policy

Not the answer you're looking for? Browse other questions tagged or ask your own question.