Fix T78854: Cell fracture fails in background mode
[blender-addons.git] / object_fracture_cell / fracture_cell_setup.py
blobccb1e204224552b192a8f8681c4bcf180898a7ac
1 # SPDX-License-Identifier: GPL-2.0-or-later
3 # <pep8 compliant>
5 # Script copyright (C) Blender Foundation 2012
7 import bpy
8 import bmesh
10 if bpy.app.background:
11 def _redraw_yasiamevil():
12 pass
13 else:
14 def _redraw_yasiamevil():
15 _redraw_yasiamevil.opr(**_redraw_yasiamevil.arg)
16 _redraw_yasiamevil.opr = bpy.ops.wm.redraw_timer
17 _redraw_yasiamevil.arg = dict(type='DRAW_WIN_SWAP', iterations=1)
20 def _points_from_object(depsgraph, scene, obj, source):
22 _source_all = {
23 'PARTICLE_OWN', 'PARTICLE_CHILD',
24 'PENCIL',
25 'VERT_OWN', 'VERT_CHILD',
28 # print(source - _source_all)
29 # print(source)
30 assert(len(source | _source_all) == len(_source_all))
31 assert(len(source))
33 points = []
35 def edge_center(mesh, edge):
36 v1, v2 = edge.vertices
37 return (mesh.vertices[v1].co + mesh.vertices[v2].co) / 2.0
39 def poly_center(mesh, poly):
40 from mathutils import Vector
41 co = Vector()
42 tot = 0
43 for i in poly.loop_indices:
44 co += mesh.vertices[mesh.loops[i].vertex_index].co
45 tot += 1
46 return co / tot
48 def points_from_verts(obj):
49 """Takes points from _any_ object with geometry"""
50 if obj.type == 'MESH':
51 mesh = obj.data
52 matrix = obj.matrix_world.copy()
53 points.extend([matrix @ v.co for v in mesh.vertices])
54 else:
55 ob_eval = ob.evaluated_get(depsgraph)
56 try:
57 mesh = ob_eval.to_mesh()
58 except:
59 mesh = None
61 if mesh is not None:
62 matrix = obj.matrix_world.copy()
63 points.extend([matrix @ v.co for v in mesh.vertices])
64 ob_eval.to_mesh_clear()
66 def points_from_particles(obj):
67 obj_eval = obj.evaluated_get(depsgraph)
68 points.extend([p.location.copy()
69 for psys in obj_eval.particle_systems
70 for p in psys.particles])
72 # geom own
73 if 'VERT_OWN' in source:
74 points_from_verts(obj)
76 # geom children
77 if 'VERT_CHILD' in source:
78 for obj_child in obj.children:
79 points_from_verts(obj_child)
81 # geom particles
82 if 'PARTICLE_OWN' in source:
83 points_from_particles(obj)
85 if 'PARTICLE_CHILD' in source:
86 for obj_child in obj.children:
87 points_from_particles(obj_child)
89 # grease pencil
90 def get_points(stroke):
91 return [point.co.copy() for point in stroke.points]
93 def get_splines(gp):
94 if gp.layers.active:
95 frame = gp.layers.active.active_frame
96 return [get_points(stroke) for stroke in frame.strokes]
97 else:
98 return []
100 if 'PENCIL' in source:
101 # Used to be from object in 2.7x, now from scene.
102 gp = scene.grease_pencil
103 if gp:
104 points.extend([p for spline in get_splines(gp) for p in spline])
106 print("Found %d points" % len(points))
108 return points
111 def cell_fracture_objects(
112 context, collection, obj,
113 source={'PARTICLE_OWN'},
114 source_limit=0,
115 source_noise=0.0,
116 clean=True,
117 # operator options
118 use_smooth_faces=False,
119 use_data_match=False,
120 use_debug_points=False,
121 margin=0.0,
122 material_index=0,
123 use_debug_redraw=False,
124 cell_scale=(1.0, 1.0, 1.0),
126 from . import fracture_cell_calc
127 depsgraph = context.evaluated_depsgraph_get()
128 scene = context.scene
129 view_layer = context.view_layer
131 # -------------------------------------------------------------------------
132 # GET POINTS
134 points = _points_from_object(depsgraph, scene, obj, source)
136 if not points:
137 # print using fallback
138 points = _points_from_object(depsgraph, scene, obj, {'VERT_OWN'})
140 if not points:
141 print("no points found")
142 return []
144 # apply optional clamp
145 if source_limit != 0 and source_limit < len(points):
146 import random
147 random.shuffle(points)
148 points[source_limit:] = []
150 # saddly we cant be sure there are no doubles
151 from mathutils import Vector
152 to_tuple = Vector.to_tuple
153 points = list({to_tuple(p, 4): p for p in points}.values())
154 del to_tuple
155 del Vector
157 # end remove doubles
158 # ------------------
160 if source_noise > 0.0:
161 from random import random
162 # boundbox approx of overall scale
163 from mathutils import Vector
164 matrix = obj.matrix_world.copy()
165 bb_world = [matrix @ Vector(v) for v in obj.bound_box]
166 scalar = source_noise * ((bb_world[0] - bb_world[6]).length / 2.0)
168 from mathutils.noise import random_unit_vector
170 points[:] = [p + (random_unit_vector() * (scalar * random())) for p in points]
172 if use_debug_points:
173 bm = bmesh.new()
174 for p in points:
175 bm.verts.new(p)
176 mesh_tmp = bpy.data.meshes.new(name="DebugPoints")
177 bm.to_mesh(mesh_tmp)
178 bm.free()
179 obj_tmp = bpy.data.objects.new(name=mesh_tmp.name, object_data=mesh_tmp)
180 collection.objects.link(obj_tmp)
181 del obj_tmp, mesh_tmp
183 mesh = obj.data
184 matrix = obj.matrix_world.copy()
185 verts = [matrix @ v.co for v in mesh.vertices]
187 cells = fracture_cell_calc.points_as_bmesh_cells(
188 verts,
189 points,
190 cell_scale,
191 margin_cell=margin,
194 # some hacks here :S
195 cell_name = obj.name + "_cell"
197 objects = []
199 for center_point, cell_points in cells:
201 # ---------------------------------------------------------------------
202 # BMESH
204 # create the convex hulls
205 bm = bmesh.new()
207 # WORKAROUND FOR CONVEX HULL BUG/LIMIT
208 # XXX small noise
209 import random
211 def R():
212 return (random.random() - 0.5) * 0.001
213 # XXX small noise
215 for i, co in enumerate(cell_points):
217 # XXX small noise
218 co.x += R()
219 co.y += R()
220 co.z += R()
221 # XXX small noise
223 bm_vert = bm.verts.new(co)
225 import mathutils
226 bmesh.ops.remove_doubles(bm, verts=bm.verts, dist=0.005)
227 try:
228 bmesh.ops.convex_hull(bm, input=bm.verts)
229 except RuntimeError:
230 import traceback
231 traceback.print_exc()
233 if clean:
234 bm.normal_update()
235 try:
236 bmesh.ops.dissolve_limit(bm, verts=bm.verts, angle_limit=0.001)
237 except RuntimeError:
238 import traceback
239 traceback.print_exc()
240 # Smooth faces will remain only inner faces, after applying boolean modifier.
241 if use_smooth_faces:
242 for bm_face in bm.faces:
243 bm_face.smooth = True
245 if material_index != 0:
246 for bm_face in bm.faces:
247 bm_face.material_index = material_index
249 # ---------------------------------------------------------------------
250 # MESH
251 mesh_dst = bpy.data.meshes.new(name=cell_name)
253 bm.to_mesh(mesh_dst)
254 bm.free()
255 del bm
257 if use_data_match:
258 # match materials and data layers so boolean displays them
259 # currently only materials + data layers, could do others...
260 mesh_src = obj.data
261 for mat in mesh_src.materials:
262 mesh_dst.materials.append(mat)
263 for lay_attr in ("vertex_colors", "uv_layers"):
264 lay_src = getattr(mesh_src, lay_attr)
265 lay_dst = getattr(mesh_dst, lay_attr)
266 for key in lay_src.keys():
267 lay_dst.new(name=key)
269 # ---------------------------------------------------------------------
270 # OBJECT
272 obj_cell = bpy.data.objects.new(name=cell_name, object_data=mesh_dst)
273 collection.objects.link(obj_cell)
274 # scene.objects.active = obj_cell
275 obj_cell.location = center_point
277 objects.append(obj_cell)
279 # support for object materials
280 if use_data_match:
281 for i in range(len(mesh_dst.materials)):
282 slot_src = obj.material_slots[i]
283 slot_dst = obj_cell.material_slots[i]
285 slot_dst.link = slot_src.link
286 slot_dst.material = slot_src.material
288 if use_debug_redraw:
289 view_layer.update()
290 _redraw_yasiamevil()
292 view_layer.update()
294 return objects
297 def cell_fracture_boolean(
298 context, collection, obj, objects,
299 use_debug_bool=False,
300 clean=True,
301 use_island_split=False,
302 use_interior_hide=False,
303 use_debug_redraw=False,
304 level=0,
305 remove_doubles=True
308 objects_boolean = []
309 scene = context.scene
310 view_layer = context.view_layer
312 if use_interior_hide and level == 0:
313 # only set for level 0
314 obj.data.polygons.foreach_set("hide", [False] * len(obj.data.polygons))
316 for obj_cell in objects:
317 mod = obj_cell.modifiers.new(name="Boolean", type='BOOLEAN')
318 mod.object = obj
319 mod.operation = 'INTERSECT'
321 if not use_debug_bool:
323 if use_interior_hide:
324 obj_cell.data.polygons.foreach_set("hide", [True] * len(obj_cell.data.polygons))
326 # Calculates all booleans at once (faster).
327 depsgraph = context.evaluated_depsgraph_get()
329 for obj_cell in objects:
331 if not use_debug_bool:
333 obj_cell_eval = obj_cell.evaluated_get(depsgraph)
334 mesh_new = bpy.data.meshes.new_from_object(obj_cell_eval)
335 mesh_old = obj_cell.data
336 obj_cell.data = mesh_new
337 obj_cell.modifiers.remove(obj_cell.modifiers[-1])
339 # remove if not valid
340 if not mesh_old.users:
341 bpy.data.meshes.remove(mesh_old)
342 if not mesh_new.vertices:
343 collection.objects.unlink(obj_cell)
344 if not obj_cell.users:
345 bpy.data.objects.remove(obj_cell)
346 obj_cell = None
347 if not mesh_new.users:
348 bpy.data.meshes.remove(mesh_new)
349 mesh_new = None
351 # avoid unneeded bmesh re-conversion
352 if mesh_new is not None:
353 bm = None
355 if clean:
356 if bm is None: # ok this will always be true for now...
357 bm = bmesh.new()
358 bm.from_mesh(mesh_new)
359 bm.normal_update()
360 try:
361 bmesh.ops.dissolve_limit(bm, verts=bm.verts, edges=bm.edges, angle_limit=0.001)
362 except RuntimeError:
363 import traceback
364 traceback.print_exc()
366 if remove_doubles:
367 if bm is None:
368 bm = bmesh.new()
369 bm.from_mesh(mesh_new)
370 bmesh.ops.remove_doubles(bm, verts=bm.verts, dist=0.005)
372 if bm is not None:
373 bm.to_mesh(mesh_new)
374 bm.free()
376 del mesh_new
377 del mesh_old
379 if obj_cell is not None:
380 objects_boolean.append(obj_cell)
382 if use_debug_redraw:
383 _redraw_yasiamevil()
385 if (not use_debug_bool) and use_island_split:
386 # this is ugly and Im not proud of this - campbell
387 for ob in view_layer.objects:
388 ob.select_set(False)
389 for obj_cell in objects_boolean:
390 obj_cell.select_set(True)
392 bpy.ops.mesh.separate(type='LOOSE')
394 objects_boolean[:] = [obj_cell for obj_cell in view_layer.objects if obj_cell.select_get()]
396 context.view_layer.update()
398 return objects_boolean
401 def cell_fracture_interior_handle(
402 objects,
403 use_interior_vgroup=False,
404 use_sharp_edges=False,
405 use_sharp_edges_apply=False,
407 """Run after doing _all_ booleans"""
409 assert(use_interior_vgroup or use_sharp_edges or use_sharp_edges_apply)
411 for obj_cell in objects:
412 mesh = obj_cell.data
413 bm = bmesh.new()
414 bm.from_mesh(mesh)
416 if use_interior_vgroup:
417 for bm_vert in bm.verts:
418 bm_vert.tag = True
419 for bm_face in bm.faces:
420 if not bm_face.hide:
421 for bm_vert in bm_face.verts:
422 bm_vert.tag = False
424 # now add all vgroups
425 defvert_lay = bm.verts.layers.deform.verify()
426 for bm_vert in bm.verts:
427 if bm_vert.tag:
428 bm_vert[defvert_lay][0] = 1.0
430 # add a vgroup
431 obj_cell.vertex_groups.new(name="Interior")
433 if use_sharp_edges:
434 for bm_edge in bm.edges:
435 if len({bm_face.hide for bm_face in bm_edge.link_faces}) == 2:
436 bm_edge.smooth = False
438 if use_sharp_edges_apply:
439 edges = [edge for edge in bm.edges if edge.smooth is False]
440 if edges:
441 bm.normal_update()
442 bmesh.ops.split_edges(bm, edges=edges)
444 for bm_face in bm.faces:
445 bm_face.hide = False
447 bm.to_mesh(mesh)
448 bm.free()