I have a number of roofs that I need to flatten to their inner lower surfaces and would really need some help. What I try to is something like this:

  1. Calculate the the centre of gravity (yellow dot) do determine the inside. I guess there might be better ways.
  2. Group nearby vertices together (gren circles). For example within a distance of 4 dm.
  3. For each group find the point with the lowest Z. If more then one exists pick the one closest to the center of gravity.Ii this case it is the red dots.
  4. Move all points in each group to the selected (red) point in step 3.
  5. Clean up the model with standard tools like merge by distance.

Any help would be appriciated, especially step 2.

IMage of example roof

Edit: I have written two scripts for planar floor slabs, one moves all vertices to the lowest point in the object and one removes all vertices higher then the lowest point. The first script does not work very well since the clean up tools fails to create a simple flat plane. The second method works much better but is a bit risky since it assumes that that there are no stray vertices below the lowest plane. Unfortunatly none of these scripts works for this case. The intentional final result in this example would be 2 flat planes with 6 vertices. Note that there are a hole in the roof for a window as well that will generate a few more vertices and planes that needs to be cleaned up afterwars. I think we can assume consistant mesh normals pointing downwards but parts of the roof might be vertical as well.

This is the code for removing vertices for planar roofs that I use

    C = bpy.context
    objs = C.selected_objects
    for ob in objs:
        loc_vertex_coordinates = [ v.co for v in ob.data.vertices ] # local coordinates of vertices
        # Find the highestZ value amongst the object's verts
        maxZ = max( [ co.z for co in loc_vertex_coordinates ] )
         # Delete all vertices below maxZ
         #Start to deselect all vertices. To deselect vertices we need to deselect faces(polygons) and edges at first
        for v in ob.data.polygons:                   
        for v in ob.data.edges:
        for v in ob.data.vertices:
            v.select = False
            v.select = v.co.z < maxZ
        # enter edit mode and delete selected vertices         
        bpy.ops.mesh.dissolve_degenerate() #Clean som more
        bpy.ops.mesh.edge_face_add() #Merge

Edit two: Here is an example file file roofs from a number of projects. They can still be bore complex with dormer windows: Link to Github example file

Edit three: I got a new roof from an architect and for some reason the script does not handle this case: Roof before script Roof adter script

The link to the Blender file containing these roof are: https://github.com/maxtillberg/ICEBridge/blob/main/roofs.blend

  • $\begingroup$ hi. so you want the final result to be 2 flat planes? Is 6 vertices result ok? like 2 quads only for the entire roof? $\endgroup$ Jul 20, 2022 at 11:56
  • 1
    $\begingroup$ You've posted this question before. What have you tried so far, where are you stuck? This looks like the hard way to achieve this result. Assuming consistent mesh normals, it seems much simpler to just select faces with normal point downwards and discard all else. $\endgroup$ Jul 20, 2022 at 11:59
  • 1
    $\begingroup$ I think you should delete polygons which normal are not facing the center of gravity instead $\endgroup$
    – Gorgious
    Jul 20, 2022 at 12:17
  • $\begingroup$ I think you are right, any idea how to write something like that? $\endgroup$ Jul 20, 2022 at 12:23
  • 1
    $\begingroup$ Have you tried searching? blender.stackexchange.com/questions/30837/… $\endgroup$ Jul 20, 2022 at 14:17

1 Answer 1


The following script will do a Merge By Distance using MERGE_BY_DISTANCE_THRESHOLD and select all faces with a normal positive z value greater than NORMAL_Z_THRESHOLD but taking in consideration holes in the roof as well. Then deletes the top vertices and further processes for unnecessary edges and vertices leaving you behind with a simplified mesh of 2 flat planes.

enter image description here

Select one or more objects to process then run the script

import bpy
import bmesh


objects = bpy.context.view_layer.objects
selected_objects = bpy.context.selected_objects

if not objects.active.select_get() and len(selected_objects) > 0:
    objects.active = selected_objects[0]

# ========================================================================================
# Starting in 3.2 context overrides are deprecated in favor of temp_override
# https://docs.blender.org/api/3.2/bpy.types.Context.html#bpy.types.Context.temp_override
# They are scheduled to be removed in 3.3
# ========================================================================================

