Global material replacement script, get Material of subobject in blocks?

During cooperation of large projects, I always encounter the situation like below:
Got a final large project model with numerous render materials from different designers, many blocks in it, and the goal is to make some rendering for presentation or simply add my part of model in it. There will be ten different glass materials, five grey colored materials, etc.

I always want to purge and rearrange the materials for simplication and better control.
There is an extension in Sketchup called material replacer that meet demand( The Fastest Way to REPLACE MATERIALS in SketchUp!). While in rhino, the way to do this is to:

  1. right click the material in material panel and hit ‘select objects’.
  2. right click another material for replacement and hit ‘assign to objects’.

The above action cannot affect objects in blocks or blocks of block which is very common in large projects. I also found topics like this, and they seems not solved yet:
https://discourse.mcneel.com/t/real-global-material-replacement-please-please-fix/11414/9
https://discourse.mcneel.com/t/how-to-replace-material-in-rhino-similar-as-the-use-as-replacement-function-in-vray/79921/11

So I wrote a script for this purpose, and the usage is like that of sketchup.

#!encoding:utf-8

# A simple script for global material replacement
# It can replace all materials(including objects in blocks) of a model with another.  
# Since I havn't found out how to get the RenderMaterial of a subobject in (nested)blocks, 
# you can only pick objects outside blocks for the script to know the two material for replacement.
import scriptcontext as sc
import Rhino
import rhinoscriptsyntax as rs

def SelectObjs(prompt, preselect=True):
    go = Rhino.Input.Custom.GetObject()
    go.SetCommandPrompt(prompt)
    go.GeometryFilter = Rhino.DocObjects.ObjectType.Brep|Rhino.DocObjects.ObjectType.Mesh
    go.SubObjectSelect = False
    if not preselect:
        go.EnablePreSelect(False, False);
        go.DeselectAllBeforePostSelect = True;
    go.Get()
    if (go.CommandResult() == Rhino.Commands.Result.Success):
        return go.Object(0)
    return None

def ChangeMat(Objects,RenderMat1,RenderMat2):
    for object in Objects:
        if type(object)!=Rhino.DocObjects.InstanceObject:
            if object.RenderMaterial!=None:
                if object.RenderMaterial.Name==RenderMat1.Name:
                    object.RenderMaterial=RenderMat2
                    object.CommitChanges()
        else:
            idef=object.InstanceDefinition
            idefIndex=idef.Index
            RefObjects=idef.GetObjects()
            for obj in RefObjects:
                if type(obj)==Rhino.DocObjects.InstanceObject:
                    ChangeMat([obj],RenderMat1,RenderMat2)
                else:
                    if obj.RenderMaterial!=None:
                        if obj.RenderMaterial.Name == RenderMat1.Name:
                            obj.RenderMaterial=RenderMat2
                            obj.CommitChanges()
                newGeometry = []
                newAttributes = []
                for object in RefObjects:
                    newGeometry.append(rs.coercegeometry(object))
                    ref = Rhino.DocObjects.ObjRef(object)
                    attr = ref.Object().Attributes
                    newAttributes.append(attr)
                InstanceDefinitionTable = sc.doc.ActiveDoc.InstanceDefinitions
                p=InstanceDefinitionTable.ModifyGeometry(idefIndex, newGeometry, newAttributes)

def ReplaceMaterials():
    RenderMat1=None
    while RenderMat1==None: 
        object1 = SelectObjs('Select Material to replace',False).Object()
        RenderMat1 = object1.RenderMaterial
    RenderMat2=None
    while RenderMat2==None or RenderMat2.Name==RenderMat1.Name: 
        object2 = SelectObjs('Select Material to replace with',False).Object()
        RenderMat2 = object2.RenderMaterial
    ObjectAll=sc.doc.Objects
    ChangeMat(ObjectAll,RenderMat1,RenderMat2)

ReplaceMaterials()

I created an aliase for this script. Almost done.
But I havn’t found out how to get the RenderMaterial of a subobject in (nested)blocks. If it can get materials of objects in blocks, one will not have to make new material assigned objects outside blocks for the script to refer to.
So does anyone know how to refer to the subobject materials in blocks? I’ve tried set the ‘go.SubObjectSelect = True’ in my script and

a=SelectObjs('***',True)
CompIndex=a.Object().GetSelectedSubObjects()

to get the Index of the subobject, but still can’t reach the rendermaterial of the selected subobject. And the nested blocks will make it more complicated.

I found the ObjRef.InstanceDefinitionPart Method to get the material of subobjects. I modified the script and it can refer to material of subobjects in blocks or blocks inside another block. But until now, I notice the script can only fetch topmost block of the selected object. So it cannot refer to too deeper subobjects.

