Cell fracture addon: 2D application, separation and cleanup

Hi all,

I’m working on a project involving a (medieval) castle. I’m focusing on the walls/turrets at the moment. It seems like a good idea to use the cell fracture addon to divide the walls up into ‘bricks’ (= voronoi cells). The final scene will be quite heavy, so it’s important to model the castle as low-poly as possible. Ideally, I’d like to start from a 2D voronoi cell structure, the cells of which can then be (randomly) extruded and transformed.

Btw, I’ve already achieved most of this goal through some python coding (and quite some trial and (even more) error). So you may want to look at this post primarily as a description of the process I went through in order to achieve this goal, rather than as a pledge for help. It’s just that some things seem a little weird – at least to a (newbie) blender-python-coder like myself. So just feel free to comment on anything if you think I’ve missed a trick. :slight_smile:

So I’d like to be able to add a plane, add a particle system to this plane, apply the cell fracture addon to this plane/particles, and end up with several (2D) meshes/faces/ngons, each representing a single voronoi cell. First thing to notice is that the addon produces 3D cells, even when applied to a (2D) plane. That’s understandable, seeing as the addon is written to work on 3D volumes/pointclouds. Second thing: the generated cells have a weird topology, like colinear vertices and redundant ngon faces. That’s also understandable, I guess. Btw, these observations are not meant as a critique on the cell fracture addon. I’m very happy to be able to use this. It just needs some post processing to fit my purposes.

So, first thing to do: reduce the 3D output of the addon to 2D (‘separation’). I’m not sure what the ‘interior vertex group’ option is for, but it seems to fit our needs. Set the addon to use ‘own particles’ and ‘interior vgroup’. You may also want to set the noise to 0.0 for best results. The code below operates on selected meshes (voronoi cells). Please note that all code is debug, not production. So if you select a camera, you’ll probably get an error.

def Separate2DVoronoi(v3Translate = [0.0, 0.0, 0.0]):
    print("")
    print("Separate2DVoronoi()")
    
    selObjects = bpy.context.selected_objects
    if bpy.ops.object.mode_set.poll(): bpy.ops.object.mode_set(mode = 'OBJECT')
 
    for cObject in selObjects:
        bpy.ops.object.select_all(action='DESELECT')
        bpy.ops.object.select_pattern(pattern=cObject.name, extend=False)
        bpy.context.scene.objects.active = cObject
        
        
        if bpy.ops.object.mode_set.poll(): bpy.ops.object.mode_set(mode = 'EDIT')
        
        bpy.ops.mesh.select_all(action='DESELECT')
        cObject.vertex_groups.active_index = 0
        bpy.ops.object.vertex_group_select()
        bpy.ops.mesh.select_all(action='INVERT')
        bpy.ops.mesh.duplicate_move(MESH_OT_duplicate={"mode":1}, TRANSFORM_OT_translate={"value":(v3Translate[0], v3Translate[1], v3Translate[2]), "constraint_axis":(False, False, False), "constraint_orientation":'GLOBAL', "mirror":False, "proportional":'DISABLED', "proportional_edit_falloff":'SMOOTH', "proportional_size":1, "snap":False, "snap_target":'CLOSEST', "snap_point":(0, 0, 0), "snap_align":False, "snap_normal":(0, 0, 0), "texture_space":False, "release_confirm":False})
        bpy.ops.mesh.separate(type='LOOSE')
        if bpy.ops.object.mode_set.poll(): bpy.ops.object.mode_set(mode = 'OBJECT')

        bpy.ops.object.origin_set(type='ORIGIN_GEOMETRY', center='MEDIAN')


Summary: for all selected meshes, select the inverse (!) of the interior vertex group (= 1 ngon), duplicate it, move it out of the way and separate it from the original cell. Afterwards, delete the original cells (box-select in 3Dview) and you’re left with only a collection of 2D ngons. Which is nice! But does this always work, we might wonder…

