Skip to content

Instantly share code, notes, and snippets.

@dnnkeeper
Last active June 24, 2026 16:45
Show Gist options
  • Select an option

  • Save dnnkeeper/b6858b3dd04b3231c10e17f9e1d2247e to your computer and use it in GitHub Desktop.

Select an option

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
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