 # 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.

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

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)

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
Tutorials19.10.2020

1. Rich Martin(1 year ago)
1. Diego Gangl(12 months ago)