Ok, assuming the previous operation went well, we’re left with a number of meshes, consisting of 1 polygon/ngon each. We observe, however, that these polygons aren’t that well defined. There appear to be some colinear edges/vertices (see also above). We’ll try to fix this using the code below. This code assumes we have several meshes selected consisting of at least 1 polygon.

def Cleanup2DVoronoiEdgesTest2(logInfo = False):
    print("")
    print("Cleanup2DVoronoiEdgesTest2()")
    
    selObjects = bpy.context.selected_objects
    if bpy.ops.object.mode_set.poll(): bpy.ops.object.mode_set(mode = 'OBJECT')
    bpy.context.tool_settings.mesh_select_mode = [True, False, False]
    for cObject in selObjects:
        bpy.ops.object.select_all(action='DESELECT')
        bpy.ops.object.select_pattern(pattern=cObject.name, extend=False)
        bpy.context.scene.objects.active = cObject
        
        
        nrPolygons = len(cObject.data.polygons)
        if not nrPolygons == 1:
            print("## WARNING: object has not got 1 and only 1 polygon ## <continuing> <TODO?>")
            if nrPolygons == 0: continue
        
        
        polygon = cObject.data.polygons[0]
        edgeKeys = polygon.edge_keys
        nrEdgeKeys = len(edgeKeys)
        if logInfo: print("--", "nrEdgeKeys:", nrEdgeKeys)
        listEdgeVertices = []
        for ie in range(nrEdgeKeys):
            if logInfo: print("--", "--", "edge_key:", "0: %02d" % edgeKeys[ie][0], "1: %02d" % edgeKeys[ie][1])
            listEdgeVertices.append(edgeKeys[ie])
            
        orderedListEdgeVertices = OrderListEdgeVertices(listEdgeVertices)
        nrOrderedListEdgeVertices = len(orderedListEdgeVertices)
        if logInfo: print("--", "nrOrderedListEdgeVertices:", nrOrderedListEdgeVertices)
        for ie in range(nrOrderedListEdgeVertices):
            edge = orderedListEdgeVertices[ie]
            if logInfo: print("--", "--", "edge:", "0: %02d" % edge[0], "1: %02d" % edge[1])
            
        
        orderedListVertices = []
        for ie in range(nrOrderedListEdgeVertices):
            edge = orderedListEdgeVertices[ie]
            orderedListVertices.append(edge[0])
        nrOrderedListVertices = len(orderedListVertices)
        if logInfo: print("--", "nrOrderedListVertices:", nrOrderedListVertices)
        
        
        cornerVertices = FilterCornerVertexIndices(cObject.data.vertices, orderedListVertices)
        nrCornerVertices = len(cornerVertices)
        if logInfo: print("--", "nrCornerVertices:", nrCornerVertices)
        if bpy.ops.object.mode_set.poll(): bpy.ops.object.mode_set(mode = 'EDIT')
        bpy.ops.mesh.select_all(action='DESELECT')
        if bpy.ops.object.mode_set.poll(): bpy.ops.object.mode_set(mode = 'OBJECT')
        for iv in range(nrCornerVertices):
            cObject.data.vertices[cornerVertices[iv]].select = True
        if bpy.ops.object.mode_set.poll(): bpy.ops.object.mode_set(mode = 'EDIT')
        bpy.ops.mesh.edge_face_add()
        bpy.ops.mesh.select_all(action='INVERT')
        bpy.ops.mesh.delete(type='VERT')
        if bpy.ops.object.mode_set.poll(): bpy.ops.object.mode_set(mode = 'OBJECT')


Summary: for all selected meshes, try to eliminate colinear edges/vertices of the first (and hopefully only) polygon. I’d hoped that polygon.edge_keys would contain the edges in loop sequence (we’re assuming a closed edge loop here). I’ve also tried cObject.data.loops, but to no avail. It doesn’t seem possible to iterate through the edges/vertices of a polygon in ‘loop-order’.

