2025-12-01
This commit is contained in:
@@ -0,0 +1,525 @@
|
||||
# SPDX-FileCopyrightText: 2010-2022 Blender Foundation
|
||||
#
|
||||
# SPDX-License-Identifier: GPL-2.0-or-later
|
||||
|
||||
# if "bpy" in locals():
|
||||
# import importlib
|
||||
# importlib.reload(fracture_cell_setup)
|
||||
|
||||
import bpy
|
||||
from bpy.props import (
|
||||
StringProperty,
|
||||
BoolProperty,
|
||||
IntProperty,
|
||||
FloatProperty,
|
||||
FloatVectorProperty,
|
||||
EnumProperty,
|
||||
)
|
||||
|
||||
from bpy.types import Operator
|
||||
|
||||
|
||||
def main_object(context, collection, 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_source_limit = kw_copy.pop("recursion_source_limit")
|
||||
recursion_clamp = kw_copy.pop("recursion_clamp")
|
||||
recursion_chance = kw_copy.pop("recursion_chance")
|
||||
recursion_chance_select = kw_copy.pop("recursion_chance_select")
|
||||
use_island_split = kw_copy.pop("use_island_split")
|
||||
use_debug_bool = kw_copy.pop("use_debug_bool")
|
||||
use_interior_vgroup = kw_copy.pop("use_interior_vgroup")
|
||||
use_sharp_edges = kw_copy.pop("use_sharp_edges")
|
||||
use_sharp_edges_apply = kw_copy.pop("use_sharp_edges_apply")
|
||||
|
||||
scene = context.scene
|
||||
|
||||
if level != 0:
|
||||
kw_copy["source_limit"] = recursion_source_limit
|
||||
|
||||
from . import fracture_cell_setup
|
||||
|
||||
# not essential but selection is visual distraction.
|
||||
obj.select_set(False)
|
||||
|
||||
if kw_copy["use_debug_redraw"]:
|
||||
obj_display_type_prev = obj.display_type
|
||||
obj.display_type = 'WIRE'
|
||||
|
||||
objects = fracture_cell_setup.cell_fracture_objects(context, collection, obj, **kw_copy)
|
||||
objects = fracture_cell_setup.cell_fracture_boolean(
|
||||
context, collection, obj, objects,
|
||||
use_island_split=use_island_split,
|
||||
use_interior_hide=(use_interior_vgroup or use_sharp_edges),
|
||||
use_debug_bool=use_debug_bool,
|
||||
use_debug_redraw=kw_copy["use_debug_redraw"],
|
||||
level=level,
|
||||
)
|
||||
|
||||
# must apply after boolean.
|
||||
if use_recenter:
|
||||
context_override = {"selected_editable_objects": objects}
|
||||
with bpy.context.temp_override(**context_override):
|
||||
bpy.ops.object.origin_set(type='ORIGIN_GEOMETRY', center='MEDIAN')
|
||||
|
||||
# ----------
|
||||
# Recursion
|
||||
if level == 0:
|
||||
for level_sub in range(1, recursion + 1):
|
||||
|
||||
objects_recurse_input = [(i, o) for i, o in enumerate(objects)]
|
||||
|
||||
if recursion_chance != 1.0:
|
||||
from mathutils import Vector
|
||||
if recursion_chance_select == 'RANDOM':
|
||||
random.shuffle(objects_recurse_input)
|
||||
elif recursion_chance_select in {'SIZE_MIN', 'SIZE_MAX'}:
|
||||
objects_recurse_input.sort(key=lambda ob_pair: (
|
||||
Vector(ob_pair[1].bound_box[0]) -
|
||||
Vector(ob_pair[1].bound_box[6])
|
||||
).length_squared)
|
||||
if recursion_chance_select == 'SIZE_MAX':
|
||||
objects_recurse_input.reverse()
|
||||
elif recursion_chance_select in {'CURSOR_MIN', 'CURSOR_MAX'}:
|
||||
c = scene.cursor.location.copy()
|
||||
objects_recurse_input.sort(key=lambda ob_pair: (ob_pair[1].location - c).length_squared)
|
||||
if recursion_chance_select == 'CURSOR_MAX':
|
||||
objects_recurse_input.reverse()
|
||||
|
||||
objects_recurse_input[int(recursion_chance * len(objects_recurse_input)):] = []
|
||||
objects_recurse_input.sort()
|
||||
|
||||
# reverse index values so we can remove from original list.
|
||||
objects_recurse_input.reverse()
|
||||
|
||||
objects_recursive = []
|
||||
for i, obj_cell in objects_recurse_input:
|
||||
assert(objects[i] is obj_cell)
|
||||
objects_recursive += main_object(context, collection, obj_cell, level_sub, **kw)
|
||||
if use_remove_original:
|
||||
collection.objects.unlink(obj_cell)
|
||||
del objects[i]
|
||||
if recursion_clamp and len(objects) + len(objects_recursive) >= recursion_clamp:
|
||||
break
|
||||
objects.extend(objects_recursive)
|
||||
|
||||
if recursion_clamp and len(objects) > recursion_clamp:
|
||||
break
|
||||
|
||||
# --------------
|
||||
# Level Options
|
||||
if level == 0:
|
||||
# import pdb; pdb.set_trace()
|
||||
if use_interior_vgroup or use_sharp_edges:
|
||||
fracture_cell_setup.cell_fracture_interior_handle(
|
||||
objects,
|
||||
use_interior_vgroup=use_interior_vgroup,
|
||||
use_sharp_edges=use_sharp_edges,
|
||||
use_sharp_edges_apply=use_sharp_edges_apply,
|
||||
)
|
||||
|
||||
if kw_copy["use_debug_redraw"]:
|
||||
obj.display_type = obj_display_type_prev
|
||||
|
||||
# testing only!
|
||||
# obj.hide = True
|
||||
return objects
|
||||
|
||||
|
||||
def main(context, **kw):
|
||||
import time
|
||||
t = time.time()
|
||||
objects_context = context.selected_editable_objects
|
||||
|
||||
kw_copy = kw.copy()
|
||||
|
||||
# mass
|
||||
mass_mode = kw_copy.pop("mass_mode")
|
||||
mass = kw_copy.pop("mass")
|
||||
collection_name = kw_copy.pop("collection_name")
|
||||
|
||||
collection = context.collection
|
||||
if collection_name:
|
||||
collection_current = collection
|
||||
collection = bpy.data.collections.get(collection_name)
|
||||
if collection is None:
|
||||
collection = bpy.data.collections.new(collection_name)
|
||||
collection_current.children.link(collection)
|
||||
del collection_current
|
||||
|
||||
objects = []
|
||||
for obj in objects_context:
|
||||
if obj.type == 'MESH':
|
||||
objects += main_object(context, collection, obj, 0, **kw_copy)
|
||||
|
||||
bpy.ops.object.select_all(action='DESELECT')
|
||||
for obj_cell in objects:
|
||||
obj_cell.select_set(True)
|
||||
|
||||
# FIXME(campbell): we should be able to initialize rigid-body data.
|
||||
if mass_mode == 'UNIFORM':
|
||||
for obj_cell in objects:
|
||||
rb = obj_cell.rigid_body
|
||||
if rb is not None:
|
||||
rb.mass = mass
|
||||
elif mass_mode == 'VOLUME':
|
||||
from mathutils import Vector
|
||||
|
||||
def _get_volume(obj_cell):
|
||||
def _getObjectBBMinMax():
|
||||
min_co = Vector((1000000.0, 1000000.0, 1000000.0))
|
||||
max_co = -min_co
|
||||
matrix = obj_cell.matrix_world.copy()
|
||||
for i in range(0, 8):
|
||||
bb_vec = matrix @ Vector(obj_cell.bound_box[i])
|
||||
min_co[0] = min(bb_vec[0], min_co[0])
|
||||
min_co[1] = min(bb_vec[1], min_co[1])
|
||||
min_co[2] = min(bb_vec[2], min_co[2])
|
||||
max_co[0] = max(bb_vec[0], max_co[0])
|
||||
max_co[1] = max(bb_vec[1], max_co[1])
|
||||
max_co[2] = max(bb_vec[2], max_co[2])
|
||||
return (min_co, max_co)
|
||||
|
||||
def _getObjectVolume():
|
||||
min_co, max_co = _getObjectBBMinMax()
|
||||
x = max_co[0] - min_co[0]
|
||||
y = max_co[1] - min_co[1]
|
||||
z = max_co[2] - min_co[2]
|
||||
volume = x * y * z
|
||||
return volume
|
||||
|
||||
return _getObjectVolume()
|
||||
|
||||
obj_volume_ls = [_get_volume(obj_cell) for obj_cell in objects]
|
||||
obj_volume_tot = sum(obj_volume_ls)
|
||||
if obj_volume_tot > 0.0:
|
||||
mass_fac = mass / obj_volume_tot
|
||||
for i, obj_cell in enumerate(objects):
|
||||
rb = obj_cell.rigid_body
|
||||
if rb is not None:
|
||||
rb.mass = obj_volume_ls[i] * mass_fac
|
||||
else:
|
||||
assert(0)
|
||||
|
||||
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 selected mesh objects"
|
||||
bl_options = {'PRESET', 'UNDO'}
|
||||
|
||||
# -------------------------------------------------------------------------
|
||||
# Source Options
|
||||
source: EnumProperty(
|
||||
name="Source",
|
||||
items=(
|
||||
('VERT_OWN', "Own Verts", "Use own vertices"),
|
||||
('VERT_CHILD', "Child Verts", "Use child object vertices"),
|
||||
('PARTICLE_OWN', "Own Particles", (
|
||||
"All particle systems of the "
|
||||
"source object"
|
||||
)),
|
||||
('PARTICLE_CHILD', "Child Particles", (
|
||||
"All particle systems of the "
|
||||
"child objects"
|
||||
)),
|
||||
('PENCIL', "Annotation Pencil", "Annotation Grease Pencil."),
|
||||
),
|
||||
options={'ENUM_FLAG'},
|
||||
default={'PARTICLE_OWN'},
|
||||
)
|
||||
|
||||
source_limit: IntProperty(
|
||||
name="Source Limit",
|
||||
description="Limit the number of input points, 0 for unlimited",
|
||||
min=0, max=5000,
|
||||
default=100,
|
||||
)
|
||||
|
||||
source_noise: FloatProperty(
|
||||
name="Noise",
|
||||
description="Randomize point distribution",
|
||||
min=0.0, max=1.0,
|
||||
default=0.0,
|
||||
)
|
||||
|
||||
cell_scale: FloatVectorProperty(
|
||||
name="Scale",
|
||||
description="Scale Cell Shape",
|
||||
size=3,
|
||||
min=0.0, max=1.0,
|
||||
default=(1.0, 1.0, 1.0),
|
||||
)
|
||||
|
||||
# -------------------------------------------------------------------------
|
||||
# Recursion
|
||||
|
||||
recursion: IntProperty(
|
||||
name="Recursion",
|
||||
description="Break shards recursively",
|
||||
min=0, max=5000,
|
||||
default=0,
|
||||
)
|
||||
|
||||
recursion_source_limit: IntProperty(
|
||||
name="Source Limit",
|
||||
description="Limit the number of input points, 0 for unlimited (applies to recursion only)",
|
||||
min=0, max=5000,
|
||||
default=8,
|
||||
)
|
||||
|
||||
recursion_clamp: IntProperty(
|
||||
name="Clamp Recursion",
|
||||
description=(
|
||||
"Finish recursion when this number of objects is reached "
|
||||
"(prevents recursing for extended periods of time), zero disables"
|
||||
),
|
||||
min=0, max=10000,
|
||||
default=250,
|
||||
)
|
||||
|
||||
recursion_chance: FloatProperty(
|
||||
name="Random Factor",
|
||||
description="Likelihood of recursion",
|
||||
min=0.0, max=1.0,
|
||||
default=0.25,
|
||||
)
|
||||
|
||||
recursion_chance_select: EnumProperty(
|
||||
name="Recurse Over",
|
||||
items=(
|
||||
('RANDOM', "Random", ""),
|
||||
('SIZE_MIN', "Small", "Recursively subdivide smaller objects"),
|
||||
('SIZE_MAX', "Big", "Recursively subdivide bigger objects"),
|
||||
('CURSOR_MIN', "Cursor Close", "Recursively subdivide objects closer to the cursor"),
|
||||
('CURSOR_MAX', "Cursor Far", "Recursively subdivide objects farther from the cursor"),
|
||||
),
|
||||
default='SIZE_MIN',
|
||||
)
|
||||
|
||||
# -------------------------------------------------------------------------
|
||||
# Mesh Data Options
|
||||
|
||||
use_smooth_faces: BoolProperty(
|
||||
name="Smooth Interior",
|
||||
description="Smooth Faces of inner side",
|
||||
default=False,
|
||||
)
|
||||
|
||||
use_sharp_edges: BoolProperty(
|
||||
name="Sharp Edges",
|
||||
description="Set sharp edges when disabled",
|
||||
default=True,
|
||||
)
|
||||
|
||||
use_sharp_edges_apply: BoolProperty(
|
||||
name="Apply Split Edge",
|
||||
description="Split sharp hard edges",
|
||||
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,
|
||||
)
|
||||
|
||||
margin: FloatProperty(
|
||||
name="Margin",
|
||||
description="Gaps for the fracture (gives more stable physics)",
|
||||
min=0.0, max=1.0,
|
||||
default=0.001,
|
||||
)
|
||||
|
||||
material_index: IntProperty(
|
||||
name="Material",
|
||||
description="Material index for interior faces",
|
||||
default=0,
|
||||
)
|
||||
|
||||
use_interior_vgroup: BoolProperty(
|
||||
name="Interior VGroup",
|
||||
description="Create a vertex group for interior verts",
|
||||
default=False,
|
||||
)
|
||||
|
||||
# -------------------------------------------------------------------------
|
||||
# Physics Options
|
||||
|
||||
mass_mode: EnumProperty(
|
||||
name="Mass Mode",
|
||||
items=(
|
||||
('VOLUME', "Volume", "Objects get part of specified mass based on their volume"),
|
||||
('UNIFORM', "Uniform", "All objects get the specified mass"),
|
||||
),
|
||||
default='VOLUME',
|
||||
)
|
||||
|
||||
mass: FloatProperty(
|
||||
name="Mass",
|
||||
description="Mass to give created objects",
|
||||
min=0.001, max=1000.0,
|
||||
default=1.0,
|
||||
)
|
||||
|
||||
# -------------------------------------------------------------------------
|
||||
# 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,
|
||||
)
|
||||
|
||||
# -------------------------------------------------------------------------
|
||||
# Scene Options
|
||||
#
|
||||
# .. different from object options in that this controls how the objects
|
||||
# are setup in the scene.
|
||||
|
||||
collection_name: StringProperty(
|
||||
name="Collection",
|
||||
description=(
|
||||
"Create objects in a collection "
|
||||
"(use existing or create new)"
|
||||
),
|
||||
)
|
||||
|
||||
# -------------------------------------------------------------------------
|
||||
# Debug
|
||||
use_debug_points: BoolProperty(
|
||||
name="Debug Points",
|
||||
description="Create mesh data showing the points used for fracture",
|
||||
default=False,
|
||||
)
|
||||
|
||||
use_debug_redraw: BoolProperty(
|
||||
name="Show Progress Realtime",
|
||||
description="Redraw as fracture is done",
|
||||
default=True,
|
||||
)
|
||||
|
||||
use_debug_bool: BoolProperty(
|
||||
name="Debug Boolean",
|
||||
description="Skip applying the boolean modifier",
|
||||
default=False,
|
||||
)
|
||||
|
||||
def execute(self, context):
|
||||
keywords = self.as_keywords() # ignore=("blah",)
|
||||
|
||||
main(context, **keywords)
|
||||
|
||||
return {'FINISHED'}
|
||||
|
||||
def invoke(self, context, event):
|
||||
# print(self.recursion_chance_select)
|
||||
wm = context.window_manager
|
||||
return wm.invoke_props_dialog(self, width=600)
|
||||
|
||||
def draw(self, context):
|
||||
layout = self.layout
|
||||
box = layout.box()
|
||||
col = box.column()
|
||||
col.label(text="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()
|
||||
rowsub.prop(self, "cell_scale")
|
||||
|
||||
box = layout.box()
|
||||
col = box.column()
|
||||
col.label(text="Recursive Shatter")
|
||||
rowsub = col.row(align=True)
|
||||
rowsub.prop(self, "recursion")
|
||||
rowsub.prop(self, "recursion_source_limit")
|
||||
rowsub.prop(self, "recursion_clamp")
|
||||
rowsub = col.row()
|
||||
rowsub.prop(self, "recursion_chance")
|
||||
rowsub.prop(self, "recursion_chance_select", expand=True)
|
||||
|
||||
box = layout.box()
|
||||
col = box.column()
|
||||
col.label(text="Mesh Data")
|
||||
rowsub = col.row()
|
||||
rowsub.prop(self, "use_smooth_faces")
|
||||
rowsub.prop(self, "use_sharp_edges")
|
||||
rowsub.prop(self, "use_sharp_edges_apply")
|
||||
rowsub.prop(self, "use_data_match")
|
||||
rowsub = col.row()
|
||||
|
||||
# on same row for even layout but in fact are not all that related
|
||||
rowsub.prop(self, "material_index")
|
||||
rowsub.prop(self, "use_interior_vgroup")
|
||||
|
||||
# could be own section, control how we subdiv
|
||||
rowsub.prop(self, "margin")
|
||||
rowsub.prop(self, "use_island_split")
|
||||
|
||||
box = layout.box()
|
||||
col = box.column()
|
||||
col.label(text="Physics")
|
||||
rowsub = col.row(align=True)
|
||||
rowsub.prop(self, "mass_mode")
|
||||
rowsub.prop(self, "mass")
|
||||
|
||||
box = layout.box()
|
||||
col = box.column()
|
||||
col.label(text="Object")
|
||||
rowsub = col.row(align=True)
|
||||
rowsub.prop(self, "use_recenter")
|
||||
|
||||
box = layout.box()
|
||||
col = box.column()
|
||||
col.label(text="Scene")
|
||||
rowsub = col.row(align=True)
|
||||
rowsub.prop(self, "collection_name")
|
||||
|
||||
box = layout.box()
|
||||
col = box.column()
|
||||
col.label(text="Debug")
|
||||
rowsub = col.row(align=True)
|
||||
rowsub.prop(self, "use_debug_redraw")
|
||||
rowsub.prop(self, "use_debug_points")
|
||||
rowsub.prop(self, "use_debug_bool")
|
||||
|
||||
|
||||
def menu_func(self, context):
|
||||
layout = self.layout
|
||||
layout.separator()
|
||||
layout.operator("object.add_fracture_cell_objects", text="Cell Fracture")
|
||||
|
||||
|
||||
def register():
|
||||
bpy.utils.register_class(FractureCell)
|
||||
bpy.types.VIEW3D_MT_object_quick_effects.append(menu_func)
|
||||
|
||||
|
||||
def unregister():
|
||||
bpy.utils.unregister_class(FractureCell)
|
||||
bpy.types.VIEW3D_MT_object_quick_effects.remove(menu_func)
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
register()
|
||||
@@ -0,0 +1,16 @@
|
||||
schema_version = "1.0.0"
|
||||
id = "cell_fracture"
|
||||
name = "Cell Fracture"
|
||||
version = "0.2.1"
|
||||
tagline = "Fractured Object Creation"
|
||||
maintainer = "Community"
|
||||
type = "add-on"
|
||||
tags = ["Object"]
|
||||
blender_version_min = "4.2.0"
|
||||
license = ["SPDX:GPL-3.0-or-later"]
|
||||
website = "https://projects.blender.org/extensions/object_fracture_cell"
|
||||
copyright = [
|
||||
"2024 ideasman42",
|
||||
"2024 phymec",
|
||||
"2024 Sergey Sharybin",
|
||||
]
|
||||
@@ -0,0 +1,103 @@
|
||||
# SPDX-FileCopyrightText: 2012 Blender Foundation
|
||||
#
|
||||
# SPDX-License-Identifier: GPL-2.0-or-later
|
||||
|
||||
def points_as_bmesh_cells(
|
||||
verts,
|
||||
points,
|
||||
points_scale=None,
|
||||
margin_bounds=0.05,
|
||||
margin_cell=0.0,
|
||||
):
|
||||
from math import sqrt
|
||||
import mathutils
|
||||
from mathutils import Vector
|
||||
|
||||
cells = []
|
||||
|
||||
points_sorted_current = [p for p in points]
|
||||
plane_indices = []
|
||||
vertices = []
|
||||
|
||||
if points_scale is not None:
|
||||
points_scale = tuple(points_scale)
|
||||
if points_scale == (1.0, 1.0, 1.0):
|
||||
points_scale = None
|
||||
|
||||
# there are many ways we could get planes - convex hull for eg
|
||||
# but it ends up fastest if we just use bounding box
|
||||
if 1:
|
||||
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_bounds, max(xa) + margin_bounds
|
||||
ymin, ymax = min(ya) - margin_bounds, max(ya) + margin_bounds
|
||||
zmin, zmax = min(za) - margin_bounds, max(za) + margin_bounds
|
||||
convexPlanes = [
|
||||
Vector((+1.0, 0.0, 0.0, -xmax)),
|
||||
Vector((-1.0, 0.0, 0.0, +xmin)),
|
||||
Vector((0.0, +1.0, 0.0, -ymax)),
|
||||
Vector((0.0, -1.0, 0.0, +ymin)),
|
||||
Vector((0.0, 0.0, +1.0, -zmax)),
|
||||
Vector((0.0, 0.0, -1.0, +zmin)),
|
||||
]
|
||||
|
||||
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(point_cell_current)
|
||||
distance_max = 10000000000.0 # a big value!
|
||||
|
||||
points_sorted_current.sort(key=lambda p: (p - point_cell_current).length_squared)
|
||||
|
||||
for j in range(1, len(points)):
|
||||
normal = points_sorted_current[j] - point_cell_current
|
||||
nlength = normal.length
|
||||
|
||||
if points_scale is not None:
|
||||
normal_alt = normal.copy()
|
||||
normal_alt.x *= points_scale[0]
|
||||
normal_alt.y *= points_scale[1]
|
||||
normal_alt.z *= points_scale[2]
|
||||
|
||||
# rotate plane to new distance
|
||||
# should always be positive!! - but abs incase
|
||||
scalar = normal_alt.normalized().dot(normal.normalized())
|
||||
# assert(scalar >= 0.0)
|
||||
nlength *= scalar
|
||||
normal = normal_alt
|
||||
|
||||
if nlength > distance_max:
|
||||
break
|
||||
|
||||
plane = normal.normalized()
|
||||
plane.resize_4d()
|
||||
plane[3] = (-nlength / 2.0) + margin_cell
|
||||
planes.append(plane)
|
||||
|
||||
vertices[:], plane_indices[:] = mathutils.geometry.points_in_planes(planes)
|
||||
if len(vertices) == 0:
|
||||
break
|
||||
|
||||
if len(plane_indices) != len(planes):
|
||||
planes[:] = [planes[k] for k in plane_indices]
|
||||
|
||||
# for comparisons use length_squared and delay
|
||||
# converting to a real length until the end.
|
||||
distance_max = 10000000000.0 # a big value!
|
||||
for v in vertices:
|
||||
distance = v.length_squared
|
||||
if distance_max < distance:
|
||||
distance_max = distance
|
||||
distance_max = sqrt(distance_max) # make real length
|
||||
distance_max *= 2.0
|
||||
|
||||
if len(vertices) == 0:
|
||||
continue
|
||||
|
||||
cells.append((point_cell_current, vertices[:]))
|
||||
del vertices[:]
|
||||
|
||||
return cells
|
||||
@@ -0,0 +1,448 @@
|
||||
# SPDX-FileCopyrightText: 2012 Blender Foundation
|
||||
#
|
||||
# SPDX-License-Identifier: GPL-2.0-or-later
|
||||
|
||||
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(context, depsgraph, 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 = obj.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_index:
|
||||
for layer in gp.layers:
|
||||
if layer.info == gp.layers.active_note:
|
||||
frame = layer.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 = context.annotation_data
|
||||
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(context, depsgraph, obj, source)
|
||||
|
||||
if not points:
|
||||
# print using fallback
|
||||
points = _points_from_object(context, depsgraph, 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()
|
||||
Reference in New Issue
Block a user