Rapidly Spawn Meshes in Blender via Scripting

Jim O'C
Level Up Coding
Published in
8 min readApr 23, 2021

--

Simple Python for making time-consuming parts quickly and cleanly

This is Part 2 in a series documenting each step in building a Blender add-on to generate printable components within a modular system, but this post can 100% be read on its own.

TL;DR: Gist of the complete script at the bottom — it’s fairly short & has docstrings.

When I started using Blender to model 3D-printable components for electronics projects, an early and surprising challenge turned out to be how to make standoff mount points for PCBs and places where components would be held together with screws.

Blender 3D Screenshot of Layout Editor showing a donut shaped mesh
How hard could it be to make a less advanced version of a donut?

I spent longer than I want to admit relying on Boolean modifiers and dealing with the artifacts and non-manifold geometries they’d produce.

Blender 3D Screenshot of Layout Editor showing a donut shaped mesh and an inner wireframe cylinder targeted as the source for a Boolean modifier
Wireframes. So many wireframes. Wireframes for every Metric hardware diameter. (Wireframe setting is in Object Properties > Viewport Display panel)

Going the Boolean Modifier route will still be the right choice in some situations, and all the time I spent over-complicating things proved useful for finding some of the tricks to work around the non-manifold issues (ie avoid applying the modifier). But the simpler way that will be obvious to more experienced modelers as a first choice is to add a Circle mesh in Object mode for one of the circumferences, toggle the object into Edit Mode and add the other Circle mesh, then use “Bridge Edge Loops” to connect them.

Blender 3D Screenshot of Layout Editor in Edit Mode showing two nested circles with the inner one selected
Multiple Meshes Inside of a Single Object & Adjusting Mesh Parameters from “Add [a thing]” Menu

This approach probably generates more faces than is strictly necessary, but since the target here is fabrication rather than rendering, I haven’t found it to cause any performance or output issues. With the edge loops bridged, and all the resulting faces selected, it’s a simple matter of extruding them to the desired height — and it’s a simple process to script!

Blender 3D Screenshot of Layout Editor in Edit Mode showing two nested circles with area in between filled by faces and all faces selected
In Edit Mode (Faces) — ‘A’ to select all, ‘E’ to extrude, [number] for target distance, ‘Return’ to commit

To script mesh operations, it is best to use the Blender Python BMesh module rather than working directly with the bpy.ops.mesh objects that are logged in the GUI’s Info Editor panel. The BMesh module still provides access to higher order mesh functions than are available in many 3D object APIs where interaction can only occur via direct manipulation of vertices & face indices, but it works as a background process and so avoids the additional overhead of repeated calls to bpy.ops (for example, seeing each added to the active Undo stack).

In some ways that might be helpful for anyone with Front-End dev experience, BMesh can be pictured as working like the shadow DOM — here’s what the BMesh docs have to say about it:

“Note that unlike bpy, a BMesh does not necessarily correspond to data in the currently open blend-file, a BMesh can be created, edited and freed without the user ever seeing or having access to it.”

When working with a bmesh, you can instantiate it in memory using bmesh.new() —and then either add new geometry that does not exist in the scene already, or by converting an existing mesh in the scene, calling .from_mesh(some_mesh_in_scene) on the newly instantiated mesh. It is important to remember that the latter removes the mesh from the scene, and so the Python script must release ownership and put it back in the scene as a mesh once the edits are completed. With that in mind, I think it becomes a lot easier to follow along the steps this program will follow.

  1. Create a BMesh
  2. Fill it with geometry
  3. Convert to Scene-friendly Mesh
  4. Add to Scene

The script will combine the class definition for a custom Standoff class, a function for extruding plane geometry defined outside the class because it will be useful for more shapes than just the Standoff class mesh, a function for converting an editable BMesh into a Mesh object which will also be defined outside of the Standoff class, and a test function to execute when the script is run as __main__. This test function will add a single Standoff mesh into the scene and mimic the behavior this module will provide when it’s integrated into a larger Blender Add-on. At a high level then, writing the interface before the implementation, the script will look like this:

import bpy
import bmesh
class Standoff:
# initializer

# public class method returning Mesh based on current class attr
# internal class methods creating BMesh with Standoff geometry# def a function for extruding planes to a desired distance# def a function for returning Mesh from input BMesh# def a test function to create a Standoff and add mesh into sceneif __name__ == "__main__":
test()

To begin the script, import the two required modules: bpy and bmesh, and define the Standoff class:

class Standoff:
def __init__(self, name='Std', m_diam=3, depth=3, segments=64):
self.name = name
self.depth = depth
self.segments = segments
self.radii = {
"inner": m_diam/2,
"outer": m_diam*1.25
}

Setting default parameters just seems like a good safeguard, and 3 seems like a reasonable number (M3 has been a fairly common diameter in my projects so far). Using a higher segment count rather than ‘Shade Smooth’ has given me good results for printing — but there could definitely be a better approach that I’ve missed. Because the attributes of the self.radii object will need to be updated whenever a new diameter is updated, this should be abstracted into a method:

class Standoff:
def __init__(self, name='Std', m_diam=3, depth=3, segments=64):
self.name = name
self.depth = depth
self.segments = segments
self.radii = self.__radii(m_diam)
def __radii(m_diam):
return { "inner": m_diam/2, "outer": m_diam*1.25 }

An instance of the Standoff class will offer a Standoff.mesh(args) interface function, accepting new input for the depth and m_diam attributes and returning mesh data ready to be added into the scene.

class Standoff:
# ...
def mesh(self, depth=None, m_diam=None):
if depth:
self.depth = depth
if m_diam:
self.radii = self.__radii(m_diam)
bm = self.__create_drum_bmesh()
return bmesh_to_mesh(bm)

