STEP File Optimization Tools Add-on - A study project

Hello!

The last couple of months I have been working on my university graduation project: Scripting my very first add-on in Blender. The add-on contains tools to support the optimization workflow of STEP files. This post is a summary of my work, please note that this add-on is not for commercial use.

Since I had no prior experience with scripting, Blender and python, this project was quite the challenge. I’m glad that I found this community, since this helped me push through many many questions and problems along the way!

So what is this for?
For those who might be unfamiliar with STEP files: This is a file format commonly used in CAD software, which is used in the engineering industry. They ususally portray machines and other engineering products, usually being very heavy and complex. When importing a STEP file in Blender, which can be done with plugins like this one: https://ambient.gumroad.com/l/stepper, a user will face thousand of objects and 1-4 million tris is not uncommon.
To optimize this for e.g. visualization in VR by hand is very tedious. So, I tried to automate some steps of the workflow!

The add-on consists of the following features:

  • A SelectByName (Predetermined) function: This function filters out objects and selects them based on their name. The names are predetermined in the script and are common names to be found in STEP files, like ‘screws’. After selecting them, the function moves the objects to sub-collections, which can be hidden easily. This automates the tedious step of manually searching for small objects to hide or delete.

  • Search Objects By Name: This function is similar like the one before, except that the user can input a word in a search field. The function will not need the exact name of the object, a part of it is sufficient to select it. This makes is easier to find objects that the previous function might have missed.

  • A Poly Filter: This function will filter objects based on the range the user has set in the UI panel. It checks the polycount of each object and organizes all objects in low-poly, mid-poly, and high-poly collections. This gives the user a better overview of the size of the objects in the scene and can speed up the optimization.

  • Find Duplicate Objects: Duplicate objects are a waste of polycount and finding them can be tedious. This script will find the duplicate objects and select them. Another button in the UI panel allows the user to move these selected duplicate objects to sub-collections.

  • Find Overlapping Objects: Sometimes the STEP files contain multiple variants of the same part/object, which are placed on over each other. To find these kind of objects and other overlapping objects in general, a function was created. It selects all overlapping objects and shows a little pop-up to the user describing how many were selected.

  • Polycount Prefix Rename: A simple functionality that adds the polycount of an objects as a prefix to its name. This makes it easier to spot high-poly objects in the outliner.

  • A Parent-Poly-Material-Rename function: This function renames the selected objects so that their name contains the name of their parent, the polycount and the name of their material.

  • End Rename Export: This rename function helps to prepare the objects for export to Unity. Please note, that this script is very specific to the workflow of the company, where the add-on was created. The script will rename the objects so the name only contains the polycount and the name of the material of the object.

Next to the functionalities of the scripts, the add-on is presented with a UI panel that makes the use and navigation easy for the user. Usability tests shows that using this add-on speeds op the optimization workflow immensely!

Some screenshots of the UI panel:

image

So now you know what it is, but how was it made?
The add-on consists out of multiple elements. Of course, there is a base structure of the bl_info at the top, the import lines, a class that defines the UI panel and integrates the other classes, and the registration und unregistration.
Before I go into detail about the functions, it is important to note that this prototype was made specifically for the user, which are the 3D developers from the company where I graduate. This has effect on how the scripts are made, since they are specifically tailored to their workflow and the add-on will not work with different workflows.

The Select By Name function:
This class utilizes Blender’s select_pattern() function. The are differnt versions integrated in the add-on. One version contains a list of predetermined words and names, the other version utilizes a StringProperty that allows the user to input a words that is run through the select_pattern() function. The selected objects are moved to sub-collections. At the end, all empty collections are removed and user feedback is generated.

bpy.ops.object.select_pattern(pattern = "*deckel*")
        for obj in bpy.context.selected_objects:
            list_of_objects.append(obj)
        bpy.ops.object.select_all(action='DESELECT')
    #Search for name user input field 
    text : bpy.props.StringProperty(name = "Enter Name", default = " ")


    def execute(self, context):
        
        t = (self.text)
        list_names = []
        num_names = len(list_names)
   
        obj = bpy.context.object  
        bpy.ops.object.select_pattern(pattern = f"*{t}*")
        for obj in selected_objects:
            list_names.append(obj)
        

        return {'FINISHED'}
        
    
    def invoke (self, context, event):
        return context.window_manager.invoke_props_dialog(self)
