I’m a big fan of the open-source 3D modelling software, OpenSCAD.
In case you’re not familiar with it, it’s a tool for creating 3D CAD models. Unlike most tools in this space though, it’s script based and uses an interpreter to translate your model definition script to a 3D model file.
For example, to create a small sphere you could do:
sphere(20);
Which would generate this sphere:
Anyway, I like OpenSCAD a lot, though it’s not without its frustrations.
Chief among them, for me at least, is not being able to save the result of a function call like sphere(20)
to a variable for use later.
This seems like a really useful feature since it would allow users to more easily compose complex objects. You’d be able to use it to break up your scripts into smaller sections and then combine them all at the end for final rendering.
You can do some of what I’m talking about here with OpenSCAD “modules”. These are essentially user-defined functions, but they seem a bit heavyweight compared to just dropping the output of some existing functions into a variable for later use.
So, I did what any sensible person would do. I wrapped OpenSCAD in an R package that I called – perhaps unsurprisingly – openscad. Now before anyone gets excited, this was a toy project to see if I could do a thing. It was fun while it lasted, but I soon abandoned the project after implementing a few things that I thought we be entertaining. I also did this over a year ago (I forgot to write it up at the time!), so I’m not recommending anyone try to use this package for anything other than what it is, a fun-sounding, half finished piece of abandonware.
To start with, I wrapped a few of the functions I thought I’d ineed in R functions. In this case I’m using R to write OpenSCAD code, so the output of the R functions is the text of the equivalent OpenSCAD function. Naturally, this makes saving those outputs to variables a snap.
Next, I decided I wanted to be able to visualise some sort of data in a physical 3D print. What better place to start than R’s built-in “volcano” data set.
The volcano data is a matrix representing the topograhic information for the inactive Maunga Whau volcano in Auckland, New Zealand.
You can visualise it in R like this:
image(volcano)
This will create an image where the darker colours indicate higher ground.
I extended my package with a helper function to take a matrix like volcano and convert it to the appropriate OpenSCAD code to produce a 3D object. The resulting 3D printed volcano can be seen below.
The eagle-eyed among you may have noticed that the object in the photo is mirrored compared to the image()
output from R.
Mistakes happen and I’d made a big one.
I’d managed to flip the matrix while I was processing it, but at least it was easily fixed.
What should I create and print next?
It obviously had to be the R logo. And, since it was the run up to Christmas, why not add a hanging loop so I could use it as an ornament on our tree?
The code I used to create this (both in R and the OpenSCAD output) can be found at the end of the post.
Overall, and even though I quickly abandoned it, this was a fascinating project to work on.
I learned that even though some things in OpenSCAD bug me – the variable thing mentioned above, using the cube()
function the create cubes and cuboids among other things – overall, it a good language and wrapping that up in another language just adds uneccesary complexity.
That complexity can, of course, have a flip side. Sometimes adding an abstraction layer like this can actually give you super-powers. For example, the R code I used to generate the volcano above looked like this:
topo_matrix(volcano)
While the OpenSCAD it generated is 5307 lines that look like this:
translate([0,0,0]){cube([5,5,100], center=false);}
translate([5,0,0]){cube([5,5,100], center=false);}
translate([10,0,0]){cube([5,5,101], center=false);}
translate([15,0,0]){cube([5,5,101], center=false);}
translate([20,0,0]){cube([5,5,101], center=false);}
translate([25,0,0]){cube([5,5,101], center=false);}
translate([30,0,0]){cube([5,5,101], center=false);}
translate([35,0,0]){cube([5,5,100], center=false);}
translate([40,0,0]){cube([5,5,100], center=false);}
...
There’s no chance I would have attempted to do this in OpenSCAD alone without some sort of helper script to create the 5307 individually positioned cuboid shapes that go into the final model. It would have simply been too much work.
It’s also interesting to think about seemingly odd things like wrapping one language in another. For example, did you know that parts of Shiny work in a similar way to this? Some of the R functions you write in a shiny app are generating, HTML, css and JavaScript behind the scenes and that’s what make up the “ui” part of your app code.
In the end, for me in this moment, I’m happy enough to work around the minor issues I have with OpenSCAD. Other, more accomplished OpenSCAD users than I, can create some incredible things with it, so I’m using it without the fun R wrapper for now. Who knows though, maybe I’ll dust it off and finish the package some day.
If you made it this far, thanks for taking the time to read this one and as a special treat here’s the R logo xmas tree ornament .stl file so you can print it off for yourself. Happy printing!
R Logo 3D model R code
library(openscad)
# ovals -------------------------------------------------------------------
oval_outer <- circle(10) |>
linear_extrude(height = 3) |>
resize(x = 31, y = 19)
oval_inner <- circle(10) |>
linear_extrude(height = 3) |>
resize(x = 24, y = 14, z = 0) |>
translate(x = 2, y = -1)
oval_final <- difference(c(oval_outer, oval_inner))
# r upright ---------------------------------------------------------------
r_upright <- cube(c(5, 17, 6)) |>
translate(x = -3, y = -13)
# r loop ------------------------------------------------------------------
r_loop_outer <- hull(c(
circle(5) |>
linear_extrude(6) |>
translate(x = 7, y = -1),
cube(c(5, 6, 6)) |>
translate(x = -3, y = -2),
cube(c(5, 6, 6)) |>
translate(x = -3, y = -6)
))
r_loop_inner <- hull(c(
circle(1.5) |>
linear_extrude(6) |>
translate(x = 6, y = -1.2),
cube(c(2, 2, 6)) |>
translate(x = -3, y = -1.7),
cube(c(2, 2, 6)) |>
translate(x = -3, y = -2.7)
))
r_loop <- difference(c(r_loop_outer, r_loop_inner))
# r leg -------------------------------------------------------------------
top_left <- cube(c(0.1, 0.1, 6)) |>
translate(x = 14, y = -13)
top_right <- cube(c(0.1, 0.1, 6)) |>
translate(x = 8.5, y = -13)
bottom_left <- cube(c(1, 1, 6)) |>
translate(x = 7, y = -5)
bottom_right <- cube(c(1, 1, 6)) |>
translate(x = 3.5, y = -5)
r_leg <- hull(c(top_left, top_right, bottom_left, bottom_right))
# hoop --------------------------------------------------------------------
hoop <- difference(c(cylinder(1, 2), cylinder(1, 1))) |>
translate(x = 2, y = 10, z = 2)
# join model --------------------------------------------------------------
model <- paste0(
"$fn=100;\n",
oval_final,
r_upright,
r_loop,
r_leg,
hoop,
collapse = "\n"
)
model_final <- structure(model, class = c("openscad", "character"))
# Write final model -------------------------------------------------------
write_scad(model_final, "r-logo.scad", overwrite = TRUE)
model_to_stl(model_final, "r-logo.stl")
OpenSCAD Code Generated by the R code above
$fn=100;
difference(){
resize([31,19,0], convexity=10){
linear_extrude(height=3, convexity=10, twist=0, scale=1){
circle(10);
}
}
translate([2,-1,0]){
resize([24,14,0], convexity=10){
linear_extrude(height=3, convexity=10, twist=0, scale=1){
circle(10);
}
}
}
}
translate([-3,-13,0]){
cube([5,17,6], center=false);
}
difference(){
hull(){
translate([7,-1,0]){
linear_extrude(height=6, convexity=10, twist=0, scale=1){
circle(5);
}
}
translate([-3,-2,0]){
cube([5,6,6], center=false);
}
translate([-3,-6,0]){
cube([5,6,6], center=false);
}
}
hull(){
translate([6,-1.2,0]){
linear_extrude(height=6, convexity=10, twist=0, scale=1){
circle(1.5);
}
}
translate([-3,-1.7,0]){
cube([2,2,6], center=false);
}
translate([-3,-2.7,0]){
cube([2,2,6], center=false);
}
}
}
hull(){
translate([14,-13,0]){
cube([0.1,0.1,6], center=false);
}
translate([8.5,-13,0]){
cube([0.1,0.1,6], center=false);
}
translate([7,-5,0]){
cube([1,1,6], center=false);
}
translate([3.5,-5,0]){
cube([1,1,6], center=false);
}
}
translate([2,10,2]){
difference(){
cylinder(h = 1, r1 = 2, r2 = 2);
cylinder(h = 1, r1 = 1, r2 = 1);
}
}