The __create_drum_bmesh method provides the unique bmesh geometry for the Standoff class:

class Standoff:
# ...
def __create_drum_bmesh(self):
"""
returns new bmesh instance for current self geom values
"""
bm = bmesh.new()
to_extrude = self.__make_footprint(bm)
extrude_faces(bm, to_extrude["faces"], self.depth)
return bm

Within this method, extrude_faces is also something that can be used on any set of faces that needs to be extruded… and so it will also exist outside the Standoff class. For now, Standoff._make_footprint(bm) is the last thing unique to the class that needs to be defined to get it up and running. As called within _create_drum_bmesh, _make_footprint should return a dictionary that includes the key faces:

class Standoff:
# ....
def __make_footprint(self, bm):
"""
takes bmesh instance,
returns dict with keys 'faces', 'edges' from
bmesh.ops.bridge_loops
"""
def circumference(radius):
"""
for radius, create circle in bm, return edges list
"""
edges = []
circ = bmesh.ops.create_circle(
bm,
radius=radius,
segments=self.segments,
cap_ends=False
)
[ edges.append(e) for v in circ["verts"]
for e in v.link_edges
if e not in edges ]
return edges
edges = [ e for r in self.radii.values() for e in
circumference(r) ]
bridged = bmesh.ops.bridge_loops(bm, edges=edges)
return bridged

The bmesh.ops.create_circle method wrapped in the circumference function, coupled with the earlier visual demonstration of ‘Add Circle’ meshes to an object within ‘Edit Mode’ gives a good picture of how the bmesh API works. For their first argument, bmesh.ops methods take a reference to a particular bmesh instance, like a human interacting with a mesh in edit mode, do some stuff to this instance, and return. Unlike bpy.ops operators, bmesh.ops methods often return dictionaries which provide access to some subset of the bmesh or the relevant modified properties. In the case of create_circle a list of the created vertices is available behind the key "verts". In this case, the circumference function uses list comprehension to iterate through this, gets the values of the edges within each vertex’s link_edges list property, appending only those that have not already been found attached to a previously iterated vertex.

Inner and outer edges are created and the resulting edges are selected by calling the circumference function for each radius value, and these edges are bridged using the bmesh.ops.bridge_loops method. This method returns a dictionary with faces and edges keys.

Creating this bridged footprint, and returning access to its dictionary fills the interface needed to call extrude_from_flat within _create_drum_bmesh. Outside of the Standoff class, extrude_from_flat can be defined as:

def extrude_faces(bm, faces, depth=1.0):
extruded = bmesh.ops.extrude_face_region(bm, geom=faces)
verts=[ e for e in extruded["geom"]
if isinstance(e, bmesh.types.BMVert) ]
del extruded
bmesh.ops.translate(bm, verts=verts, vec=(0.0, 0.0, depth))

The bmesh.ops.extrude methods create additional geometry within the bmesh but, respecting the single-responsibility principle, they do not themselves transform the geometry. In my view, this is an instance where it becomes especially easy to picture that the bmesh module interfaces Blender operations that are abstracted & condensed within the GUI. There are subtle distinctions (but dramatic differences in outcomes) between the many bmesh.ops.extrude methods, and a large number of optional parameters for configuring their behavior.

Once the bmesh.types.BMVert instances are filtered from the geom sequence returned by the extrusion and copied into a newly created list, the extruded object can and should be removed from memory to make sure the bmesh and resulting mesh stay in a valid state. Finally, the bmesh.ops.translate method is called — in this instance translating the vertices by thedepth value along the z axis relative to the bmesh instance’s object matrix (the method also accepts a space argument to set a different relative transform origin).

With this logic complete, the desired geometry for the standoff now exists within a bmesh as returned within the Standoff class initializer’s bmesh_to_mesh( self._create_drum_bmesh() ) call. All that is left is to implement this function that will handle transforming a bmesh_to_mesh and clear the bmesh from memory, and then to add the resulting mesh created for the Standoff class instance to the scene. Outside of the Standoff class, bmesh_to_mesh can be written as:

def bmesh_to_mesh(bm, me=None):
if not me:
me = bpy.data.meshes.new("Mesh")
bm.to_mesh(me)
bm.free()
return me

By writing this function with a default me=None parameter and a conditional to handle it, it can also handle updating an existing Mesh instance.

In production, adding the Mesh into the scene will be handled elsewhere, so for now, a basic version of that operation can happen inside of a test function for use when the module runs as main:

def test(m_diam=2.5, depth=3, name="Standoff"):
def add_mesh_to_collection(me, name):
"""
gets reference to collection in active bpy.context,
creates new object with 'me' Mesh arg as obj data value
links created object into referenced collection
"""
collection = bpy.context.collection.objects
obj = bpy.data.objects.new(name, me)
collection.link(obj)
return obj
std = Standoff(m_diam=m_diam, depth=depth, name=name)
add_mesh_to_collection(std.mesh(), std.name)
if __name__ == "__main__":
test()
Blender 3D Layout View showing donut shaped drum object selected, with 3D Print Tools -> Analyze menu open and passing results of manifold geometry checks
Created via Script, 3D Print Tools -> Analyze Menu shows clean manifold geometry because of using the correct `bmesh.ops.extrude` method for the task.

From here, this Standoff class can be extended to make slightly more complex geometry and to allow dynamic adjustment of the model via a UI menu — but that will all go into separate articles. The next post in this series will document how to create an Add-on, registering this script behind a custom Blender UI element, configured to pass adjustable values for the metric_diameter, depth, and segments arguments.

For now, the complete gist is below — thank you for reading, and for any comments on how to improve my code or explanation.

--

--

Developer, Scuba Instructor, Plant-Based. Runner, because part of my brain thinks more will always be more and needs the reminder.🖖 Lore shot first.