Skip to content

Commit ea42c71

Browse files
Weislclaude
andcommitted
#631 Cache modifier-stack bake during collider drag
convert_to_mesh()/apply_all_modifiers() re-baked the base object's full modifier stack via evaluated_depsgraph_get() + new_from_object() on every MOUSEMOVE delta while dragging (Loose Parts + Use Modifier Stack), even though the base object's own modifiers don't change during that drag. In scenes with many other objects/modifiers, each of those re-evaluations costs tens to hundreds of ms since Blender has to resolve the whole scene's dependency graph, not just the one object. Cache the baked mesh per (base object, use_modifiers) for the operator's lifetime instead, turning a per-drag-delta cost into a one-time bake. Verified via Blender MCP: ~31% faster per drag step (1.54s -> 1.06s avg) running the real execute() path 15x in a 6k-object stress scene. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
1 parent ebc7469 commit ea42c71

1 file changed

Lines changed: 80 additions & 23 deletions

File tree

collider_shapes/add_bounding_primitive.py

Lines changed: 80 additions & 23 deletions
Original file line numberDiff line numberDiff line change
@@ -1281,6 +1281,20 @@ def remove_empty_collection(context, collection_name):
12811281
if collection is not None and len(collection.objects) == 0:
12821282
bpy.data.collections.remove(collection)
12831283

1284+
def _clear_modifier_bake_cache(self):
1285+
"""Free the mesh datablocks cached by convert_to_mesh() /
1286+
apply_all_modifiers() (see self._modifier_bake_cache). They're kept
1287+
alive only by this dict - never linked to any object - so they must
1288+
be removed explicitly on confirm/cancel or they'd leak as orphan
1289+
mesh data."""
1290+
cache = getattr(self, '_modifier_bake_cache', None)
1291+
if not cache:
1292+
return
1293+
meshes = [me for me in cache.values() if me and me.name in bpy.data.meshes]
1294+
if meshes:
1295+
bpy.data.batch_remove(meshes)
1296+
self._modifier_bake_cache = {}
1297+
12841298
@staticmethod
12851299
def set_collections(obj, collections):
12861300
"""link an object to a collection"""
@@ -1297,8 +1311,7 @@ def set_collections(obj, collections):
12971311
col.objects.unlink(obj)
12981312

12991313
# Modifiers
1300-
@staticmethod
1301-
def apply_all_modifiers(context, obj):
1314+
def apply_all_modifiers(self, context, obj, cache_key=None):
13021315
"""Replace obj's mesh data with the fully evaluated result of its
13031316
modifier stack - including any unrealized instances a modifier like
13041317
Geometry Nodes "Instance on Points" produces without a "Realize
@@ -1310,17 +1323,31 @@ def apply_all_modifiers(context, obj):
13101323
can't bake unrealized instances into real geometry. Evaluating via
13111324
the depsgraph and merging instances manually (merge_object_instances)
13121325
sidesteps that limitation.
1326+
1327+
`cache_key`, if given, reuses/populates self._modifier_bake_cache so
1328+
repeated calls for the same source object (e.g. once per MOUSEMOVE
1329+
delta while dragging) don't each pay for a fresh depsgraph
1330+
evaluation - see convert_to_mesh() for why that matters (#631).
13131331
"""
13141332
context.view_layer.objects.active = obj
13151333
if not obj.modifiers:
13161334
return
13171335

1336+
cached_mesh = self._modifier_bake_cache.get(cache_key) if cache_key else None
1337+
if cached_mesh is not None and cached_mesh.name in bpy.data.meshes:
1338+
old_data = obj.data
1339+
obj.data = cached_mesh.copy()
1340+
obj.modifiers.clear()
1341+
if old_data.users == 0:
1342+
bpy.data.meshes.remove(old_data)
1343+
return
1344+
13181345
depsgraph = context.evaluated_depsgraph_get()
13191346
me = bpy.data.meshes.new_from_object(obj.evaluated_get(depsgraph), depsgraph=depsgraph)
13201347

13211348
bm = bmesh.new()
13221349
bm.from_mesh(me)
1323-
OBJECT_OT_add_bounding_object.merge_object_instances(bm, obj, depsgraph)
1350+
self.merge_object_instances(bm, obj, depsgraph)
13241351
bm.to_mesh(me)
13251352
bm.free()
13261353

@@ -1331,6 +1358,9 @@ def apply_all_modifiers(context, obj):
13311358
if old_data.users == 0:
13321359
bpy.data.meshes.remove(old_data)
13331360

1361+
if cache_key:
1362+
self._modifier_bake_cache[cache_key] = me.copy()
1363+
13341364
@staticmethod
13351365
def remove_all_modifiers(context, obj):
13361366
"""Remove all modifiers of an object"""
@@ -1387,31 +1417,44 @@ def restore_obj_mod_from_dic(modifier_dic):
13871417
modifier.show_in_editmode = mod_entry["show_in_editmode"]
13881418

