Meshes with Python & Blender : Circles and Cylinders

In the last part of this series we’ll look at mak­ing cir­cles and cylin­ders. They are a lot trick­i­er than it seems! We’ll be build­ing on every­thing from the pre­vi­ous parts, as well as doing some Bmesh to fix normals.

Tutorial Series

Setup

Let’s start import­ing the usu­al pack­ages and Bmesh. We will be using it to fix nor­mals at the end. Also bring back good old object_from_data() and set_smooth() from the pre­vi­ous part

import bpy
import bmesh
import math


# ------------------------------------------------------------------------------
# Utility Functions

def set_smooth(obj):
    """ Enable smooth shading on an mesh object """

    for face in obj.data.polygons:
        face.use_smooth = True


def object_from_data(data, name, scene, select=True):
    """ Create a mesh object and link it to a scene """

    mesh = bpy.data.meshes.new(name)
    mesh.from_pydata(data['verts'], data['edges'], data['faces'])

    obj = bpy.data.objects.new(name, mesh)
    scene.collection.objects.link(obj)

    bpy.context.view_layer.objects.active = obj
    obj.select = True

    mesh.validate(verbose=True)

    return obj

The first step to mak­ing a cylin­der is mak­ing a 2D circle.

Making a circle

First, we need to put ver­tices in a cir­cle around a point. We’ll cal­cu­late the the angle at which each ver­tex is. This angle is actu­al­ly a polar coor­di­nate, so to get X, Y coor­di­nates we can use we’ll also to con­vert it to Cartesian. You can read more about the math on mathisfun.com and on Wikipedia Of course we are pass­ing the val­ue of Z, so we don’t need to cal­cu­late that. For the ini­tial cir­cle Z will be zero.

def vertex_circle(segments, z):
    """ Return a ring of vertices """
    verts = []

    for i in range(segments):
        angle = (math.pi*2) * i / segments
        verts.append((math.cos(angle), math.sin(angle), z))

    return verts

We can use this right away to make a cir­cle but it would only be a cir­cle of ver­tices. Let’s also add edges too. Adding edges to a cir­cle is pret­ty sim­ple. Just loop around each ver­tex and con­nect the cur­rent index plus the next one, then add the final edge which con­nects the last index with the first (zero). Following the lessons of part 4, let’s make a func­tion to make circles.

def make_circle(name, segments=32):
    """ Make a circle """

    data = {
            'verts': vertex_circle(segments, 0),
            'edges': [],
            'faces': [],
           }

    data['edges'] = [(i, i+1) for i in range(segments)]
    data['edges'].append((segments - 1, 0))

    scene = bpy.context.scene
    return object_from_data(data, name, scene)

make_circle('Some Circle', 64)

It doesn’t seem like much, but it’s a sol­id first step.

You might see some mes­sages from validate() com­plain­ing about the nor­mals being zero. That’s because there are no faces to cal­cu­late nor­mals. We will come back and add a way to fill this cir­cle lat­er on, reusing the code that adds caps to the cylinder

Circles all the way up

What is a cylin­der if not a cir­cle extrud­ed in 3D space? To extrude the cir­cle in the Z axis we can loop through vertex_circle() giv­ing it increas­ing val­ues of Z.

Time to make_cylinder()

def make_cylinder(name, segments=64, rows=4):
    """ Make a cylinder """

    data = { 'verts': [], 'edges': [], 'faces': [] }

    for z in range(rows):
        data['verts'].extend(vertex_circle(segments, z))

    scene = bpy.context.scene
    obj = object_from_data(data, name, scene)

    return obj

Notice vertex_circle() returns a list of ver­tices, so we need to use expand instead of append to keep the same list struc­ture. Also, we’re not adding edges here. We will flesh­ing out this mesh with actu­al faces, so there’s no need for edges. Making faces fol­lows the log­ic from the grids in part 1. Except this time, it goes around in a ring.