def use_temp_override():
    ''' Determine whether Blender is 3.2 or newer and requires
        the temp_override function, or is older and requires
        the context override dictionary
    version = bpy.app.version
    major = version[0]
    minor = version[1]

    return not (major < 3 or (major == 3 and minor < 2))

win = bpy.context.window
scr = win.screen

def get_areas(type):
    return [area for area in scr.areas if area.type == type]

def get_regions(areas):
    return [region for region in areas[0].regions if region.type == 'WINDOW']

def select_outer_edges():
    o = bpy.context.edit_object
    m = o.data
    bm = bmesh.from_edit_mesh(m)
    bm.select_mode |= {'EDGE'}
    for e in bm.edges:
        e.select = e.is_boundary

def unselect_isolated_faces(bm):
    bm = bmesh.from_edit_mesh(o.data)

    for face in bm.faces:
        if not face.select:
        no_selected_adj_faces = True
        for e in face.edges:
            for f in e.link_faces:
                if not f is face:
                    no_selected_adj_faces = no_selected_adj_faces and not f.select

        face.select = not no_selected_adj_faces

def select_top_faces(o):
    bm = bmesh.from_edit_mesh(o.data)
    face_indices = []

    for f in bm.faces:
        f.select = f.normal.z > t and ((f.normal.x + f.normal.y)*0.5 < 0)

    face_indices = [f.index for f in bm.faces if f.select]

    for f in bm.faces:
        f.select = False

    for f in bm.faces:
        f.select = f.index in face_indices

    for f in bm.faces:
        f.select = f.normal.z > t and (-(f.normal.x + f.normal.y)*0.5 < 0)


    for f in bm.faces:
        if f.index in face_indices:
            f.select = True

def process_object(o):

    areas  = get_areas('VIEW_3D')

    # ========================================================================================
    # (if) execute using temp override
    # ========================================================================================

    if use_temp_override():

        with bpy.context.temp_override(window=win, area=areas[0], regions=get_regions(areas)[0], screen=scr):


            bpy.context.view_layer.objects.active = o






    # ========================================================================================
    # (else) execute using legacy override
    # ========================================================================================        

        override = {
            'window': win,
            'screen': scr,
            'area': areas[0],
            'region': get_regions(areas)[0],

        bpy.ops.object.select_all(override, action='DESELECT')
        bpy.context.view_layer.objects.active = o

        bpy.ops.mesh.select_all(override, action='SELECT')
        bpy.ops.mesh.remove_doubles(override, threshold=MERGE_BY_DISTANCE_THRESHOLD)
        bpy.ops.mesh.select_all(override, action='DESELECT')


        bpy.ops.mesh.delete(override, type='VERT')
        bpy.ops.mesh.select_all(override, action='SELECT')
        bpy.ops.mesh.normals_make_consistent(override, inside=False)
        bpy.ops.mesh.select_mode(override, type="EDGE")


        bpy.ops.mesh.select_mode(override, type="VERT")
        bpy.ops.mesh.select_all(override, action='INVERT')

# ========================================================================================
# execute script
# ========================================================================================

list = [o for o in selected_objects]

for o in list:
    if not o.type == 'MESH':


for o in list:
  • $\begingroup$ Thanks a lot, this will save me a huge ammount of time. $\endgroup$ Jul 22, 2022 at 8:00
  • $\begingroup$ you're welcome! glad I could help :) $\endgroup$ Jul 22, 2022 at 8:02
  • $\begingroup$ @user2404987 hey how are u. hmmm u should probably open a new question with the new requirements and link this thread as this has been resolved with the intial requirements. that way u could also draw more attention from fellow community members who can help out. maybe they can even come up with a better algorithm. il try to take a look at it in the morning. $\endgroup$ Jul 28, 2022 at 15:19
  • 1
    $\begingroup$ Thanks for the suggestion Harry, still new at StackExchange. I owe you a lot, at least a nice dinner. $\endgroup$ Jul 28, 2022 at 16:39

You must log in to answer this question.

Not the answer you're looking for? Browse other questions tagged .