for obj in list_of_objects:
            print(obj.name)
            if obj.type == 'MESH':
                obj.select_set(True)
                
                coll = obj.users_collection
                InsideCollection = bpy.data.collections['Inside Collection']
                OutsideCollection = bpy.data.collections['Outside Collection']
                
                if InsideCollection in coll:
                    bpy.data.collections['Inside Collection'].objects.unlink(obj)
                    bpy.data.collections['Hidden Objects Inside'].objects.link(obj)
                    
                
                if OutsideCollection in coll:
                    bpy.data.collections['Outside Collection'].objects.unlink(obj)
                    bpy.data.collections['Hidden Objects Outside'].objects.link(obj)

image

.
.

The Poly Filter function:
This function utilizes IntProperties, lists, collections and various loops. LPS, MPS and HPS are the variables for the IntProperties. The user can change these values in the UI panel.

  #Define a function to filter selected objects based on poly count 
        def FilterPoly():
                    
            for obj in bpy.context.selected_objects:
                if obj.type == 'MESH' and len(obj.data.polygons) > LPS and len(obj.data.polygons) < LPE:
                    list_lpObjects.append(obj)

                    
        
            for obj in bpy.context.selected_objects:
                if obj.type == 'MESH' and len(obj.data.polygons) > MPS and len(obj.data.polygons) < MPE:
                    list_mpObjects.append(obj)

        
        
        
            for obj in bpy.context.selected_objects:
                if obj.type == 'MESH' and len(obj.data.polygons) > HPS and len(obj.data.polygons) < HPE:
                    list_hpObjects.append(obj)

image

Before calling the defined function, the polycount of each object is retrieved and added to a list. Once this is done, the script organizes the objects based on their polycount and adds them to the appropriate lists.

 #Get poly count of all meshes and store them in list_polyCount
        for obj in selected_objects:
            if obj.type == 'MESH':
                #Reference mesh as obj.data for later
                mesh = obj.data
                #add all the object's poly counts in list_polyCount
                list_polycount.append(len(mesh.polygons))

Finally, the objects are organized into different sub-collections. As mentioned, this add-on is made specifically for the workflow within the company, where so-called “Inside Collections” and “Outside Collections” are used. This is for seperating the visible objects that are on the outside and the objects that are on the inside of a model.

The following code organizes the objects to the correct sub-collections. This is repeated for low-poly, mid-poly and high-poly objects, but for clarification I’ll only add the code for one of these iterations.

print ("Mid-poly objects list" , len(list_mpObjects))            
        
        for obj in list_mpObjects:
            if obj.type == 'MESH':
                coll = obj.users_collection
                
                
                if InsideCollection in coll or HiddenInside in coll:
                    bpy.data.collections['MP Objects Inside'].objects.link(obj)
                    if InsideCollection in coll:
                        bpy.data.collections ['Inside Collection'].objects.unlink(obj)
                    if HiddenInside in coll:
                        bpy.data.collections ['Hidden Objects Inside'].objects.unlink(obj)
                else:
                    pass
                
                if OutsideCollection in coll or HiddenOutside in coll:
                    bpy.data.collections['MP Objects Outside'].objects.link(obj)
                    if OutsideCollection in coll:
                        bpy.data.collections ['Outside Collection'].objects.unlink(obj)
                    if HiddenOutside in coll:
                        bpy.data.collections ['Hidden Objects Outside'].objects.unlink(obj)

