Blender and OpenSCAD are two of my favourite open-source 3D creation apps. Blender because it’s a swiss army knife of 3D making and rendering tools – although when it comes to automating a host of similar parts it can get a little out of its depth. And OpenSCAD, because it makes parametric design quite simple – but for some seemingly-simple things, such as adjusting individual vertices, it can get unwieldy and even impractical to use. So I find sometimes it’s easiest to design ‘blanks’ in Blender for tweaking in OpenSCAD.
Writing code to do that tweaking directly in OpenSCAD is obviously quite possible, using the import command – it can handle importing STL and OBJ files, so getting blank parts from Blender isn’t any real issue. But for more complex designs we may want to create a number of subtly different customised variants, or even automate some vertex-level values en-route between the two programs. So sometimes a Blender-SCAD bridge, using Python 3 and OBJ files, can be just what we need. So I thought I’d share some code for that sort of task, which you can download from the Parth3D experimental code repository on Github.
To test our code we’ll first need to make some OBJ files in Blender, which will represent our product ‘blanks’. I chose to use a basic cube and a six-sided, hexagonal, cylinder – as shown in the screengrab below. Once created, I went to the file menu and exported each individually as ASCII Wavefront OBJ files (binary ones won’t work for our code). I unimaginatively called them testcube.obj and testcylinder.obj, and placed them in the same folder as the code.
Please ensure the ‘selected only’ option is checked in the export dialog, otherwise things may not go to plan. Also, note from the screengrab that the faces in the objects are triangles – the code will not work with quad or n-gon faces in the OBJ file. If you don’t know how to do that don’t worry – when you create each object make sure to select it, go to the edit menu (e.g. ‘tab’ on the keyboard), select all (e.g. ‘a’ on the keyboard), and use ‘Ctrl-T’ on your keyboard to triangulate the model.
In terms of Python 3 imports for our code, there isn’t much to see. Basically, we’re only importing the standard Python random number module, just for use in our jitter additions. So that part of the code just looks like this:
import random
Next, we need some code to load our Blender-exported OBJ files. The function I used is shown below. It’s quite simple really, as it just iterates over all the lines of text looking for ones starting with ‘v’ (vertices) or ‘f’ (faces). Those lines are split using the single space between elements. The face lines are a little more complicated because each face-vertex-entry can be in more than one format, such as ‘124’ or ‘124/93/278’ – the former is just a simple vertex index, the latter is a shorthand that allows adding texture coordinate and face normal index values to each vertex. And please remember that indices start at 1 in OBJ files, which is why the code subtracts one from values it finds. At the end, the ‘load_obj‘ function returns lists for all of the vertices and triangles it finds.
def load_obj(fname):
verts = []
tris = []
with open(fname, "r") as fp:
for line in fp:
line = " ".join(line.strip().split())
bits=line.split(" ")
if bits[0] == "v":
if len(bits) != 4:
return [False, "Wrong vertex format (" + line + ")!"]
v1 = float(bits[1])
v2 = float(bits[2])
v3 = float(bits[3])
verts.append([v1, v2, v3])
elif bits[0] == "f":
if len(bits) != 4:
return [False, "Wrong number of vertices in face (" + line + ")!"]
if bits[1].find("/") > 0:
t1 = int(bits[1].split("/")[0]) - 1
else:
t1 = int(bits[1]) - 1
if bits[2].find("/") > 0:
t2 = int(bits[2].split("/")[0]) - 1
else:
t2 = int(bits[2]) - 1
if bits[3].find("/") > 0:
t3 = int(bits[3].split("/")[0]) - 1
else:
t3 = int(bits[3]) - 1
if t1 < 0 or t2 < 0 or t3 < 0:
return [False, "Negative face indices are not supported!"]
tris.append([t1,t2,t3])
mv = -1
for c in range(0, len(tris)):
if tris[c][0] > mv: mv = tris[c][0]
if tris[c][1] > mv: mv = tris[c][1]
if tris[c][2] > mv: mv = tris[c][2]
if mv > (len(verts) - 1):
return [False, "Vertex indices don't match number of vertices!"]
return [verts, tris]
Obviously (I hope) the next thing we’ll need is a way to format the vertex and face lists into a format OpenSCAD understands. To do that we’re going to use the ‘polyhedron‘ command (see the manual here) which is uber-compatible with that kind of data. The ‘make_openscad_module_text‘ function below is designed to that end and you can see that all it does is create text for a polyhedron by iterating over the vertices and faces. It puts the polyhedron in a module (which you name yourself) for flexibility, although you can easily change that in the code if you prefer. Also, our function has a ‘swapyz‘ parameter – that’s because Blender uses the Z axis for up, whereas OBJ files commonly use the Y axis, so we can swap them to make life easier.
def make_openscad_module_text(verts, tris, mname='object', swapyz=False):
if len(verts)==0 or len(tris)==0: return False
mtxt = ""
mtxt = mtxt + 'module ' + mname + '()\n'
mtxt = mtxt + '{\n'
mtxt = mtxt + ' polyhedron(points=['
for i, v in enumerate(verts):
if swapyz:
mtxt = mtxt + '[' + str(v[0]) + ', ' + str(v[2]) + ', ' + str(v[1]) + ']'
else:
mtxt = mtxt + '[' + str(v[0]) + ', ' + str(v[1]) + ', ' + str(v[2]) + ']'
if i < len(verts)-1:
mtxt = mtxt + ', '
mtxt = mtxt + '], faces=['
for i, t in enumerate(tris):
mtxt = mtxt + '[' + str(t[0]) + ', ' + str(t[1]) + ', ' + str(t[2]) + ']'
if i < len(tris)-1:
mtxt = mtxt + ', '
mtxt = mtxt + ']);\n'
mtxt = mtxt + '}\n'
mtxt = mtxt + '\n'
return mtxt
The code above covers the most important elements of our Blender-OpenSCAD pipeline. So now for some code to show that we can modify the 3D model en-route between them. We’ll just use a simple bit of jitteriness, by adding a random value to each vertex in a list of them. I’ve put an ‘add_vertex_jitter‘ function below to do that, with ‘maxrand‘ being the maximum (plus or minus) value to add to the coordinates. And for fun (isn’t coding so joyous!) I’ve added the ability to restrict the jitter to selected axes only.
def add_vertex_jitter(verts, maxrand, axes='xyz'):
nverts = []
for vert in verts:
if 'x' in axes:
x = vert[0] + ((random.random() * 2 * maxrand) -maxrand)
if 'y' in axes:
y = vert[1] + ((random.random() * 2 * maxrand) -maxrand)
if 'z' in axes:
z = vert[2] + ((random.random() * 2 * maxrand) -maxrand)
nverts.append([x, y, z])
return nverts
Finally we’re ready to bring the code together with a main function, as shown below. It simply opens a new text file called objscadtest.scad and writes some basic OpenSCAD code into it for a difference between our two OBJ files. Then it loads each of those OBJ files, converts the vertex/face values to polyhedron text, and writes them to the files. The code adds jitter to the cube only.
if __name__ == "__main__":
with open("objscadtest.scad", "w") as fp:
fp.write("difference()\n")
fp.write("{\n")
fp.write(" test_cube();\n")
fp.write(" test_cylinder();\n")
fp.write("}\n")
fp.write("\n")
verts, tris = load_obj("testcube.obj")
verts = add_vertex_jitter(verts, 0.3)
ctxt = make_openscad_module_text(verts, tris, "test_cube", swapyz=True)
fp.write(ctxt)
verts, tris = load_obj("testcylinder.obj")
ctxt = make_openscad_module_text(verts, tris, "test_cylinder", swapyz=True)
fp.write(ctxt)
print("Finished!")
And that’s all there is to our code – pretty simple I hope you’ll agree. But you can make it much more complex, and more suitable for your needs, quite easily. For example, you can create very complex operations involving a number of OBJ-file ‘blanks’, possibly dynamically deciding which are required based on parameters, with or without one-off vertex-level operations on some of them.
Now you can run the code in your terminal using something like ‘python obj2scad.py‘, or by clicking a button if you have one of those flashy modern integrated development environments. Once you see ‘Finished!’ on your console you should find that a new file has appeared in your code folder named ‘objscadtest.scad‘. If you open it in OpenSCAD you should see that it contains basic SCAD code for creating a constructive-solid-geometry difference object – that is our jittery cube with a hexagonal hole vertically through it. On my laptop it looked like the screengrab below. Yours will look a little different of course, because the jitter is random.
So, there we have it – a basic codebase for creating pipelines between Blender and OpenSCAD using Python 3 and OBJ files. Of course, if you’re up to the challenge, you could even automate it more using some shell-scripts (a.k.a. batch files in Windows) or more Python code. And I hope you find it useful 🙂