def face(segments, i, row):
    """ Return a face on a cylinder """

    ring_start = segments * row
    base = segments * (row + 1)

    return (base - 1, ring_start, base, (base + segments) - 1)

Just like in grids, the best way to fig­ure out is to turn on debug­ging and inspect­ing ver­tex indices. After all, this func­tion only needs to return the right num­bers. To make all faces across all rings and rows, we need to loop through both.

    for i in range(segments):
        for row in range(0, rows - 1):
            data['faces'].append(face(segments, i, row))

You might notice we are miss­ing the last face in each ring. The last face is a spe­cial case because the indices “rewind”.

We need to add a spe­cial case for that last face with a dif­fer­ent for­mu­la. On one side we need to grab ver­tices near the end of both rings, on the oth­er we need to grab the begin­ning verts. It’s sim­i­lar to the last edge in the circle.

def face(segments, i, row):
    """ Return a face on a cylinder """

    if i == segments - 1:
        ring_start = segments * row
        base = segments * (row + 1)

        return (base - 1, ring_start, base, (base + segments) - 1)
    else:
        base = (segments * row) + i
        return (base, base + 1, base + segments + 1, base + segments)

Now we have one good look­ing cylinder.

We’re just miss­ing caps to com­plete the mesh.

Capping the cylinder

There are two ways we can cap this cylinder:

  • Ngons. All ver­tices in the ring con­nect­ed as a sin­gle face.
  • A tri­an­gle fan. A series of tri­an­gles con­nect­ing to a cen­tral vertex.

To make a tri­an­gle fan we need to put a ver­tex in the mid­dle of the ring, and then go loop the ring con­nect­ing ver­tices to it. On the oth­er hand, to make an Ngon only we just loop through the ver­tices’ indices and put them all in a sin­gle tuple. This is clear­er to see in the bot­tom cap code.

def bottom_cap(verts, faces, segments, cap='NGON'):
    """ Build bottom caps as triangle fans """

    if cap == 'TRI':
        verts.append((0, 0, 0))
        center_vert = len(verts) - 1

        [faces.append((i, i+1, center_vert)) for i in range(segments - 1)]
        faces.append((segments - 1, 0, center_vert))

    elif cap == 'NGON':
        faces.append([i for i in range(segments)])

    else:
        print('[!] Passed wrong type to bottom cap')

In the case of a tri­an­gles fan we also need to fill the last face sep­a­rate­ly. The code for top_cap() is sim­i­lar, but we have to off­set the num­ber of indices to get the indices in the top row.

def top_cap(verts, faces, segments, rows, cap='NGON'):
    """ Build top caps as triangle fans """

    if cap == 'TRI':
        verts.append((0, 0, rows - 1))
        center_vert = len(verts) - 1
        base = segments * (rows - 1)

        [faces.append((base+i, base+i+1, center_vert))
                       for i in range(segments - 1)]

        faces.append((segments * rows - 1, base, center_vert))

    elif cap == 'NGON':
        base = (rows - 1) * segments
        faces.append([i + base for i in range(segments)])

    else:
        print('[!] Passed wrong type to top cap')

Now we can call them in the cylin­der func­tion. Since we already pass­ing the cap type, we can make it a para­me­ter in make_cylinder() and let the caller pass None to dis­able caps.

    if cap:
        bottom_cap(data['verts'], data['faces'], segments, cap)
        top_cap(data['verts'], data['faces'], segments, rows, cap)

Filling the circle

A cylin­der is an extrud­ed cir­cle, remem­ber? Therefore the bot­tom ring is the same as the first cir­cle we made and we can now use the bottom_cap() func­tion to fill it. Note that if we make faces for the cir­cle, we don’t need to set­up edges.

def make_circle(name, segments=32, fill=None):
    """ Make a circle """

    data = {
            'verts': vertex_circle(segments, 0),
            'edges': [],
            'faces': [],
           }

    if fill:
        bottom_cap(data['verts'], data['faces'], segments, fill)
    else:
        data['edges'] = [(i, i+1) for i in range(segments)]
        data['edges'].append((segments - 1, 0))

    scene = bpy.context.scene
    return object_from_data(data, name, scene)

