diff --git a/object_fracture_voroni/__init__.py b/object_fracture_voroni/__init__.py index 0f78474afae429adf9866e18abe8a64cb2e8b8e9..a63eef41567a6aa80d710defba4bd0936d796553 100644 --- a/object_fracture_voroni/__init__.py +++ b/object_fracture_voroni/__init__.py @@ -31,48 +31,213 @@ bl_info = { "category": "Object"} -if "bpy" in locals(): - import imp - imp.reload(fracture_cell_setup) +#if "bpy" in locals(): +# import imp +# imp.reload(fracture_cell_setup) import bpy +from bpy.props import (StringProperty, + BoolProperty, + IntProperty, + FloatProperty, + EnumProperty) + from bpy.types import Operator -def main(context): +def main_object(scene, obj, level, **kw): + import random + + # pull out some args + kw_copy = kw.copy() + use_recenter = kw_copy.pop("use_recenter") + use_remove_original = kw_copy.pop("use_remove_original") + recursion = kw_copy.pop("recursion") + recursion_chance = kw_copy.pop("recursion_chance") + from . import fracture_cell_setup + + objects = fracture_cell_setup.cell_fracture_objects(scene, obj, **kw_copy) + objects = fracture_cell_setup.cell_fracture_boolean(scene, obj, objects) + + # todo, split islands. + + # must apply after boolean. + if use_recenter: + bpy.ops.object.origin_set({"selected_editable_objects": objects}, + type='ORIGIN_GEOMETRY', center='MEDIAN') + + if level < recursion: + objects_recursive = [] + for i in range(len(objects) - 1, -1, -1): # reverse loop + + if recursion_chance == 1.0 or recursion_chance < random.random(): + obj_cell = objects[i] + objects_recursive += main_object(scene, obj_cell, level + 1, **kw) + if use_remove_original: + scene.objects.unlink(obj_cell) + del objects[i] + objects.extend(objects_recursive) + + + # testing only! + obj.hide = True + return objects + + +def main(context, **kw): + import time + t = time.time() + scene = context.scene obj = context.active_object - objects = fracture_cell_setup.cell_fracture_objects(context, obj) - objects = fracture_cell_setup.cell_fracture_boolean(context, obj, objects) + objects = main_object(scene, obj, 0, **kw) bpy.ops.object.select_all(action='DESELECT') for obj_cell in objects: obj_cell.select = True + + print("Done! %d objects in %.4f sec" % (len(objects), time.time() - t)) class FractureCell(Operator): bl_idname = "object.add_fracture_cell_objects" bl_label = "Cell Fracture Helper Objects" + bl_options = {'PRESET'} + + # ------------------------------------------------------------------------- + # Source Options + source = EnumProperty( + name="Source", + items=(('VERT_OWN', "Own Verts", "Use own vertices"), + ('EDGE_OWN', "Own Edges", "Use own edges"), + ('FACE_OWN', "Own Faces", "Use own faces"), + ('VERT_CHILD', "Child Verts", "Use own vertices"), + ('EDGE_CHILD', "Child Edges", "Use own edges"), + ('FACE_CHILD', "Child Faces", "Use own faces"), + ('PARTICLE', "Particles", ("All particle systems of the " + "source object")), + ('PENCIL', "Grease Pencil", "This objects grease pencil"), + ), + options={'ENUM_FLAG'}, + default={'PARTICLE', 'VERT_OWN'} # 'VERT_OWN', 'EDGE_OWN', 'FACE_OWN' + ) + + source_limit = IntProperty( + name="Source Limit", + description="Limit the number of input points, 0 for unlimited", + min=0, max=5000, + default=1000, + ) + + source_noise = FloatProperty( + name="Noise", + description="Randomize point distrobution", + min=0.0, max=1.0, + default=0.0, + ) + + # ------------------------------------------------------------------------- + # Mesh Data Options + + use_smooth_faces = BoolProperty( + name="Smooth Faces", + default=False, + ) + + use_smooth_edges = BoolProperty( + name="Smooth Edges", + description="Set sharp edges whem disabled", + default=True, + ) + + use_data_match = BoolProperty( + name="Match Data", + description="Match original mesh materials and data layers", + default=True, + ) + + use_island_split = BoolProperty( + name="Split Islands", + description="Split disconnected meshes", + default=True, + ) + + # ------------------------------------------------------------------------- + # Object Options + + use_recenter = BoolProperty( + name="Recenter", + description="Recalculate the center points after splitting", + default=True, + ) + + use_remove_original = BoolProperty( + name="Remove Original", + description="Removes the parents used to create the shatter", + default=True, + ) + + # ------------------------------------------------------------------------- + # Recursion + + recursion = IntProperty( + name="Recursion", + description="Break shards resursively", + min=0, max=5000, + default=0, + ) + + recursion_chance = FloatProperty( + name="Random Factor", + description="Likelyhood of recursion", + min=0.0, max=1.0, + default=1.0, + ) def execute(self, context): - main(context) + keywords = self.as_keywords() # ignore=("blah",) + + main(context, **keywords) + return {'FINISHED'} -''' -class INFO_MT_add_fracture_objects(bpy.types.Menu): - bl_idname = "INFO_MT_add_fracture_objects" - bl_label = "Fracture Helper Objects" + + def invoke(self, context, event): + wm = context.window_manager + return wm.invoke_props_dialog(self, width=600) def draw(self, context): layout = self.layout - layout.operator_context = 'INVOKE_REGION_WIN' - - layout.operator("object.import_fracture_bomb", - text="Bomb") - layout.operator("object.import_fracture_projectile", - text="Projectile") - layout.operator("object.import_fracture_recorder", - text="Rigidbody Recorder") -''' + box = layout.box() + col = box.column() + col.label("Point Source") + rowsub = col.row() + rowsub.prop(self, "source") + rowsub = col.row() + rowsub.prop(self, "source_limit") + rowsub.prop(self, "source_noise") + rowsub = col.row() + + box = layout.box() + col = box.column() + col.label("Mesh Data") + rowsub = col.row(align=True) + rowsub.prop(self, "use_smooth_faces") + rowsub.prop(self, "use_smooth_edges") + rowsub.prop(self, "use_data_match") + # rowsub.prop(self, "use_island_split") # TODO + + box = layout.box() + col = box.column() + col.label("Object") + rowsub = col.row(align=True) + rowsub.prop(self, "use_recenter") + + box = layout.box() + col = box.column() + col.label("Recursive Shatter") + rowsub = col.row(align=True) + rowsub.prop(self, "recursion") + rowsub.prop(self, "recursion_chance") #def menu_func(self, context): # self.layout.menu("INFO_MT_add_fracture_objects", icon="PLUGIN") diff --git a/object_fracture_voroni/fracture_cell_calc.py b/object_fracture_voroni/fracture_cell_calc.py index 3b5d6da8dcc2823d4503eb9bd2788166247235f3..e25a0ee3951c2c3b823005ec93770f99a00cb89c 100644 --- a/object_fracture_voroni/fracture_cell_calc.py +++ b/object_fracture_voroni/fracture_cell_calc.py @@ -27,7 +27,7 @@ def points_as_bmesh_cells(verts, points, margin=0.01): cells = [] - sortedVoronoiPoints = [p for p in points] + points_sorted_current = [p for p in points] plane_indices = [] vertices = [] @@ -37,7 +37,7 @@ def points_as_bmesh_cells(verts, points, margin=0.01): xa = [v[0] for v in verts] ya = [v[1] for v in verts] za = [v[2] for v in verts] - + xmin, xmax = min(xa) - margin, max(xa) + margin ymin, ymax = min(ya) - margin, max(ya) + margin zmin, zmax = min(za) - margin, max(za) + margin @@ -50,19 +50,19 @@ def points_as_bmesh_cells(verts, points, margin=0.01): Vector((0.0, 0.0, -1.0, -abs(zmin))), ] - for i, curVoronoiPoint in enumerate(points): + for i, point_cell_current in enumerate(points): planes = [None] * len(convexPlanes) for j in range(len(convexPlanes)): planes[j] = convexPlanes[j].copy() - planes[j][3] += planes[j].xyz.dot(curVoronoiPoint) - maxDistance = 10000000000.0 # a big value! + planes[j][3] += planes[j].xyz.dot(point_cell_current) + distance_max = 10000000000.0 # a big value! - sortedVoronoiPoints.sort(key=lambda p: (p - curVoronoiPoint).length_squared) + points_sorted_current.sort(key=lambda p: (p - point_cell_current).length_squared) for j in range(1, len(points)): - normal = sortedVoronoiPoints[j] - curVoronoiPoint + normal = points_sorted_current[j] - point_cell_current nlength = normal.length - if nlength > maxDistance: + if nlength > distance_max: break plane = normal.normalized() @@ -77,17 +77,17 @@ def points_as_bmesh_cells(verts, points, margin=0.01): if len(plane_indices) != len(planes): planes[:] = [planes[k] for k in plane_indices] - maxDistance = vertices[0].length + distance_max = vertices[0].length for k in range(1, len(vertices)): distance = vertices[k].length - if maxDistance < distance: - maxDistance = distance - maxDistance *= 2.0 + if distance_max < distance: + distance_max = distance + distance_max *= 2.0 if len(vertices) == 0: continue - cells.append((curVoronoiPoint, vertices[:])) + cells.append((point_cell_current, vertices[:])) vertices[:] = [] return cells diff --git a/object_fracture_voroni/fracture_cell_setup.py b/object_fracture_voroni/fracture_cell_setup.py index de3e1210470a7a8698a7402feeee73a76a648f31..631f11e3411bd01b413ee9c7dc3d6065f10f67fc 100644 --- a/object_fracture_voroni/fracture_cell_setup.py +++ b/object_fracture_voroni/fracture_cell_setup.py @@ -23,30 +23,152 @@ import bpy import bmesh +def _points_from_object(obj, source): -def cell_fracture_objects(context, obj, method={'PARTICLES'}, clean=True): + _source_all = { + 'PARTICLE', 'PENCIL', + 'VERT_OWN', 'EDGE_OWN', 'FACE_OWN', + 'VERT_CHILD', 'EDGE_CHILD', 'FACE_CHILD'} - #assert(method in {'OTHER', 'PARTICLES'}) - - from . import fracture_cell_calc + print(source - _source_all) + print(source) + assert(len(source | _source_all) == len(_source_all)) + assert(len(source)) points = [] - if 'PARTICLES' in method: + 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): + if obj.type == 'MESH': + mesh = obj.data + matrix = obj.matrix_world.copy() + points.extend([matrix * v.co for v in mesh.vertices]) + + def points_from_edges(obj): + if obj.type == 'MESH': + mesh = obj.data + matrix = obj.matrix_world.copy() + points.extend([matrix * edge_center(mesh, e) for e in mesh.edges]) + + def points_from_faces(obj): + if obj.type == 'MESH': + mesh = obj.data + matrix = obj.matrix_world.copy() + points.extend([matrix * poly_center(mesh, p) for p in mesh.polygons]) + + # geom own + if 'VERT_OWN' in source: + points_from_verts(obj) + if 'EDGE_OWN' in source: + points_from_edges(obj) + if 'FACE_OWN' in source: + points_from_faces(obj) + + # geom children + if 'VERT_CHILD' in source: + for obj_child in obj.children: + points_from_verts(obj_child) + if 'EDGE_CHILD' in source: + for obj_child in obj.children: + points_from_edges(obj_child) + if 'FACE_CHILD' in source: + for obj_child in obj.children: + points_from_faces(obj_child) + + # geom particles + if 'PARTICLE' in source: points.extend([p.location.copy() for psys in obj.particle_systems for p in psys.particles]) - if 'OTHER' in method: - for obj_other in context.selected_objects: - if obj_other.type == 'MESH': - mesh = obj_other.data - matrix = obj_other.matrix_world.copy() - points.extend([matrix * v.co for v in mesh.vertices]) + # 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: + gp = obj.grease_pencil + if gp: + points.extend([p for spline in get_splines(gp) + for p in spline]) + + return points + + +def cell_fracture_objects(scene, obj, + source={'PARTICLE'}, + source_limit=0, + source_noise=0.0, + clean=True, + # operator options + use_smooth_faces=False, + use_smooth_edges=True, + use_data_match=False, + use_island_split=False, + ): + + from . import fracture_cell_calc + + # ------------------------------------------------------------------------- + # GET POINTS + + points = _points_from_object(obj, source) if not points: + # print using fallback + points = _points_from_object(obj, source | {'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:] = [] + + + # saddly we cant 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 + + + if source_noise > 0.0: + # 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 = (bb_world[0] - bb_world[6]).length / 2.0 + + from mathutils.noise import noise_vector + + points[:] = [p + (noise_vector(p) * scalar) for p in points] + + # end remove doubles + # ------------------ + mesh = obj.data matrix = obj.matrix_world.copy() verts = [matrix * v.co for v in mesh.vertices] @@ -54,17 +176,14 @@ def cell_fracture_objects(context, obj, method={'PARTICLES'}, clean=True): cells = fracture_cell_calc.points_as_bmesh_cells(verts, points) # some hacks here :S - scene = context.scene cell_name = obj.name + "_cell" objects = [] for center_point, cell_points in cells: - mesh = bpy.data.meshes.new(name=cell_name) - obj_cell = bpy.data.objects.new(name=cell_name, object_data=mesh) - scene.objects.link(obj_cell) - # scene.objects.active = obj_cell - obj_cell.location = center_point + + # --------------------------------------------------------------------- + # BMESH # create the convex hulls bm = bmesh.new() @@ -73,10 +192,12 @@ def cell_fracture_objects(context, obj, method={'PARTICLES'}, clean=True): bm_vert.tag = True import mathutils - bm.transform(mathutils.Matrix.Translation((+100.0, +100.0, +100.0))) # BUG IN BLENDER bmesh.ops.remove_doubles(bm, {'TAG'}, 0.0001) - bmesh.ops.convex_hull(bm, {'TAG'}) - bm.transform(mathutils.Matrix.Translation((-100.0, -100.0, -100.0))) # BUG IN BLENDER + try: + bmesh.ops.convex_hull(bm, {'TAG'}) + except RuntimeError: + import traceback + traceback.print_exc() if clean: for bm_vert in bm.verts: @@ -84,20 +205,66 @@ def cell_fracture_objects(context, obj, method={'PARTICLES'}, clean=True): for bm_edge in bm.edges: bm_edge.tag = True bm.normal_update() - bmesh.ops.dissolve_limit(bm, {'TAG'}, {'TAG'}, 0.001) + try: + bmesh.ops.dissolve_limit(bm, {'TAG'}, {'TAG'}, 0.001) + except RuntimeError: + import traceback + traceback.print_exc() + + if use_smooth_faces: + for bm_face in bm.faces: + bm_face.smooth = True + + if use_smooth_edges: + for bm_edge in bm.edges: + bm_edge.smooth = True + + + # --------------------------------------------------------------------- + # MESH - bm.to_mesh(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.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) + scene.objects.link(obj_cell) + # scene.objects.active = obj_cell + obj_cell.location = center_point objects.append(obj_cell) - + scene.update() + + # move this elsewhere... + for obj_cell in objects: + game = obj_cell.game + game.physics_type = 'RIGID_BODY' + game.use_collision_bounds = True + game.collision_bounds_type = 'TRIANGLE_MESH' + return objects -def cell_fracture_boolean(context, obj, objects, apply=True, clean=True): - scene = context.scene +def cell_fracture_boolean(scene, obj, objects, apply=True, clean=True): objects_boolean = [] @@ -107,7 +274,9 @@ def cell_fracture_boolean(context, obj, objects, apply=True, clean=True): mod.operation = 'INTERSECT' if apply: - mesh_new = obj_cell.to_mesh(scene, apply_modifiers=True, settings='PREVIEW') + mesh_new = obj_cell.to_mesh(scene, + apply_modifiers=True, + settings='PREVIEW') mesh_old = obj_cell.data obj_cell.data = mesh_new obj_cell.modifiers.remove(mod) @@ -119,10 +288,12 @@ def cell_fracture_boolean(context, obj, objects, apply=True, clean=True): scene.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 - if clean: + if clean and mesh_new is not None: bm = bmesh.new() bm.from_mesh(mesh_new) for bm_vert in bm.verts: @@ -130,9 +301,14 @@ def cell_fracture_boolean(context, obj, objects, apply=True, clean=True): for bm_edge in bm.edges: bm_edge.tag = True bm.normal_update() - bmesh.ops.dissolve_limit(bm, {'TAG'}, {'TAG'}, 0.01) + try: + bmesh.ops.dissolve_limit(bm, {'TAG'}, {'TAG'}, 0.001) + except RuntimeError: + import traceback + traceback.print_exc() bm.to_mesh(mesh_new) bm.free() - objects_boolean.append(obj_cell) + if obj_cell is not None: + objects_boolean.append(obj_cell) return objects_boolean