#!encoding:utf-8

# A simple script for global material replacement
# It can replace all materials(including objects in blocks) of a model with another.  
# You can pick subobject in a block or in a block inside another block for material reference,
# but until now it can't recognize 'deeper' subobjects.

import scriptcontext as sc
import Rhino
import rhinoscriptsyntax as rs

def SelectObjs(prompt, preselect=True):
    go = Rhino.Input.Custom.GetObject()
    go.SetCommandPrompt(prompt)
    go.GeometryFilter = Rhino.DocObjects.ObjectType.Brep|Rhino.DocObjects.ObjectType.Mesh
    go.SubObjectSelect = True
    go.ChooseOneQuestion = True
    if not preselect:
        go.EnablePreSelect(False, False);
        go.DeselectAllBeforePostSelect = True;
    go.Get()
    if (go.CommandResult() == Rhino.Commands.Result.Success):
        return go.Object(0)
    return None

def SelectMaterial(SelectedObject):
    if rs.IsBlockInstance(SelectedObject.Object())==False:
        Selection=SelectedObject.Object()
    else:
        Selection = SelectedObject.InstanceDefinitionPart()
        if rs.IsBlockInstance(Selection):
            CompIndex=SelectedObject.Object().GetSelectedSubObjects()
            ObjectList=Selection.InstanceDefinition.GetObjects()
            Selection= ObjectList[CompIndex[0].Index]
    return Selection.RenderMaterial

def ChangeMat(Objects,RenderMat1,RenderMat2):
    for object in Objects:
        if type(object)!=Rhino.DocObjects.InstanceObject:
            if object.RenderMaterial!=None:
                if object.RenderMaterial.Name==RenderMat1.Name:
                    object.RenderMaterial=RenderMat2
                    object.CommitChanges()
        else:
            idef=object.InstanceDefinition
            idefIndex=idef.Index
            RefObjects=idef.GetObjects()
            for obj in RefObjects:
                if type(obj)==Rhino.DocObjects.InstanceObject:
                    ChangeMat([obj],RenderMat1,RenderMat2)
                else:
                    if obj.RenderMaterial!=None:
                        if obj.RenderMaterial.Name == RenderMat1.Name:
                            obj.RenderMaterial=RenderMat2
                            obj.CommitChanges()
                newGeometry = []
                newAttributes = []
                for object in RefObjects:
                    newGeometry.append(rs.coercegeometry(object))
                    ref = Rhino.DocObjects.ObjRef(object)
                    attr = ref.Object().Attributes
                    newAttributes.append(attr)
                InstanceDefinitionTable = sc.doc.ActiveDoc.InstanceDefinitions
                p=InstanceDefinitionTable.ModifyGeometry(idefIndex, newGeometry, newAttributes)

def ReplaceMaterials():
    RenderMat1=None
    while RenderMat1==None: 
        object1 = SelectObjs('Select Material to replace',False)
        RenderMat1 = SelectMaterial(object1)
    RenderMat2=None
    while RenderMat2==None or RenderMat2.Name==RenderMat1.Name: 
        object2 = SelectObjs('Select Material to replace with',False)
        RenderMat2 = SelectMaterial(object2)
    ObjectAll=sc.doc.Objects
    ChangeMat(ObjectAll,RenderMat1,RenderMat2)

ReplaceMaterials()

The script is here. Hope can help others. If anyone know how to get rendermaterials of ‘deeper’ subobjects, please let me know.
-Chenlong

ReplaceMaterialsV2.py (3.3 KB)

maybe this sample helps?

Thank you @Gijs . The key to the problem is that how to let the script know which of the innermost subobject is clicked by the user. I figured out that the
CompIndex=SelectedObject.Object().GetSelectedSubObjects()
sentence can get the Index of innermost subobject, but the index is only for the inner block, not for the upper-level blocks.
While the Selection = SelectedObject.InstanceDefinitionPart() only fetch the second level block that the user select.

So the problem is clear- there is a gap between the innermost and the outermost level blocks. If we want to get the inner most subobject we selected, the two way:

  1. to get the index tree of the selected object in the block.
  2. to use the “SelectedObject” method.
    Until now, it seems to me that neither road is possible. sigh… :smiling_face_with_tear:

I see, from your previous post I understood the user would not (need to) click on any object. I thought you wanted the script to automatically go through all of your scene objects and replace material X with material Y but reading a bit through your script I see that you retrieve the material name from the clicked object and that that part goes wrong when you click on an object inside a nested block. Is this correct?

Yes, that’s the key point. So the question will be:
How to let the Rhino.Input.Custom.GetObject() retrieve the material of the innermost object of nested block.