At the end of the script, all empty collections are removed and a user feedback message is created.

  #delete all empty collections from the outliner
        for coll in Insidecollections_children:
            if len(coll.objects) == 0:
                bpy.data.collections.remove(coll)

        for coll in Outsidecollections_children:
            if len(coll.objects) == 0:
                bpy.data.collections.remove(coll)
           
            
        bpy.ops.object.select_all(action='DESELECT')  


        #User feedback
        def UserFeedbackGood(message = "", title = "Poly Filter", icon='INFO'):
            def draw (self, context):
                self.layout.label(text=message)         
       
            bpy.context.window_manager.popup_menu(draw, title=title, icon=icon)
    
        UserFeedbackGood("The script was succesful. All objects in the scene were sorted into different collections based on their polycount.")
       
        return {'FINISHED'}
    

def menu_func(self, context):
    self.layout.operator(PolyFilterOperator.bl_idname, text=PolyFilterOperator.bl_label) 

.
.
The Find Duplicate function:
This function determines if there are duplicate objects in the scene based on their object data and location. It is supposed to take linked objects into account. The script select the duplicate objects and with a seperate button in the UI, the user can choose to move them to sub-collections.

  def execute(self, context):
        
        list_of_duplicates = []
        num_dupl = len(list_of_duplicates)
                            
        
        ob = bpy.context.active_object
        selected = bpy.context.selected_objects
        
        #First check to filter out the most basic copies
        for obj1 in selected:
            for obj2 in selected:
                if obj1.type != 'MESH' and obj2.type != 'MESH':
                    if obj1 == obj2:
                        obj1.select_set(False)
                        obj2.select_set(False)
                        continue
                    
                    if obj1.data != obj2.data:
                        continue 
                    
                    matr1 = obj1.matrix_world
                    matr2 = obj2.matrix_world
                    
                    if matr1 == matr2:
                        print ("Duplicate found! ", obj1.name, obj2.name)
#                        obj1.select_set(True)
                        obj2.select_set(True)
                        list_of_duplicates.append (obj2)
                        if obj1.type == 'EMPTY' and obj2.type == 'EMPTY':
                            obj1.select_set(False)
                            obj2.select_set(False)
                                    
                    
                else:
                    print ("No duplicates", obj1.name, obj2.name)
                    if obj1.type == 'EMPTY' and obj2.type == 'EMPTY':
                            obj1.select_set(False)
                            obj2.select_set(False)
            
        return {'FINISHED'}   
    
         
    
    #User feedback
    def UserFeedbackGood(message = "", title = "Find Duplicates", icon='INFO'):
        def draw (self, context):
            self.layout.label(text=message)         
       
            bpy.context.window_manager.popup_menu(draw, title=title, icon=icon)
    
        UserFeedbackGood(f"The script was succesful.{list_of_duplicates} duplicate objects in the scene are selected.")
                     
        return {'FINISHED'}
    

image

.
.
The Find Overlap function:
This function is somewhat similar to the Find Duplicate function. However , it finds and selects objects that are overlapping, not duplicates. Sometimes, STEP files have multiple versions of the same part/object included and this script can be useful to detect these kind of variations.

def execute(self, context):
        
        list_overlaps = []
        num_overlaps = len(list_overlaps)

        mesh_objs = [obj for obj in bpy.data.objects if obj.type == 'MESH']
        test_grp = []
        tg2 = test_grp
        
        
        #add all objects in scene to test group list
        for obj in mesh_objs:
            test_grp.append(obj)
#            print (len(test_grp))  #debug
       
       #first rough check. If they are the same objects, then are not tested. If they have the same location, idem 
        for obj1 in test_grp:
            for obj2 in test_grp:
                if not obj1 == obj2:
                    if obj1.matrix_world == obj2.matrix_world:
                        continue
                
                    matr1 = obj1.matrix_world
                    matr2 = obj2.matrix_world
                    
                    for obj2 in test_grp:
                        tg2.remove (obj2)
                        for obj1 in tg2:
                            continue
       
                        #get the geometry in world coordinates
                        vert1 = [matr1 @ v.co for v in obj1.data.vertices]
                        poly1 = [p.vertices for p in obj1.data.polygons]
        
                        vert2 = [matr2 @ v.co for v in obj2.data.vertices]
                        poly2 = [p.vertices for p in obj2.data.polygons]
        
                        #create the BVH trees
                        bvh1 = BVHTree.FromPolygons(vert1, poly1)
                        bvh2 = BVHTree.FromPolygons(vert2, poly2)
            
                        #test if overlap 
                        if bvh1.overlap(bvh2):                    
                            print ("Overlapping objects found: ",obj1.name,",",obj2.name)
                            list_overlaps.append(obj2)
                            obj1.select_set(True)
                            obj2.select_set(True) 
                        else:
                            pass
   
        return {'FINISHED'}




    #User feedback
    def UserFeedbackGood(message = "", title = "Find Overlaps", icon='INFO'):
        def draw (self, context):
            self.layout.label(text=message)         
       
        bpy.context.window_manager.popup_menu(draw, title=title, icon=icon)
    
        UserFeedbackGood(f"The script was succesful.{num_overlaps} overlapping objects in the scene are selected.")
                     
        return {'FINISHED'}    

