Rapidly Spawn Meshes in Blender via Scripting
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.
I spent longer than I want to admit relying on Boolean modifiers and dealing with the artifacts and non-manifold geometries they’d produce.
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.
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!
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.
- Create a
BMesh
- Fill it with geometry
- Convert to Scene-friendly
Mesh
- 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 bmeshclass 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()
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.