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.
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