.
.

The Polycount Prefix Rename function
A simple, yet very useful script that add the object’s polycount as a prefix to its name.

#add the poly count as a prefix to each object's name
        for obj in selected_objects:
            if obj.type == 'MESH':
                mesh = obj.data
                polycount = len(mesh.polygons)
                prefix = polycount
                list_polycount_prefix.append(prefix)
                obj.name = '{}_{}'.format(prefix, obj.name)

.
.
The Parent-Poly-Material Rename function:
A rename function similar to the previous, only that is also adds the name of the object’s parent and material as a prefix.

 #add the poly count and material as a prefix to each object's name
        for obj in selected_objects:
            if obj.type == 'MESH':            
                material = obj.active_material
                mesh = obj.data
                polycount = len(mesh.polygons)
                parrent = obj.parent
                
                
                obj.name = '{}_{}_{}'.format(parrent, polycount, material)

.
.
The EndRename Export function
This function is another simple renaming function, suited to prep the objects for export to Unity in the way that is specific for this workflow. It works just like the other renaming functiosn, only that is adds only the polycount and active material to the name and replaces the old name.

for obj in selected_objects:
            if obj.type == 'MESH':
                mesh = obj.data
                polycount = len(mesh.polygons)
                prefix = polycount
                obj.name = '{}_{}'.format(prefix, obj.active_material.name)

.
.
Registration and Unregistration
Since this add-on contains so many different classes, the registration and unregistration was done with a loop and utilizes a list.

classes = [Optimization_Properties, OptimizationToolsPanel, PolyFilter, PolyPrefixRename, 
ParentPolyMat_Rename, EndRenameExport, FindDuplicates, MoveDuplicates, 
SelectByName, SearchForName, MoveNames, FindOverlap, CleanMesh] 


def register ():
    print ('registered') #debug
    for cls in classes:
        bpy.utils.register_class(cls)
    
    bpy.types.Scene.pf_property = bpy.props.PointerProperty(type=Optimization_Properties)
        

        

def unregister():
    print ('unregistered') #debug
    for cls in classes:
        bpy.utils.unregister_class(cls)
        
        del bpt.types.Scene.pf_property
        

if __name__ == "__main__":
    register()

.
.
This is it! Thank you for reading and if you have any questions or feedback, I’d be happy to respond!
Cheers!

3 Likes

I am not super knowledgeable of STEP workflows, so there are lots of specialized features you certainly know best.

However the part of replacing duplicate objects, is on my watchlist. I think is so important that applies to universally everywhere. And not only for on the importing stage, but having the ability to clean up or optimize a project at any moment, through an operator.

Also I don’t know exactly if you are interested to improve the script further, once you send the objects into a collection for hiding. Instead you can can swap them with linked duplicates (create new - copy transfrom - delete old), so in this thinking they become “instances” rather than duplicate data.
bpy.ops.object.duplicate_move_linked(TRANSFORM_OT_translate={"value":(0, 0, 0)})

Hey, thank you for your input!
I totally agree with you that it’s important to have the ability to clean up and optimize project. This was also the original thought when starting this project. And yes, I do plan to improve this script further! I like your idea about the linked duplicates, I’ll check it out! Thanks :slight_smile:

1 Like