So we need to order these edges ourselves (see code below). This is done by the function OrderListEdgeVertices(). We then select the corner (non-colinear) vertices (using VerticesAreColinear()) and make a face with them. We then delete everything except the newly created face.


    
def OrderListEdgeVertices(listEdgeVertices):
    nrEdges = len(listEdgeVertices)
    if(nrEdges < 2): return listEdgeVertices
    
    rvList = []
    edgeVertices1 = None
    edgeVertices2 = None
    for ie in range(nrEdges - 1):
        if ie == 0: edgeVertices1 = listEdgeVertices[ie]
        else: edgeVertices1 = edgeVertices2
        edgeVertices2 = listEdgeVertices[ie + 1]
        
        orderedEdgeVertices = OrderEdgeVertices(edgeVertices1, edgeVertices2)
        if ie == 0: rvList.append(orderedEdgeVertices[0])
        rvList.append(orderedEdgeVertices[1])
        
    return rvList

    
def VerticesAreColinear(prevCo, currCo, nextCo, len2Limit = 0.000001, cos2Limit = 0.999):
    v1 = mathutils.Vector((prevCo[0] - currCo[0], prevCo[1] - currCo[1], prevCo[2] - currCo[2]))
    n2v1 = v1.length_squared
    if n2v1 < len2Limit: return True
    v2 = mathutils.Vector((nextCo[0] - currCo[0], nextCo[1] - currCo[1], nextCo[2] - currCo[2]))
    n2v2 = v2.length_squared
    if n2v2 < len2Limit: return True
    
    sprod = v1.dot(v2)
    sprod2 = sprod * sprod
    
    cos2 = 0
    try: cos2 = sprod2 / (n2v1 * n2v2)
    except: cos2 = 0
    if cos2 > cos2Limit: return True
    
    return False
    

I had to remove some code from the quote above as this post was (rightly, probably) judged too long for this forum. I’ll be glad to fill in any missing parts in a follow-up.

Thanks in advance for any comments,
g

Update: it seems like bpy.ops.mesh.edge_face_add() produces some weird results if you have cornerVertices selected that already have an edge between them. I didn’t notice this before as I was testing on a limited number of cells.

This can be solved by iterating through the cornerVertices 1 by 1, duplicating them (and moving them out of the way) and assigning these duplicates to a (newly created) vertex group. We can then selected all vertices in this group and apply edge_face_add() on them. Afterwards we throw away the original geometry.

Here’s the updated code:

def Cleanup2DVoronoiEdgesTest2(logInfo = False, v3Translate = [0.0, 0.0, 1.0]):
    print("")
    print("Cleanup2DVoronoiEdgesTest2()")
    
    selObjects = bpy.context.selected_objects
    if bpy.ops.object.mode_set.poll(): bpy.ops.object.mode_set(mode = 'OBJECT')
    bpy.context.tool_settings.mesh_select_mode = [True, False, False]
    for cObject in selObjects:
        bpy.ops.object.select_all(action='DESELECT')
        bpy.ops.object.select_pattern(pattern=cObject.name, extend=False)
        bpy.context.scene.objects.active = cObject
        
        
        nrPolygons = len(cObject.data.polygons)
        if not nrPolygons == 1:
            print("## WARNING: object has not got 1 and only 1 polygon ## <continuing> <TODO?>")
            if nrPolygons == 0: continue
        
        
        polygon = cObject.data.polygons[0]
        edgeKeys = polygon.edge_keys
        nrEdgeKeys = len(edgeKeys)
        if logInfo: print("--", "nrEdgeKeys:", nrEdgeKeys)
        listEdgeVertices = []
        for ie in range(nrEdgeKeys):
            if logInfo: print("--", "--", "edge_key:", "0: %02d" % edgeKeys[ie][0], "1: %02d" % edgeKeys[ie][1])
            listEdgeVertices.append(edgeKeys[ie])
            
        orderedListEdgeVertices = OrderListEdgeVertices(listEdgeVertices)
        nrOrderedListEdgeVertices = len(orderedListEdgeVertices)
        if logInfo: print("--", "nrOrderedListEdgeVertices:", nrOrderedListEdgeVertices)
        for ie in range(nrOrderedListEdgeVertices):
            edge = orderedListEdgeVertices[ie]
            if logInfo: print("--", "--", "edge:", "0: %02d" % edge[0], "1: %02d" % edge[1])
            
        
        orderedListVertices = []
        for ie in range(nrOrderedListEdgeVertices):
            edge = orderedListEdgeVertices[ie]
            orderedListVertices.append(edge[0])
        nrOrderedListVertices = len(orderedListVertices)
        if logInfo: print("--", "nrOrderedListVertices:", nrOrderedListVertices)
        
        
        cornerVertices = FilterCornerVertexIndices(cObject.data.vertices, orderedListVertices)
        nrCornerVertices = len(cornerVertices)
        if logInfo: print("--", "nrCornerVertices:", nrCornerVertices)
        
        if bpy.ops.object.mode_set.poll(): bpy.ops.object.mode_set(mode = 'EDIT')
        bpy.ops.mesh.select_all(action='DESELECT')
        
        if bpy.ops.object.mode_set.poll(): bpy.ops.object.mode_set(mode = 'OBJECT')
        nrVertices = len(cObject.data.vertices)
        if logInfo: print("--", "nrVertices:", nrVertices)
        newGroup = cObject.vertex_groups.new('newGroup')
        if logInfo: print("--", "newGroup:", newGroup)
        for ivv in range(nrVertices):
            if not ivv in cornerVertices: 
                continue
            if logInfo: print("--", "--", "cornerVertex.co:", cObject.data.vertices[ivv].co)
            
            if bpy.ops.object.mode_set.poll(): bpy.ops.object.mode_set(mode = 'EDIT')
            bpy.ops.mesh.select_all(action='DESELECT')
            if bpy.ops.object.mode_set.poll(): bpy.ops.object.mode_set(mode = 'OBJECT')
            cObject.data.vertices[ivv].select = True
            
            if bpy.ops.object.mode_set.poll(): bpy.ops.object.mode_set(mode = 'EDIT')
            bpy.ops.mesh.duplicate_move(MESH_OT_duplicate={"mode":1}, TRANSFORM_OT_translate={"value":(v3Translate[0], v3Translate[1], v3Translate[2]), "constraint_axis":(False, False, False), "constraint_orientation":'GLOBAL', "mirror":False, "proportional":'DISABLED', "proportional_edit_falloff":'SMOOTH', "proportional_size":1, "snap":False, "snap_target":'CLOSEST', "snap_point":(0, 0, 0), "snap_align":False, "snap_normal":(0, 0, 0), "texture_space":False, "release_confirm":False})
            
            if bpy.ops.object.mode_set.poll(): bpy.ops.object.mode_set(mode = 'OBJECT')
            selectedVerts = filter(lambda v: v.select, cObject.data.vertices)
            indicesDupliVertices = []
            for sv in selectedVerts: indicesDupliVertices.append(sv.index)
            newGroup.add(indicesDupliVertices, 1.0, 'ADD')
            
            
        if bpy.ops.object.mode_set.poll(): bpy.ops.object.mode_set(mode = 'EDIT')
        bpy.ops.mesh.select_all(action='DESELECT')
        cObject.vertex_groups.active_index = newGroup.index
        bpy.ops.object.vertex_group_select()
        bpy.ops.mesh.edge_face_add()
        bpy.ops.mesh.separate(type='LOOSE')
        
        if bpy.ops.object.mode_set.poll(): bpy.ops.object.mode_set(mode = 'OBJECT')
        bpy.ops.object.origin_set(type='ORIGIN_GEOMETRY', center='MEDIAN')


Apparently, empty lines disappear in a code block. Sorry for the ‘unreadability’.