Wow I'm seriously impressed (and surprised) I've managed to get Blender to slice my 80k face object into 2000 slices in under 8 seconds (top image - on the R is the original, L is the sliced version).
Unfortunately there seems to be a glitch where sometimes layers are lost or only partially formed - I think it's a bug in the bisect function, not my code - eg bottom photo: the icosphere is missing the middle layer if the slice would be at exactly the midline of the icosphere .
I've included the code below in case its useful to someone.
import bpy, bmesh
from bpy import context as C
from mathutils import Vector
# This code will slice the selected object into a set of vertices and edges
# along the direction of the unit vector (normal)
# in steps of step_size in local coordinate space
#(this ignores the scale property, so make sure to apply it before)
# If you want to slice at a different angle then just change the normal, and start
# far enough down the z-axis to ensure the plane will slice the entire model
# You can calculate step size to give appropriate thickness slices with sqrt(x*x + y*y + z*z)
# NB this strategy won't work if you want to cut perpendicular to the z- axis
# in that case you'll need to change the code a bit more
def newobj(bm, name):
me = bpy.data.meshes.new(name)
bm.to_mesh(me)
ob = bpy.data.objects.new(name,me)
#C.scene.objects.link(ob)
bpy.data.collections['Slices'].objects.link(ob)
return ob
# https://blender.stackexchange.com/questions/32283/what-are-all-values-in-bound-box
def bounds(obj, local=False):
local_coords = obj.bound_box[:]
om = obj.matrix_world
if not local:
worldify = lambda p: om * Vector(p[:])
coords = [worldify(p).to_tuple() for p in local_coords]
else:
coords = [p[:] for p in local_coords]
rotated = zip(*coords[::-1])
push_axis = []
for (axis, _list) in zip('xyz', rotated):
info = lambda: None
info.max = max(_list)
info.min = min(_list)
info.distance = info.max - info.min
push_axis.append(info)
import collections
originals = dict(zip(['x', 'y', 'z'], push_axis))
o_details = collections.namedtuple('object_details', 'x y z')
return o_details(**originals)
#bb = bounds(obj)
#print("bounds(bb) = ((",bb.x.min,",",bb.x.max,"),(",bb.y.min,",",bb.y.max,"),(",bb.z.min,",",bb.z.max,"))")
# UID Code
def make_key(obj):
return hash(obj.name + str(time.time()))
def get_id(self):
if "id" not in self.keys():
self["id"] = make_key(self)
return self["id"]
# set the id type to all objects.
#bpy.types.Object.id = property(get_id)
# could store them in the file as a datastore in the window manager.
#wm = bpy.context.window_manager
#wm["objects"] = 0
#rna = wm.get("_RNA_UI", {})
#rna["objects"] = {o.name: o.id for o in bpy.data.objects}
#wm["objects"] = len(rna["objects"])
#wm["_RNA_UI"] = rna
# This code will slice the selected object into a set of vertices and edges
# along the direction of the unit vector (normal)
# in steps of step_size in local coordinate space
#(this ignores the scale property, so make sure to apply it before)
# If you want to slice at a different angle then just change the normal, and start
# far enough down the z-axis to ensure the plane will slice the entire model
# You can calculate step size to give appropriate thickness slices with sqrt(x*x + y*y + z*z)
# NB this strategy won't work if you want to cut perpendicular to the z- axis
# in that case you'll need to change the code a bit more
def slicer(step_size = 0.01, normalOfSlice = (0,0,1)):
object_details = bounds(C.object, True)
bpy.ops.object.mode_set(mode='OBJECT')
# If necessary create a new collection to hold the slices
if not "Slices" in bpy.data.collections:
bpy.context.scene.collection.children.link(bpy.data.collections.new("Slices"))
startLoc = object_details.z.min
stopLoc = object_details.z.max
steps = int((stopLoc - startLoc)/step_size) + 1 # + 1 because we want the faces on either end
print("")
print("******************** STARTING NEW RUN ********************")
print("Slicing into ", steps, "slices")
print("between dimensions of: ", startLoc, ":", stopLoc)
lBound = 0
halving = [steps]
slices = []
slices.append(bmesh.new())
slices[0].from_mesh(C.object.data)
# Add UIDs to the faces of the original object which will be propogated through the model
# for these purposes just using the original ID will be fine
#faceUID = bm.faces.layers.integer.new('faceUID')
#faceUID = bm.faces.layers.integer.get('faceUID')
#for face in bm.faces:
# face[faceUID] = new_UID()
#while (halving[-1] > lBound):
while len(halving) > 0:
if lBound + 1 < halving[-1]:
# At the moment we're just created successively halved copies of the mesh.
# And adding them to the list
# Even though we are duplicate meshes and running the function twice
# it is still faster than operating from an original (uncopied) mesh because the bisect function
# 1. doesn't seem to index the geometry in any way (so time is proportional to the mesh size)
# Splitting at this stage effectively creates indexed bits of meshes)
# 2. There appears to be no way to create an inner and an outer copy
# in the same step (eg by passing in two bmesh objects to the bisect function)
# Thus the bisect function has to be run once on each copy
curSlice = int(lBound+(halving[-1]-lBound)/2)
slicePoint = startLoc+curSlice*step_size
halving.append(curSlice)
print ("Splitting Mesh into 2 halves with bounds: ",lBound, ":", halving[-1], ":", halving[-2], "and SlicePoint=", startLoc+lBound*step_size, "-", slicePoint, "-", startLoc+halving[-2]*step_size )
slices.append(slices[-1].copy())
#This slices the original mesh and discards geometry
# on the inner = lower index = negative side of the plane
bmesh.ops.bisect_plane(
slices[-2],
geom=slices[-2].verts[:]+slices[-2].edges[:]+slices[-2].faces[:],
plane_co=(0,0,slicePoint-step_size),
plane_no=normalOfSlice,
clear_inner=True)
#This slices the duplicated copy of the mesh and discards geometry
# on the outer = higher index = positive side of the plane
bmesh.ops.bisect_plane(
slices[-1],
geom=slices[-1].verts[:]+slices[-1].edges[:]+slices[-1].faces[:],
plane_co=(0,0,slicePoint+step_size),
plane_no=normalOfSlice,
clear_outer=True)
#print("len(slices) post splitting: ",len(slices))
else:
# we've just halved the slice directly above the lower bound so slice again to extract
# the top and bottom vertex loops (ie discarding the inner and outer geometry for both)
# then create two objects from the data
# Finally step back up to next level clearing out the redundant slice data
curSlice = int(lBound+(halving[-1]-lBound)/2)
upperSlicePoint = startLoc+(curSlice+1.5)*step_size
lowerSlicePoint = startLoc+(curSlice+0.5)*step_size
slices.append(slices[-1].copy())
print("Extracting last two layers: lowerSlicePoint = ",lowerSlicePoint, "upperSlicePoint = ",upperSlicePoint)
# for vert in slices[-1].verts:
# print( 'v %f %f %f' % (vert.co.x, vert.co.y, vert.co.z) )
#This time slice the mesh and discard both inner and outer geometry to just leave the vertex loops
bmesh.ops.bisect_plane(
slices[-2],
geom=slices[-2].verts[:]+slices[-2].edges[:]+slices[-2].faces[:],
plane_co=(0,0,upperSlicePoint),
plane_no=normalOfSlice,
clear_inner=True,
clear_outer=True)
newobj(slices[-2], "bisect-"+str(upperSlicePoint))
#This time slice the mesh and discard both inner and outer geometry to just leave the vertex loops
bmesh.ops.bisect_plane(
slices[-1],
geom=slices[-1].verts[:]+slices[-1].edges[:]+slices[-1].faces[:],
plane_co=(0,0,lowerSlicePoint),
plane_no=normalOfSlice,
clear_inner=True,
clear_outer=True)
newobj(slices[-1], "bisect-"+str(lowerSlicePoint))
#print("halving list = ", halving)
#print ("extracted:",lBound, halving[-1])
lBound = halving[-1]+1
#print ("update: lb1",lBound, halving[-1])
#print ("del:",halving[-1])
del halving[-1]
#print("len(slices) post extraction: ",len(slices))
del slices[-2:]
#print("len(slices) post drop end: ",len(slices))
#print("len(halving) post drop end: ",len(slices))
# if len(halving) > 0:
# print("checking for halving[-1] > lBound exit criteria:",halving[-1],lBound)
# if len(halving) == 0:
# print("EXITING: len(halving) == 0")
# break
print("FINISHED")
print("Sliced object into ", steps, "slices of ", step_size, "thickness")
slices.clear()
#C.object.user_clear() # without this, removal would raise an error.
#bpy.data.objects.remove(C.object, True)
#slicer(step_size = 0.01, normalOfSlice = (0,0,1))
slicer(1, (0,0,1))