Smooth it up

Now that we have com­plet­ed the mesh, let’s pol­ish it. We can add a call to set_smooth() to enable it smooth shad­ing for it. We can also add some mod­i­fiers like we did in the last part. We’ll add some bev­el and an edge split to fix the shad­ing. We only want to add a bev­el mod­i­fi­er if we actu­al­ly have caps though.

    set_smooth(obj)

    if cap:
        bevel = obj.modifiers.new('Bevel', 'BEVEL')
        bevel.limit_method = 'ANGLE'

    obj.modifiers.new('Edge Split', 'EDGE_SPLIT')

If you try this you will find there’s some­thing wrong with the bot­tom cap. The bev­el mod­i­fi­er is mak­ing it look real­ly weird and dent­ed. This is usu­al­ly a sign of messed up nor­mals. Jump into edit mode and enable nor­mals from the Display panel.

As you can see, the nor­mals of the bot­tom cap are point­ing up, when they should actu­al­ly be point­ing down. We need to fix that to get a good bev­el. You can do that by going into edit mode, and press­ing CTRL+N. But there’s also a way to it in code.

Fixing normals

In order to fix the nor­mals we’ll use one of bmesh oper­a­tors. These are dif­fer­ent from the reg­u­lar Blender oper­a­tors as they don’t depend on con­text. They will work as long as you give them a valid bmesh object.

Bmesh is a spe­cial Blender API that gives you very close access to the inter­nal mesh edit­ing API. It’s quite faster than oth­er meth­ods and more flex­i­ble. However, when it comes to cre­at­ing mesh­es from scratch Bmesh doesn’t offer any­thing too dif­fer­ent from the oth­er way. That’s why this series hasn’t touched on Bmesh until this point. In order to use bmesh we first cre­ate an bmesh object, then fill it with data (in this case using from_mesh()). Once we are fin­ished with it, we write the new data to the mesh and free the bmesh object from memory.

I won’t get too deep in Bmesh now since that would take an entire sep­a­rate tuto­r­i­al. You can read more about Bmesh in the docs. For now, let’s make a func­tion that takes a mesh and fix­es it’s normals.

def recalculate_normals(mesh):
    """ Make normals consistent for mesh """

    bm = bmesh.new()
    bm.from_mesh(mesh)

    bmesh.ops.recalc_face_normals(bm, faces=bm.faces)

    bm.to_mesh(mesh)
    bm.free()

Adding a call to this func­tion in make_cylinder() yields the final, awe­some look­ing cylinder.

Final Code

import bpy
import bmesh
import math


# ------------------------------------------------------------------------------
# Utility Functions

def set_smooth(obj):
    """ Enable smooth shading on an mesh object """

    for face in obj.data.polygons:
        face.use_smooth = True


def object_from_data(data, name, scene, select=True):
    """ Create a mesh object and link it to a scene """

    mesh = bpy.data.meshes.new(name)
    mesh.from_pydata(data['verts'], data['edges'], data['faces'])

    obj = bpy.data.objects.new(name, mesh)
    scene.collection.objects.link(obj)

    bpy.context.view_layer.objects.active = obj
    obj.select = True

    mesh.update(calc_edges=True)
    mesh.validate(verbose=True)

    return obj


def recalculate_normals(mesh):
    """ Make normals consistent for mesh """

    bm = bmesh.new()
    bm.from_mesh(mesh)

    bmesh.ops.recalc_face_normals(bm, faces=bm.faces)

    bm.to_mesh(mesh)
    bm.free()


# ------------------------------------------------------------------------------
# Geometry functions

def vertex_circle(segments, z):
    """ Return a ring of vertices """
    verts = []

    for i in range(segments):
        angle = (math.pi*2) * i / segments
        verts.append((math.cos(angle), math.sin(angle), z))

    return verts


