# SPDX-License-Identifier: GPL-2.0-or-later # Script copyright (C) Blender Foundation 2012 import bpy import bmesh if bpy.app.background: def _redraw_yasiamevil(): pass else: def _redraw_yasiamevil(): _redraw_yasiamevil.opr(**_redraw_yasiamevil.arg) _redraw_yasiamevil.opr = bpy.ops.wm.redraw_timer _redraw_yasiamevil.arg = dict(type='DRAW_WIN_SWAP', iterations=1) def _points_from_object(depsgraph, scene, obj, source): _source_all = { 'PARTICLE_OWN', 'PARTICLE_CHILD', 'PENCIL', 'VERT_OWN', 'VERT_CHILD', } # print(source - _source_all) # print(source) assert(len(source | _source_all) == len(_source_all)) assert(len(source)) points = [] def edge_center(mesh, edge): v1, v2 = edge.vertices return (mesh.vertices[v1].co + mesh.vertices[v2].co) / 2.0 def poly_center(mesh, poly): from mathutils import Vector co = Vector() tot = 0 for i in poly.loop_indices: co += mesh.vertices[mesh.loops[i].vertex_index].co tot += 1 return co / tot def points_from_verts(obj): """Takes points from _any_ object with geometry""" if obj.type == 'MESH': mesh = obj.data matrix = obj.matrix_world.copy() points.extend([matrix @ v.co for v in mesh.vertices]) else: ob_eval = ob.evaluated_get(depsgraph) try: mesh = ob_eval.to_mesh() except: mesh = None if mesh is not None: matrix = obj.matrix_world.copy() points.extend([matrix @ v.co for v in mesh.vertices]) ob_eval.to_mesh_clear() def points_from_particles(obj): obj_eval = obj.evaluated_get(depsgraph) points.extend([p.location.copy() for psys in obj_eval.particle_systems for p in psys.particles]) # geom own if 'VERT_OWN' in source: points_from_verts(obj) # geom children if 'VERT_CHILD' in source: for obj_child in obj.children: points_from_verts(obj_child) # geom particles if 'PARTICLE_OWN' in source: points_from_particles(obj) if 'PARTICLE_CHILD' in source: for obj_child in obj.children: points_from_particles(obj_child) # grease pencil def get_points(stroke): return [point.co.copy() for point in stroke.points] def get_splines(gp): if gp.layers.active: frame = gp.layers.active.active_frame return [get_points(stroke) for stroke in frame.strokes] else: return [] if 'PENCIL' in source: # Used to be from object in 2.7x, now from scene. gp = scene.grease_pencil if gp: points.extend([p for spline in get_splines(gp) for p in spline]) print("Found %d points" % len(points)) return points def cell_fracture_objects( context, collection, obj, source={'PARTICLE_OWN'}, source_limit=0, source_noise=0.0, clean=True, # operator options use_smooth_faces=False, use_data_match=False, use_debug_points=False, margin=0.0, material_index=0, use_debug_redraw=False, cell_scale=(1.0, 1.0, 1.0), ): from . import fracture_cell_calc depsgraph = context.evaluated_depsgraph_get() scene = context.scene view_layer = context.view_layer # ------------------------------------------------------------------------- # GET POINTS points = _points_from_object(depsgraph, scene, obj, source) if not points: # print using fallback points = _points_from_object(depsgraph, scene, obj, {'VERT_OWN'}) if not points: print("no points found") return [] # apply optional clamp if source_limit != 0 and source_limit < len(points): import random random.shuffle(points) points[source_limit:] = [] # sadly we can't be sure there are no doubles from mathutils import Vector to_tuple = Vector.to_tuple points = list({to_tuple(p, 4): p for p in points}.values()) del to_tuple del Vector # end remove doubles # ------------------ if source_noise > 0.0: from random import random # boundbox approx of overall scale from mathutils import Vector matrix = obj.matrix_world.copy() bb_world = [matrix @ Vector(v) for v in obj.bound_box] scalar = source_noise * ((bb_world[0] - bb_world[6]).length / 2.0) from mathutils.noise import random_unit_vector points[:] = [p + (random_unit_vector() * (scalar * random())) for p in points] if use_debug_points: bm = bmesh.new() for p in points: bm.verts.new(p) mesh_tmp = bpy.data.meshes.new(name="DebugPoints") bm.to_mesh(mesh_tmp) bm.free() obj_tmp = bpy.data.objects.new(name=mesh_tmp.name, object_data=mesh_tmp) collection.objects.link(obj_tmp) del obj_tmp, mesh_tmp mesh = obj.data matrix = obj.matrix_world.copy() verts = [matrix @ v.co for v in mesh.vertices] cells = fracture_cell_calc.points_as_bmesh_cells( verts, points, cell_scale, margin_cell=margin, ) # some hacks here :S cell_name = obj.name + "_cell" objects = [] for center_point, cell_points in cells: # --------------------------------------------------------------------- # BMESH # create the convex hulls bm = bmesh.new() # WORKAROUND FOR CONVEX HULL BUG/LIMIT # XXX small noise import random def R(): return (random.random() - 0.5) * 0.001 # XXX small noise for i, co in enumerate(cell_points): # XXX small noise co.x += R() co.y += R() co.z += R() # XXX small noise bm_vert = bm.verts.new(co) import mathutils bmesh.ops.remove_doubles(bm, verts=bm.verts, dist=0.005) try: bmesh.ops.convex_hull(bm, input=bm.verts) except RuntimeError: import traceback traceback.print_exc() if clean: bm.normal_update() try: bmesh.ops.dissolve_limit(bm, verts=bm.verts, angle_limit=0.001) except RuntimeError: import traceback traceback.print_exc() # Smooth faces will remain only inner faces, after applying boolean modifier. if use_smooth_faces: for bm_face in bm.faces: bm_face.smooth = True if material_index != 0: for bm_face in bm.faces: bm_face.material_index = material_index # --------------------------------------------------------------------- # MESH mesh_dst = bpy.data.meshes.new(name=cell_name) bm.to_mesh(mesh_dst) bm.free() del bm if use_data_match: # match materials and data layers so boolean displays them # currently only materials + data layers, could do others... mesh_src = obj.data for mat in mesh_src.materials: mesh_dst.materials.append(mat) for lay_attr in ("vertex_colors", "uv_layers"): lay_src = getattr(mesh_src, lay_attr) lay_dst = getattr(mesh_dst, lay_attr) for key in lay_src.keys(): lay_dst.new(name=key) # --------------------------------------------------------------------- # OBJECT obj_cell = bpy.data.objects.new(name=cell_name, object_data=mesh_dst) collection.objects.link(obj_cell) # scene.objects.active = obj_cell obj_cell.location = center_point objects.append(obj_cell) # support for object materials if use_data_match: for i in range(len(mesh_dst.materials)): slot_src = obj.material_slots[i] slot_dst = obj_cell.material_slots[i] slot_dst.link = slot_src.link slot_dst.material = slot_src.material if use_debug_redraw: view_layer.update() _redraw_yasiamevil() view_layer.update() return objects def cell_fracture_boolean( context, collection, obj, objects, use_debug_bool=False, clean=True, use_island_split=False, use_interior_hide=False, use_debug_redraw=False, level=0, remove_doubles=True ): objects_boolean = [] scene = context.scene view_layer = context.view_layer if use_interior_hide and level == 0: # only set for level 0 obj.data.polygons.foreach_set("hide", [False] * len(obj.data.polygons)) for obj_cell in objects: mod = obj_cell.modifiers.new(name="Boolean", type='BOOLEAN') mod.object = obj mod.operation = 'INTERSECT' if not use_debug_bool: if use_interior_hide: obj_cell.data.polygons.foreach_set("hide", [True] * len(obj_cell.data.polygons)) # Calculates all booleans at once (faster). depsgraph = context.evaluated_depsgraph_get() for obj_cell in objects: if not use_debug_bool: obj_cell_eval = obj_cell.evaluated_get(depsgraph) mesh_new = bpy.data.meshes.new_from_object(obj_cell_eval) mesh_old = obj_cell.data obj_cell.data = mesh_new obj_cell.modifiers.remove(obj_cell.modifiers[-1]) # remove if not valid if not mesh_old.users: bpy.data.meshes.remove(mesh_old) if not mesh_new.vertices: collection.objects.unlink(obj_cell) if not obj_cell.users: bpy.data.objects.remove(obj_cell) obj_cell = None if not mesh_new.users: bpy.data.meshes.remove(mesh_new) mesh_new = None # avoid unneeded bmesh re-conversion if mesh_new is not None: bm = None if clean: if bm is None: # ok this will always be true for now... bm = bmesh.new() bm.from_mesh(mesh_new) bm.normal_update() try: bmesh.ops.dissolve_limit(bm, verts=bm.verts, edges=bm.edges, angle_limit=0.001) except RuntimeError: import traceback traceback.print_exc() if remove_doubles: if bm is None: bm = bmesh.new() bm.from_mesh(mesh_new) bmesh.ops.remove_doubles(bm, verts=bm.verts, dist=0.005) if bm is not None: bm.to_mesh(mesh_new) bm.free() del mesh_new del mesh_old if obj_cell is not None: objects_boolean.append(obj_cell) if use_debug_redraw: _redraw_yasiamevil() if (not use_debug_bool) and use_island_split: # this is ugly and Im not proud of this - campbell for ob in view_layer.objects: ob.select_set(False) for obj_cell in objects_boolean: obj_cell.select_set(True) bpy.ops.mesh.separate(type='LOOSE') objects_boolean[:] = [obj_cell for obj_cell in view_layer.objects if obj_cell.select_get()] context.view_layer.update() return objects_boolean def cell_fracture_interior_handle( objects, use_interior_vgroup=False, use_sharp_edges=False, use_sharp_edges_apply=False, ): """Run after doing _all_ booleans""" assert(use_interior_vgroup or use_sharp_edges or use_sharp_edges_apply) for obj_cell in objects: mesh = obj_cell.data bm = bmesh.new() bm.from_mesh(mesh) if use_interior_vgroup: for bm_vert in bm.verts: bm_vert.tag = True for bm_face in bm.faces: if not bm_face.hide: for bm_vert in bm_face.verts: bm_vert.tag = False # now add all vgroups defvert_lay = bm.verts.layers.deform.verify() for bm_vert in bm.verts: if bm_vert.tag: bm_vert[defvert_lay][0] = 1.0 # add a vgroup obj_cell.vertex_groups.new(name="Interior") if use_sharp_edges: for bm_edge in bm.edges: if len({bm_face.hide for bm_face in bm_edge.link_faces}) == 2: bm_edge.smooth = False if use_sharp_edges_apply: edges = [edge for edge in bm.edges if edge.smooth is False] if edges: bm.normal_update() bmesh.ops.split_edges(bm, edges=edges) for bm_face in bm.faces: bm_face.hide = False bm.to_mesh(mesh) bm.free()