Last active
June 24, 2026 16:45
-
-
Save dnnkeeper/b6858b3dd04b3231c10e17f9e1d2247e to your computer and use it in GitHub Desktop.
Blender plugin for finding similar meshes and linking their data. Select->Similar Objects. N->Tools
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| import bpy | |
| from concurrent.futures import ThreadPoolExecutor | |
| bl_info = { | |
| "name": "Link Similar Meshes", | |
| "blender": (2, 80, 0), | |
| "category": "Object", | |
| "version": (1, 0, 1), | |
| "author": "dnnkeeper", | |
| "description": "Finds similar meshes in a scene and links their data", | |
| "location": "View3D > Tool", | |
| "warning": "", | |
| "wiki_url": "", | |
| "tracker_url": "", | |
| } | |
| # Function to check similarity between two objects. It checks if the number of vertices, coordinates, UV coordinates, and materials are the same. | |
| # To reduce the number of comparisons, it checks 10 vertices evenly distributed in the array of vertices, 10 UV coordinates evenly distributed in the UV data, and the materials. | |
| def is_similar(obj1, obj2): | |
| # Check if the materials are the same | |
| if len(obj1.data.materials) != len(obj2.data.materials): | |
| return False | |
| for mat1, mat2 in zip(obj1.data.materials, obj2.data.materials): | |
| if mat1 != mat2: | |
| return False | |
| # Check if the number of vertices is the same | |
| if len(obj1.data.vertices) != len(obj2.data.vertices): | |
| return False | |
| # Check 10 vertices evenly distributed in the array of vertices | |
| num_vertices = len(obj1.data.vertices) | |
| step = max(1, num_vertices // 10) | |
| for i in range(0, num_vertices, step): | |
| if obj1.data.vertices[i].co != obj2.data.vertices[i].co: | |
| return False | |
| # Check 10 UV coordinates evenly distributed in the UV data | |
| num_uvs = len(obj1.data.uv_layers.active.data) | |
| uv_step = max(1, num_uvs // 10) | |
| for i in range(0, num_uvs, uv_step): | |
| if obj1.data.uv_layers.active.data[i].uv != obj2.data.uv_layers.active.data[i].uv: | |
| return False | |
| return True | |
| # List to store unique objects and their similar copies | |
| unique_objects = [] | |
| similar_objects = {} | |
| # Operator to analyze the scene for similar objects | |
| class OBJECT_OT_find_similar(bpy.types.Operator): | |
| bl_idname = "object.find_similar" | |
| bl_label = "Find All Similar Objects" | |
| def execute(self, context): | |
| global unique_objects, similar_objects | |
| unique_objects = [] | |
| similar_objects = {} | |
| similar_count = 0 | |
| def process_object(current_object): | |
| nonlocal similar_count | |
| if current_object.type == 'MESH': | |
| found_similar_data = False | |
| for unique_object in unique_objects: | |
| if is_similar(current_object, unique_object): | |
| if unique_object.name not in similar_objects: | |
| similar_objects[unique_object.name] = [] | |
| similar_objects[unique_object.name].append(current_object.name) | |
| found_similar_data = True | |
| similar_count += 1 | |
| break | |
| if not found_similar_data: | |
| unique_objects.append(current_object) | |
| with ThreadPoolExecutor() as executor: | |
| executor.map(process_object, bpy.context.view_layer.objects) | |
| self.report({'INFO'}, f"Found {len(unique_objects)} unique objects and {similar_count} similar objects.") | |
| return {'FINISHED'} | |
| # Operator to find similar objects for the active object | |
| class OBJECT_OT_find_similar_active(bpy.types.Operator): | |
| bl_idname = "object.find_similar_active" | |
| bl_label = "Find Similar Objects for Active Object" | |
| def execute(self, context): | |
| global unique_objects, similar_objects | |
| active_object = context.view_layer.objects.active | |
| if not active_object or active_object.type != 'MESH': | |
| self.report({'WARNING'}, "No active mesh object selected") | |
| return {'CANCELLED'} | |
| unique_objects = [active_object] | |
| similar_objects = {active_object.name: []} | |
| similar_count = 0 | |
| for obj in bpy.context.view_layer.objects: | |
| if obj != active_object and obj.type == 'MESH': | |
| if is_similar(active_object, obj): | |
| similar_objects[active_object.name].append(obj.name) | |
| similar_count += 1 | |
| # Select the active object and its similar objects | |
| bpy.ops.object.select_all(action='DESELECT') | |
| active_object.select_set(True) | |
| for similar_object_name in similar_objects[active_object.name]: | |
| similar_object = bpy.data.objects.get(similar_object_name) | |
| if similar_object: | |
| similar_object.select_set(True) | |
| self.report({'INFO'}, f"Found and selected {similar_count} similar objects for {active_object.name}.") | |
| return {'FINISHED'} | |
| # Operator to link similar objects | |
| class OBJECT_OT_link_similar(bpy.types.Operator): | |
| bl_idname = "object.link_similar" | |
| bl_label = "Link Similar Objects" | |
| def execute(self, context): | |
| global unique_objects, similar_objects | |
| linked_count = 0 | |
| for unique_object_name, similar_list in similar_objects.items(): | |
| unique_object = bpy.data.objects[unique_object_name] | |
| for similar_object_name in similar_list: | |
| similar_object = bpy.data.objects[similar_object_name] | |
| similar_object.data = unique_object.data | |
| linked_count += 1 | |
| self.report({'INFO'}, f"Linked {linked_count} similar objects.") | |
| return {'FINISHED'} | |
| # Operator to link similar objects by unique object name | |
| class OBJECT_OT_link_similar_by_name(bpy.types.Operator): | |
| bl_idname = "object.link_similar_by_name" | |
| bl_label = "Link Similar Objects by Name" | |
| unique_object_name: bpy.props.StringProperty() | |
| def execute(self, context): | |
| global similar_objects | |
| linked_count = 0 | |
| if self.unique_object_name in similar_objects: | |
| unique_object = bpy.data.objects.get(self.unique_object_name) | |
| if unique_object: | |
| for similar_object_name in similar_objects[self.unique_object_name]: | |
| similar_object = bpy.data.objects.get(similar_object_name) | |
| if similar_object: | |
| similar_object.data = unique_object.data | |
| linked_count += 1 | |
| self.report({'INFO'}, f"Linked {linked_count} similar objects for {self.unique_object_name}") | |
| else: | |
| self.report({'WARNING'}, f"No similar objects found for {self.unique_object_name}") | |
| return {'FINISHED'} | |
| # Operator to select an object by name | |
| class OBJECT_OT_select_object(bpy.types.Operator): | |
| bl_idname = "object.select_object" | |
| bl_label = "Select Object" | |
| object_name: bpy.props.StringProperty() | |
| def execute(self, context): | |
| obj = bpy.data.objects.get(self.object_name) | |
| if obj: | |
| bpy.context.view_layer.objects.active = obj | |
| for o in bpy.context.view_layer.objects: | |
| o.select_set(False) | |
| obj.select_set(True) | |
| context.view_layer.objects.active = obj | |
| bpy.ops.view3d.view_selected(use_all_regions=False) | |
| self.report({'INFO'}, f"Selected {self.object_name}") | |
| else: | |
| self.report({'WARNING'}, f"Object {self.object_name} not found") | |
| return {'FINISHED'} | |
| # Operator to select similar objects by unique object name | |
| class OBJECT_OT_select_similar(bpy.types.Operator): | |
| bl_idname = "object.select_similar" | |
| bl_label = "Select Similar Objects" | |
| unique_object_name: bpy.props.StringProperty() | |
| def execute(self, context): | |
| global similar_objects | |
| if self.unique_object_name in similar_objects: | |
| for obj in bpy.context.view_layer.objects: | |
| obj.select_set(False) | |
| unique_object = bpy.data.objects.get(self.unique_object_name) | |
| if unique_object: | |
| unique_object.select_set(True) | |
| for similar_object_name in similar_objects[self.unique_object_name]: | |
| similar_object = bpy.data.objects.get(similar_object_name) | |
| if similar_object: | |
| similar_object.select_set(True) | |
| self.report({'INFO'}, f"Selected similar objects for {self.unique_object_name}") | |
| else: | |
| self.report({'WARNING'}, f"No similar objects found for {self.unique_object_name}") | |
| return {'FINISHED'} | |
| # Panel to display the results | |
| class OBJECT_PT_similar_panel(bpy.types.Panel): | |
| bl_label = "Similar Objects" | |
| bl_idname = "OBJECT_PT_similar_panel" | |
| bl_space_type = 'VIEW_3D' | |
| bl_region_type = 'UI' | |
| bl_category = 'Tool' | |
| def draw(self, context): | |
| layout = self.layout | |
| active_object = context.view_layer.objects.active | |
| if active_object and active_object.type == 'MESH': | |
| layout.operator("object.find_similar_active", text=f"Find Similar Objects for {active_object.name}") | |
| layout.operator("object.find_similar") | |
| layout.operator("object.link_similar") | |
| if len(unique_objects) > 0: | |
| layout.prop(context.scene, "show_unique_objects", text=f"Unique Objects: {len(unique_objects)}", emboss=False, icon="TRIA_DOWN" if context.scene.show_unique_objects else "TRIA_RIGHT") | |
| if context.scene.show_unique_objects: | |
| for obj in unique_objects: | |
| row = layout.row() | |
| op = row.operator("object.select_object", text=obj.name) | |
| op.object_name = obj.name | |
| similar_count = len(similar_objects.get(obj.name, [])) | |
| op = row.operator("object.select_similar", text=f"Select Similar ({similar_count})") | |
| op.unique_object_name = obj.name | |
| op = row.operator("object.link_similar_by_name", text="Link Similar") | |
| op.unique_object_name = obj.name | |
| def menu_func(self, context): | |
| self.layout.separator() | |
| self.layout.operator("object.find_similar_active", text="Similar Objects") | |
| # Register and unregister classes | |
| def register(): | |
| bpy.utils.register_class(OBJECT_OT_find_similar_active) | |
| bpy.utils.register_class(OBJECT_OT_find_similar) | |
| bpy.utils.register_class(OBJECT_OT_link_similar) | |
| bpy.utils.register_class(OBJECT_OT_link_similar_by_name) | |
| bpy.utils.register_class(OBJECT_OT_select_object) | |
| bpy.utils.register_class(OBJECT_OT_select_similar) | |
| bpy.utils.register_class(OBJECT_PT_similar_panel) | |
| bpy.types.VIEW3D_MT_select_object.append(menu_func) | |
| bpy.types.Scene.show_unique_objects = bpy.props.BoolProperty(name="Show Unique Objects", default=False) | |
| def unregister(): | |
| bpy.utils.unregister_class(OBJECT_OT_find_similar_active) | |
| bpy.utils.unregister_class(OBJECT_OT_find_similar) | |
| bpy.utils.unregister_class(OBJECT_OT_link_similar) | |
| bpy.utils.unregister_class(OBJECT_OT_link_similar_by_name) | |
| bpy.utils.unregister_class(OBJECT_OT_select_object) | |
| bpy.utils.unregister_class(OBJECT_OT_select_similar) | |
| bpy.utils.unregister_class(OBJECT_PT_similar_panel) | |
| bpy.types.VIEW3D_MT_select_object.remove(menu_func) | |
| del bpy.types.Scene.show_unique_objects | |
| if __name__ == "__main__": | |
| register() |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment