2
$\begingroup$

I hope this question isn't too broad for Blender stackexchange.

Quite often in my work I import topographical data specifying the flat geometry of buildings. I'm trying to make a script that put roofs on these buildings. I have some Python experiences, but I'm having a really tough time using Python in Blender, and haven't been able to find any in depth tutorials to help me on my way.

The goal of this script is to iterate of all selected faces, check if they have four sides, extrude the faces, then scale the shortest edges to 0. I want to have the shortest edges so to roof aligns correctly. See the image for an example of what I want to achieve.

Before and after

$\endgroup$
  • $\begingroup$ I have a question that may help a Scriptwriter hone in on what you are looking for. Are the buildings one piece or separate objects? $\endgroup$ – MarcClintDion Jul 17 '14 at 12:44
5
$\begingroup$

Here is a quick script that extrudes selected faces of selected mesh objects, if the face has 4 vertices and the angle between the face normal and the up axis is lower than 3°:

import bpy
import bmesh
from math import degrees
from mathutils import Vector

up = Vector((0, 0, 1))

for ob in bpy.context.selected_editable_objects:
    if ob.type == 'MESH':
        me = ob.data
        if me.is_editmode:
            bm = bmesh.from_edit_mesh(me)
        else:
            bm = bmesh.new()
            bm.from_mesh(me)
        bm.normal_update()

        for face in bm.faces:
            if len(face.verts) == 4 and face.select:
                try:
                    angle = degrees(face.normal.angle(up))
                except ValueError:
                    continue
                if angle < 3: # some threshold
                    ret = bmesh.ops.extrude_face_region(bm, geom=[face])
                    verts = [e for e in ret['geom'] if isinstance(e, bmesh.types.BMVert)]
                    faces = [e for e in ret['geom'] if isinstance(e, bmesh.types.BMFace)]
                    bmesh.ops.translate(bm, vec=face.normal * -0.7, verts=verts)

                    e1, e2, e3, e4 = faces[0].edges
                    if (e1.calc_length() + e3.calc_length()) < (e2.calc_length() + e4.calc_length()):
                        edges = [e1, e3]
                    else:
                        edges = [e2, e4]
                    bmesh.ops.collapse(bm, edges=edges)

        if me.is_editmode:
            bmesh.update_edit_mesh(me)
        else:
            bm.to_mesh(me)
        me.update()

The two shorter edges of the top-face is collapsed to create a sensible roof.

Note that it doesn't account of degenerated quads (non-planar, self-intersecting), but should work good enough for your use case.

$\endgroup$
  • $\begingroup$ This one is interesting. I added 2 separate cubes then entered Object mode for one of the cubes and duplicated and moved it. It can be run in both Edit Mode of Object mode and whatever is selected in Edit mode is what will have the roof added. This gives a lot of control since both Edit and Object mode selections influence the results. $\endgroup$ – MarcClintDion Jul 17 '14 at 16:48
2
$\begingroup$

Here is another solution:

import bpy
import bmesh

def extrude_and_collapse(obj, height):
    if not obj or obj.mode != 'OBJECT':
        raise RuntimeException("requires object mode")

    bm = bmesh.new()
    bm.from_mesh(obj.data)

    selection = [face for face in bm.faces if face.select and len(face.verts) == 4]
    for face in selection:
        face.select = False

    result = bmesh.ops.extrude_face_region(
                            bm, 
                            geom=selection,
                            use_keep_orig=False
                            )

    created_verts = tuple(elem for elem in result['geom'] if isinstance(elem, bmesh.types.BMVert))
    created_faces = tuple(elem for elem in result['geom'] if isinstance(elem, bmesh.types.BMFace))
    edit_faces = []

    for face in created_faces:
        face.tag = False
        for vertex in face.verts:
            if not vertex in created_verts:
                break
        else:
            face.tag = True
            edit_faces.append((face, face.normal.copy()))

    collapse_edges = set()
    for face, normal in edit_faces:
        for vertex in face.verts:
            n = 0   
            for linked_face in vertex.link_faces:
                if linked_face.tag:
                    n += 1
            vertex.co += height * normal / n  

        edge_min = min(face.edges, key=lambda x: x.calc_length())
        for loop in edge_min.link_loops:
            if loop.face == face:
                break
        loop = loop.link_loop_next
        loop = loop.link_loop_next
        edge_opposite = loop.edge

        collapse_edges.add(edge_min)
        collapse_edges.add(edge_opposite)

    bmesh.ops.collapse(
        bm,
        edges=list(collapse_edges)
        )

    bm.select_flush(True)
    bm.normal_update()
    bm.to_mesh(obj.data)
    bm.free()
    obj.data.update()

extrude_and_collapse(bpy.context.active_object, 1.0)
$\endgroup$
  • $\begingroup$ This script is giving me odd results, I've uploaded a screenCapture to PasteAll pasteall.org/pic/74101 $\endgroup$ – MarcClintDion Jul 17 '14 at 16:50
  • $\begingroup$ For the given task it should work. (Test #1, #2) But it is not bulletproof. There still might occur plenty of situtations which the script does not handle. $\endgroup$ – pink vertex Jul 17 '14 at 17:48
  • $\begingroup$ Another test Checking wether the edge ratio is correct, --- Selection, --- Result $\endgroup$ – pink vertex Jul 17 '14 at 18:02
  • $\begingroup$ @MarcClintDion He assumes all houses are one object and the top faces are selected. Than it works nicely. $\endgroup$ – stacker Jul 17 '14 at 20:34

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service, privacy policy and cookie policy

Not the answer you're looking for? Browse other questions tagged or ask your own question.