Flatten roofs into inner ceiling

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.

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:
v.select=False
for v in ob.data.edges:
v.select=False
for v in ob.data.vertices:
v.select = False
v.select = v.co.z < maxZ
# enter edit mode and delete selected vertices
bpy.ops.object.mode_set(mode='EDIT')
bpy.ops.mesh.delete(type='VERT')
bpy.ops.mesh.remove_doubles()
bpy.ops.mesh.dissolve_degenerate() #Clean som more
bpy.ops.object.mode_set(mode='OBJECT')


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:

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

• 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? Commented Jul 20, 2022 at 11:56
• 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. Commented Jul 20, 2022 at 11:59
• I think you should delete polygons which normal are not facing the center of gravity instead Commented Jul 20, 2022 at 12:17
• I think you are right, any idea how to write something like that? Commented Jul 20, 2022 at 12:23
• Have you tried searching? blender.stackexchange.com/questions/30837/… Commented Jul 20, 2022 at 14:17

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.

Select one or more objects to process then run the script

import bpy
import bmesh

NORMAL_Z_THRESHOLD = 0.1
MERGE_BY_DISTANCE_THRESHOLD = 0.025

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
bm.select_flush_mode()
m.update()

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

for face in bm.faces:
if not face.select:
continue
for e in face.edges:
for f in e.link_faces:
if not f is face:

face.select = not no_selected_adj_faces

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

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

unselect_isolated_faces(bm)

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)

unselect_isolated_faces(bm)

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.ops.object.select_all(action='DESELECT')

o.select_set(True)
bpy.context.view_layer.objects.active = o

bpy.ops.object.editmode_toggle()
bpy.ops.mesh.select_all(action='SELECT')
bpy.ops.mesh.remove_doubles(threshold=MERGE_BY_DISTANCE_THRESHOLD)
bpy.ops.mesh.select_all(action='DESELECT')

select_top_faces(o)

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

select_outer_edges()

bpy.ops.mesh.select_mode(type="VERT")
bpy.ops.mesh.select_all(action='INVERT')
bpy.ops.mesh.dissolve_verts()
bpy.ops.object.editmode_toggle()

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

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

bpy.ops.object.select_all(override, action='DESELECT')

o.select_set(True)
bpy.context.view_layer.objects.active = o

bpy.ops.object.editmode_toggle(override)
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')

select_top_faces(o)

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")

select_outer_edges()

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

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

list = [o for o in selected_objects]

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

process_object(o)

for o in list:
o.select_set(True)

• Thanks a lot, this will save me a huge ammount of time. Commented Jul 22, 2022 at 8:00
• you're welcome! glad I could help :) Commented Jul 22, 2022 at 8:02
• @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. Commented Jul 28, 2022 at 15:19
• Thanks for the suggestion Harry, still new at StackExchange. I owe you a lot, at least a nice dinner. Commented Jul 28, 2022 at 16:39