13891419
def convert_to_mesh(self, context, object, use_modifiers=False):
1390-
mods = self.store_obj_mod_in_dic(object)
1420+
# Baking (evaluated_depsgraph_get + new_from_object) is only needed
1421+
# for use_modifiers=True - the base object's modifier stack doesn't
1422+
# change between drag deltas, so reuse the last bake instead of
1423+
# re-evaluating the whole scene's depsgraph on every MOUSEMOVE (#631).
1424+
cache_key = (object, use_modifiers) if use_modifiers else None
1425+
cached_mesh = self._modifier_bake_cache.get(cache_key) if cache_key else None
1426+
1427+
if cached_mesh is not None and cached_mesh.name in bpy.data.meshes:
1428+
me = cached_mesh.copy()
1429+
else:
1430+
mods = self.store_obj_mod_in_dic(object)
13911431

1392-
for mod in object.modifiers:
1393-
mod.show_viewport = use_modifiers
1394-
mod.show_in_editmode = use_modifiers
1432+
for mod in object.modifiers:
1433+
mod.show_viewport = use_modifiers
1434+
mod.show_in_editmode = use_modifiers
13951435

1396-
if use_modifiers:
1397-
deg = context.evaluated_depsgraph_get()
1398-
me = bpy.data.meshes.new_from_object(object.evaluated_get(deg), depsgraph=deg)
1399-
1400-
bm = bmesh.new()
1401-
bm.from_mesh(me)
1402-
self.merge_object_instances(bm, object, deg)
1403-
bm.to_mesh(me)
1404-
bm.free()
1405-
else:
1406-
# Create mesh from base data without applying modifiers
1407-
me = object.data.copy()
1408-
me.update()
1436+
if use_modifiers:
1437+
deg = context.evaluated_depsgraph_get()
1438+
me = bpy.data.meshes.new_from_object(object.evaluated_get(deg), depsgraph=deg)
1439+
1440+
bm = bmesh.new()
1441+
bm.from_mesh(me)
1442+
self.merge_object_instances(bm, object, deg)
1443+
bm.to_mesh(me)
1444+
bm.free()
1445+
else:
1446+
# Create mesh from base data without applying modifiers
1447+
me = object.data.copy()
1448+
me.update()
1449+
1450+
self.restore_obj_mod_from_dic(mods)
1451+
1452+
if cache_key:
1453+
self._modifier_bake_cache[cache_key] = me.copy()
14091454

14101455
new_obj = bpy.data.objects.new(object.name + "_mesh", me)
14111456
col = self.add_to_collections(context, new_obj, 'tmp_mesh', hide=False, color=self.prefs.col_tmp_collection_color)
14121457

1413-
self.restore_obj_mod_from_dic(mods)
1414-
14151458
new_obj.matrix_world = object.matrix_world
14161459
context.view_layer.objects.active = new_obj
14171460
return new_obj
@@ -1515,7 +1558,7 @@ def get_pre_processed_mesh_objs(self, context, default_world_spc=True, use_local
15151558
tmp_ob = delete_non_selected_verts(tmp_ob)
15161559

15171560
if self.my_use_modifier_stack:
1518-
self.apply_all_modifiers(context, tmp_ob)
1561+
self.apply_all_modifiers(context, tmp_ob, cache_key=(base_ob, True))
15191562
base = tmp_ob
15201563

15211564
self.tmp_meshes.append(tmp_ob)
@@ -1663,6 +1706,7 @@ def cancel_cleanup(self, context, delete_colliders=True):
16631706
# Delete temporary objects
16641707
self.remove_objects(self.tmp_meshes)
16651708
self.remove_empty_collection(context, 'tmp_mesh')
1709+
self._clear_modifier_bake_cache()
16661710

16671711
self.reset_display(context)
16681712

@@ -1744,6 +1788,18 @@ def __init__(self, *args, **kwargs):
17441788
self.collision_group_idx = 0
17451789
self._naming_cache = {}
17461790

1791+
# Modifier-stack bake results (convert_to_mesh / apply_all_modifiers),
1792+
# keyed by (base_ob, use_modifiers). Baking involves an
1793+
# evaluated_depsgraph_get() + new_from_object(), which in scenes with
1794+
# many other objects/modifiers costs tens to hundreds of ms - and
1795+
# execute() (hence this bake) reruns on every MOUSEMOVE delta while
1796+
# dragging the collider's own parameters, even though the base
1797+
# object's own modifier stack doesn't change during that drag. Caching
1798+
# the baked mesh here turns that into a one-time cost per base object
1799+
# for the operator's lifetime (#631). Cleared in
1800+
# _clear_modifier_bake_cache() on confirm/cancel.
1801+
self._modifier_bake_cache = {}
1802+
17471803
@classmethod
17481804
def poll(cls, context):
17491805
count = 0
@@ -2338,6 +2394,7 @@ def modal(self, context, event):
23382394
# Delete temporary generated meshes
23392395
self.remove_objects(self.tmp_meshes)
23402396
self.remove_empty_collection(context, 'tmp_mesh')
2397+
self._clear_modifier_bake_cache()
23412398

23422399
try:
23432400
bpy.types.SpaceView3D.draw_handler_remove(self._handle, 'WINDOW')

0 commit comments

Comments
 (0)