Skip to main content
6 of 8
more details on the problem.

How to find and fix local minima in any mesh for 3d printing (Scripting)

I am randomly generating organic shapes that I want to 3D print lightweight and thus with 0% infill, a single perimeter, and zero top and bottom layers. To make this work properly I need the mesh to contain no local minima (no vertices lower than all their connected vertices) other than at the base of the mesh, and all walls (edges/faces) must be minimally sloped. Vertices at local minima in the z direction will cause slicers to begin extrusions in mid-air and thus the print will fail.

randomly generate organic shape

My first idea is to find any vertex which is below all of it's connected vertices (neighbors) and move it up in the z direction until it is sufficiently above its neighbors. "Sufficiently above" would be determined by the minimum angle (~30°) of the edges between the vertex and its neighbors compared to the XY plane. A better solution might be to move the vertex along its normal vector.

Once a vertex has been adjusted, its neighboring vertices could be checked and iterated through until the entire model has been checked and "fixed".

Since there will always be vertices at the base of the model, either the algorithm could ignore those, or the algorithm could limit itself to only iterate on a vertex group (or on the currently selected vertices). Or maybe the algorithm could work by starting with the base vertices, and then traveling up the mesh adjusting each vertex as it goes.

I have started coding the script and I'm to the point where I need to calculate where to move an unsupported vertex to. I'm stuck here. I should probably use some sort of vector math that I forgot 25 years ago. :)

I can visualize the solution, but I have no idea how to calculate it. Comments are in the code.

import bpy
import bmesh
import math
from mathutils import Vector, Matrix

def slope(v,t):
    rise = (v.co.z - t.co.z)
    run = math.sqrt((v.co.x-t.co.x)**2+(v.co.y-t.co.y)**2)
    if run:
        return rise/run
    else:
        return 100*rise

#EDIT mode#
me = bpy.context.edit_object.data
bm = bmesh.from_edit_mesh(me)

vertices = sorted([v for v in bm.verts], key= lambda v : v.co.z) # sort by z height

# Find the lowest Z value amongst the object's verts, this is the base. Ignore these vertices
minZ = min( [ v.co.z for v in bm.verts ] ) 

for v in vertices:
    v.tag = False

allTagged = False
while allTagged = False: # once every vertex is tagged, we're finished
    allTagged = True
    for v in vertices:
        if v.co.z > minZ and not v.tag: # found an untagged, non-base vertex...
            allTagged = False  # ...Need to keep looping.
            v.tag = True # tag this vertex as analyzed/fixed
            neighbors = [e.other_vert(v) for e in v.link_edges] # create a list of connected neighbors
            print("target: " + str(v.co) + " Neighbors: " + str([v.co for v in neighbors])) # debugging
            unsupported = True #default each vertex as unsupported unless...
            for n in neighbors:
                print(slope(v,n)) # debugging
                if slope(v, n) > 0.3: # ...a connected vertex is sloped down enough...
                    unsupported = False # ...in which case this vertex is supported.
                    # future optimization - could create a list of "supporting vertices" for each vertex. If a vertex is verified as supported, and at least one "supporting vertex" isn't moved, no need to recheck this vertex.
            if unsupported:
                for n in neighbors:
                    n.tag = False # need to recheck neighbors after moving vertex (NOTE: this isn't optimal)
                print("Target is unsupported.") # debugging
                
                #TODO - how to calculate where to move the unsupported vertex in order to be supported?
                # Should move the vertex along its normal.
                # Imagine an inverted cone originating at each neighboring vertex. The slope of the cone is our target slope.
                # The point where the normal vector intersects the first cone is where the vertex should move to.
                # How to calculate that?

So my code correctly identifies the two vertices in the below sample blend file that are unsupported. All that is left is to move the vertices to the closest supported point along their normal vector. Sounds so simple...

I found this document that provides some pseudo-code that will probably help. I'll try porting it to Python tomorrow. The more I think about this problem though, the more complicated I realize it is. Sometimes, instead of unsupported points needing to move, the neighboring vertices may need to move. I think that depends on whether the normal vector points up or down.

Also, I can create a mesh where the normal of an unsupported vertex won't pass through the cones of the neighboring vertices because the edges will never reach a high enough angle, but the faces would. And really, the attached faces are what's important for slicing. So now I need to figure out how to check faces instead of edges.

Here is a blend file with a sample object. The vertex in the middle of the cube (local minima) needs to be moved so there is no local minima and so that at least one connected edge is at least ~20 degrees from horizontal. The vertex at the center/top should be moved until at least one connected edge is more than ~20 degrees from horizontal too.

Sample Object

Sample blend file