def face(segments, i, row):
    """ Return a face on a cylinder """

    if i == segments - 1:
        ring_start = segments * row
        base = segments * (row + 1)

        return (base - 1, ring_start, base, (base + segments) - 1)

    else:
        base = (segments * row) + i
        return (base, base + 1, base + segments + 1, base + segments)


def bottom_cap(verts, faces, segments, cap='NGON'):
    """ Build bottom caps as triangle fans """

    if cap == 'TRI':
        verts.append((0, 0, 0))
        center_vert = len(verts) - 1

        [faces.append((i, i+1, center_vert)) for i in range(segments - 1)]
        faces.append((segments - 1, 0, center_vert))

    elif cap == 'NGON':
        faces.append([i for i in range(segments)])

    else:
        print('[!] Passed wrong type to bottom cap')


def top_cap(verts, faces, segments, rows, cap='NGON'):
    """ Build top caps as triangle fans """

    if cap == 'TRI':
        verts.append((0, 0, rows - 1))
        center_vert = len(verts) - 1
        base = segments * (rows - 1)

        [faces.append((base+i, base+i+1, center_vert))
                       for i in range(segments - 1)]

        faces.append((segments * rows - 1, base, center_vert))

    elif cap == 'NGON':
        base = (rows - 1) * segments
        faces.append([i + base for i in range(segments)])

    else:
        print('[!] Passed wrong type to top cap')


# ------------------------------------------------------------------------------
# Main Functions

def make_circle(name, segments=32, fill=None):
    """ Make a circle """

    data = {
            'verts': vertex_circle(segments, 0),
            'edges': [],
            'faces': [],
           }

    if fill:
        bottom_cap(data['verts'], data['faces'], segments, fill)
    else:
        data['edges'] = [(i, i+1) for i in range(segments)]
        data['edges'].append((segments - 1, 0))

    scene = bpy.context.scene
    return object_from_data(data, name, scene)


def make_cylinder(name, segments=64, rows=4, cap=None):
    """ Make a cylinder """

    data = { 'verts': [], 'edges': [], 'faces': [] }

    for z in range(rows):
        data['verts'].extend(vertex_circle(segments, z))

    for i in range(segments):
        for row in range(0, rows - 1):
            data['faces'].append(face(segments, i, row))

    if cap:
        bottom_cap(data['verts'], data['faces'], segments, cap)
        top_cap(data['verts'], data['faces'], segments, rows, cap)


    scene = bpy.context.scene
    obj = object_from_data(data, name, scene)
    recalculate_normals(obj.data)
    set_smooth(obj)

    bevel = obj.modifiers.new('Bevel', 'BEVEL')
    bevel.limit_method = 'ANGLE'

    obj.modifiers.new('Edge Split', 'EDGE_SPLIT')

    return obj


# ------------------------------------------------------------------------------
# Main Code

#make_circle('Circle', 64)
make_cylinder('Cylinder', 128, 4, 'TRI')

Wrap up

That’s it for this tuto­r­i­al, and the whole series! I hope these tuto­ri­als have been use­ful for you, or at least learned a new trick or two. We have talked about mak­ing 2D grids, cubes, icos­pheres, cir­cles and cylin­ders. We’ve also gone into debug­ging, man­ag­ing com­plex­i­ty, set­ting smooth shad­ing, adding and apply­ing mod­i­fiers, recal­cu­lat­ing nor­mals and read­ing data from files. It’s quite a lot, but it’s only the begin­ning. There are plen­ty more things to explore and learn in Blender!

Things you can do for yourself:

  • Use Bmesh instead of from_pydata() to build the mesh
  • Make a new vertex_circle() that makes spi­rals instead of circles
  • Refactor the cap func­tions into a sin­gle one, that can make both caps, one or none.

Got any ques­tions, or sug­ges­tions for new tuto­ri­als? Leave a com­ment below!

All the posts you can read
TutorialsBlender, Meshes, Procedural, Python19.10.2020