2
$\begingroup$

I want to rename objects and their meshes by their triangle counts in a particular collection. The script should identify similar objects and put them in a collection then rename them based on their triangle counts in a format below:

Format:

  • LOD0: Cube

  • LOD1: Cube_LOD1

  • LOD2: Cube_LOD2

  • LOD3: Cube_LOD3

  • LOD4: Cube_LOD4

    and so on...

LOD0 should not have a suffix.

enter image description here


From:

enter image description here

TO:

enter image description here


$\endgroup$
9
  • $\begingroup$ What do you mean with "similar objects" ? Do you mean the name of the objects? $\endgroup$
    – Rivin
    May 10, 2021 at 21:54
  • $\begingroup$ Yes the name of the objects $\endgroup$
    – Karan
    May 11, 2021 at 0:29
  • $\begingroup$ If there is a way to count triangles of meshes in the bpy, this should be very easy. Will see about it… $\endgroup$
    – TheLabCat
    May 11, 2021 at 1:55
  • 2
    $\begingroup$ @ZargulTheWizard you can count faces easily (bmesh instances have a faces property, which you can pass to len(). However, if not all faces are triangles, you either need to triangulate first, or for higher performance, count vertices of each face and determine the number of triangles that face would produce. I found this question: blender.stackexchange.com/questions/102597/… and the formula there is: num_tris = sum(len(f.verts) - 2 for f in bm.faces) or num_tris = len(bm.calc_loop_triangles()) $\endgroup$ May 11, 2021 at 10:42
  • 1
    $\begingroup$ IDK what IDK is? $\endgroup$
    – batFINGER
    May 11, 2021 at 11:30

2 Answers 2

1
$\begingroup$

I came up with this script as a solution.

It automatically finds similar objects, sorts them by triangles, add them to collection and rename the object and the mesh.

Edit: doesn't make new set of collection when press twice

import bpy

def get_object_with_mesh():
    l = []
    for obj in bpy.data.objects:
        if obj.type == "MESH":
            l.append(obj)
    return l

def get_collection(name):
    collection = bpy.data.collections.get(name, None)
    if collection is None:
        coll = bpy.data.collections.new(name)
        bpy.context.scene.collection.children.link(coll)
        return coll
    return collection

def get_basename(name):
    split = name.split(".")
    if split[-1].isnumeric():
        return get_basename(".".join(split[:-1]))
    else:
        split = name.split("_LOD")
        if split[-1].isnumeric():
            return get_basename("_LOD".join(split[:-1]))
        else:
            return "_LOD".join(split)

def get_similars(objects):
    object_dict = {}
    for obj in objects:
        name = get_basename(obj.name)
        object_dict.setdefault(name, [])
        object_dict[name].append(obj)
    return object_dict

def change_link_collection(collection, objects):
    # unlinkt
    for obj in objects:
        if obj in bpy.context.scene.collection.objects.values():
            bpy.context.scene.collection.objects.unlink(obj)
    # link
        if obj not in collection.objects.values():
            collection.objects.link(obj)

def count_triangles(object):
    object.data.calc_loop_triangles()
    return len(object.data.loop_triangles)

def rename_objects_by_triangles(objects, name, reverse):
    sorted_objects = sorted(objects, key= count_triangles, reverse= reverse)
    sorted_objects[0].name = name
    sorted_objects[0].data.name = name
    for i, obj in enumerate(sorted_objects[1:], 1):
        new_name = name + "_LOD" + str(i)
        obj.name = new_name
        obj.data.name = new_name
    return sorted_objects

def parent_objects(objects):
    for object in objects[1:]:
        object.parent = objects[0]
        object.matrix_parent_inverse = parent.matrix_world.inverted()

def append_to_collection(objects, name):
    if objects[0].parent is None:
        change_link_collection(self.get_collection("Meshes"), [objects[0]])
    parent_collection = get_collection("LODs")
    if len(objects) > 1 or objects[0].parent is not None:
        child_collection = get_collection("LOD_" + name)
        if child_collection in bpy.context.scene.collection.children.values():
            bpy.context.scene.collection.children.unlink(child_collection)
        if not (child_collection in parent_collection.children.values()):
            parent_collection.children.link(child_collection)
        if objects[0].parent is None:
            change_link_collection(child_collection, objects[1:])
        else:
            change_link_collection(child_collection, objects)
    

def main():
    object_dict = get_similars(get_object_with_mesh())
    for name in object_dict:
        # change the boolean value of the function "rename_objects_by_triangles" to sort:
        # from low to high triangles -> False                  |
        # from high to low triangles -> True                   V
        objects = rename_objects_by_triangles(object_dict[name], name, False)
        append_to_collection(objects, name)
        parent_objects(objects)
main()

To convert this script to an Operator:

I added an EnumProperty to decide with a dropdown to sort from 'High to Low' or from 'Low to High'

import bpy
from bpy.props import EnumProperty

class OBJECT_OT_collect_similars(bpy.types.Operator):
    bl_idname = 'object.collect_similars'
    bl_label = 'Collect Similar Object'
    bl_options = {'REGISTER'}
    
    sort : EnumProperty(name= 'sort', items= [('LTH','Low To High', ''),('HTL','High To Low','')])
    
    def execute(self, context):
        object_dict = self.get_similars(self.get_object_with_mesh())
        for name in object_dict:                       
            objects = self.rename_objects_by_triangles(object_dict[name], name, self.sort == 'HTL')
            self.parent_objects(objects)
            self.append_to_collection(objects, name)
        return {'FINISHED'}
    
    def get_object_with_mesh(self):
        l = []
        for obj in bpy.data.objects:
            if obj.type == "MESH":
                l.append(obj)
        return l

    def get_collection(self, name):
        collection = bpy.data.collections.get(name, None)
        if collection is None:
            coll = bpy.data.collections.new(name)
            bpy.context.scene.collection.children.link(coll)
            return coll
        return collection

    def get_basename(self, name):
        split = name.split(".")
        if split[-1].isnumeric():
            return self.get_basename(".".join(split[:-1]))
        else:
            split = name.split("_LOD")
            if split[-1].isnumeric():
                return self.get_basename("_LOD".join(split[:-1]))
            else:
                return "_LOD".join(split)

    def get_similars(self, objects):
        object_dict = {}
        for obj in objects:
            name = self.get_basename(obj.name)
            object_dict.setdefault(name, [])
            object_dict[name].append(obj)
        return object_dict

    def change_link_collection(self, collection, objects):
        # unlinkt
        for obj in objects:
            if obj in bpy.context.scene.collection.objects.values():
                bpy.context.scene.collection.objects.unlink(obj)
        # link
            if obj not in collection.objects.values():
                collection.objects.link(obj)

    def count_triangles(self, object):
        object.data.calc_loop_triangles()
        return len(object.data.loop_triangles)

    def rename_objects_by_triangles(self, objects, name, reverse):
        sorted_objects = sorted(objects, key= self.count_triangles, reverse= reverse)
        sorted_objects[0].name = name
        sorted_objects[0].data.name = name
        for i, obj in enumerate(sorted_objects[1:], 1):
            new_name = name + "_LOD" + str(i)
            obj.name = new_name
            obj.data.name = new_name
        return sorted_objects

    def parent_objects(self, objects):
        parent = objects[0]
        for object in objects[1:]:
            object.parent = parent
            object.matrix_parent_inverse = parent.matrix_world.inverted()
            

    def append_to_collection(self, objects, name):
        if objects[0].parent is None:
            self.change_link_collection(self.get_collection("Meshes"), [objects[0]])
        parent_collection = self.get_collection("LODs")
        if len(objects) > 1 or objects[0].parent is not None:
            child_collection = self.get_collection("LOD_" + name)
            if child_collection in bpy.context.scene.collection.children.values():
                bpy.context.scene.collection.children.unlink(child_collection)
            if not (child_collection in parent_collection.children.values()):
                parent_collection.children.link(child_collection)
            if objects[0].parent is None:
                self.change_link_collection(child_collection, objects[1:])
            else:
                self.change_link_collection(child_collection, objects)
            
bpy.utils.register_class(OBJECT_OT_collect_similars)
$\endgroup$
8
  • $\begingroup$ It makes another set of collections when I press twice $\endgroup$
    – Karan
    May 13, 2021 at 8:27
  • $\begingroup$ Sorry for the late answer. How to implement an Operator: docs.blender.org/api/current/bpy.types.Operator.html $\endgroup$
    – Rivin
    May 14, 2021 at 10:30
  • $\begingroup$ Not working with objects name Cube.001_LOD1, Cube.001_LOD2, Cube.001_LOD3, Cube.001_LOD4, etc :( $\endgroup$
    – Karan
    May 14, 2021 at 12:29
  • $\begingroup$ What do you want as a result? Example: 'Cube.001' as base name or 'Cube' as base name $\endgroup$
    – Rivin
    May 14, 2021 at 12:37
  • $\begingroup$ Cube as a base name and parent of all the LODs eg: LOD1, LOD2, etc $\endgroup$
    – Karan
    May 14, 2021 at 12:50
2
$\begingroup$

import bpy
name = 'Cube'  # case sensitive
collection_name = f'{name}.collection'
i = 0
obj = bpy.data.objects.get(name)
group = []
while obj: 
    # this loop assumes you don't have a missing object
    # (e.g. Cube, Cube.001, Cube.003 will not reach to the Cube.003)
    group.append(obj)
    i += 1
    obj = bpy.data.objects.get(f'{name}.{i:03d}')

'''lines 4-12 could be replaced with a single line below, but the line below
will match objects with names 'Cube.000', 'Cubes', 'Cube_copy' etc. as well
as it will go through all objects on the scene, which (rarely) could  be slow'''
# group = [o for o in bpy.data.objects if o.name.startswith(name)]

target_collection = bpy.data.collections.get(collection_name)
if not target_collection:
    target_collection = bpy.data.collections.new(collection_name)
    bpy.context.scene.collection.children.link(target_collection)

def sort_key(obj):
    obj.data.calc_loop_triangles()
    return len(obj.data.loop_triangles)

result = list(enumerate(sorted(group, key=sort_key), start=1))
    
for i, obj in result:
    obj.name = name + (f'_LOD{i}' if i<len(result) else '') 
    for collection in obj.users_collection:
        collection.objects.unlink(obj)
    target_collection.objects.link(obj)

How to put similar named objects into collection in python?

Finding Vertices, Edges, Faces, and Tris using Python

$\endgroup$

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.