Over the last few days I have become interested with the idea of using programming-language based software to create 3D models. One the languages I have been playing with is OpenSCAD, which has proven tremendously helpful in creating interesting shapes.
I am currently trying to create a flower with OpenSCAD, and I have encountered an issue that I have not been able to circumvent using the documentation or other resources I found online.
Here is the short form of the question:
Can I pass a function as a parameter to an OpenSCAD module?
If so, how? If not, why not and what can I do instead?
This brings me to the long form of the question with specifics to my situation:
I am trying to create petals using a linear extrusion of a 2D polar function, and intersecting that with a 3D function.
To do this, I am starting out with two very nice modules I found on http://spolearninglab.com/curriculum/lessonPlans/hacking/resources/software/3d/openscad/openscad_math.html. I do not claim to have written them in the first place.
First - 3D plotter by Dan Newman /* 3Dplot.scad */
// 3dplot -- the 3d surface generator
// x_range -- 2-tuple [x_min, x_max], the minimum and maximum x values
// y_range -- 2-tuple [y_min, y_max], the minimum and maximum y values
// grid -- 2-tuple [grid_x, grid_y] indicating the number of grid cells along the x and y axes
// z_min -- Minimum expected z-value; used to bound the underside of the surface
// dims -- 2-tuple [x_length, y_length], the physical dimensions in millimeters
//Want to pass in z(x,y) as parameter
module 3dplot(x_range=[-10, +10], y_range=[-10,10], grid=[50,50], z_min=-5, dims=[80,80]){
dx = ( x_range[1] - x_range[0] ) / grid[0];
dy = ( y_range[1] - y_range[0] ) / grid[1];
// The translation moves the object so that its center is at (x,y)=(0,0)
// and the underside rests on the plane z=0
scale([dims[0]/(max(x_range[1],x_range[0])-min(x_range[0],x_range[1])),
dims[1]/(max(y_range[1],y_range[0])-min(y_range[0],y_range[1])),1])
translate([-(x_range[0]+x_range[1])/2, -(y_range[0]+y_range[1])/2, -z_min])
union()
{
for ( x = [x_range[0] : dx : x_range[1]] )
{
for ( y = [y_range[0] : dy : y_range[1]] )
{
polyhedron(points=[[x,y,z_min], [x+dx,y,z_min], [x,y,z(x,y)], [x+dx,y,z(x+dx,y)],
[x+dx,y+dy,z_min], [x+dx,y+dy,z(x+dx,y+dy)]],
faces=prism_faces_1);
polyhedron(points=[[x,y,z_min], [x,y,z(x,y)], [x,y+dy,z_min], [x+dx,y+dy,z_min],
[x,y+dy,z(x,y+dy)], [x+dx,y+dy,z(x+dx,y+dy)]],
faces=prism_faces_2);
}
}
}
}
Second - 2D Grapher /* 2dgraphing.scad */
// function to convert degrees to radians
function d2r(theta) = theta*360/(2*pi);
// These functions are here to help get the slope of each segment, and use that to find points for a correctly oriented polygon
function diffx(x1, y1, x2, y2, th) = cos(atan((y2-y1)/(x2-x1)) + 90)*(th/2);
function diffy(x1, y1, x2, y2, th) = sin(atan((y2-y1)/(x2-x1)) + 90)*(th/2);
function point1(x1, y1, x2, y2, th) = [x1-diffx(x1, y1, x2, y2, th), y1-diffy(x1, y1, x2, y2, th)];
function point2(x1, y1, x2, y2, th) = [x2-diffx(x1, y1, x2, y2, th), y2-diffy(x1, y1, x2, y2, th)];
function point3(x1, y1, x2, y2, th) = [x2+diffx(x1, y1, x2, y2, th), y2+diffy(x1, y1, x2, y2, th)];
function point4(x1, y1, x2, y2, th) = [x1+diffx(x1, y1, x2, y2, th), y1+diffy(x1, y1, x2, y2, th)];
function polarX(theta) = cos(theta)*r(theta);
function polarY(theta) = sin(theta)*r(theta);
module nextPolygon(x1, y1, x2, y2, x3, y3, th) {
if((x2 > x1 && x2-diffx(x2, y2, x3, y3, th) < x2-diffx(x1, y1, x2, y2, th) || (x2 <= x1 && x2-diffx(x2, y2, x3, y3, th) > x2-diffx(x1, y1, x2, y2, th)))) {
polygon(
points = [
point1(x1, y1, x2, y2, th),
point2(x1, y1, x2, y2, th),
// This point connects this segment to the next
point4(x2, y2, x3, y3, th),
point3(x1, y1, x2, y2, th),
point4(x1, y1, x2, y2, th)
],
paths = [[0,1,2,3,4]]
);
}
else if((x2 > x1 && x2-diffx(x2, y2, x3, y3, th) > x2-diffx(x1, y1, x2, y2, th) || (x2 <= x1 && x2-diffx(x2, y2, x3, y3, th) < x2-diffx(x1, y1, x2, y2, th)))) {
polygon(
points = [
point1(x1, y1, x2, y2, th),
point2(x1, y1, x2, y2, th),
// This point connects this segment to the next
point1(x2, y2, x3, y3, th),
point3(x1, y1, x2, y2, th),
point4(x1, y1, x2, y2, th)
],
paths = [[0,1,2,3,4]]
);
}
else {
polygon(
points = [
point1(x1, y1, x2, y2, th),
point2(x1, y1, x2, y2, th),
point3(x1, y1, x2, y2, th),
point4(x1, y1, x2, y2, th)
],
paths = [[0,1,2,3]]
);
}
}
module 2dgraph(bounds=[-10,10], th=2, steps=10, polar=false, parametric=false) {
step = (bounds[1]-bounds[0])/steps;
union() {
for(i = [bounds[0]:step:bounds[1]-step]) {
if(polar) {
nextPolygon(polarX(i), polarY(i), polarX(i+step), polarY(i+step), polarX(i+2*step), polarY(i+2*step), th);
}
else if(parametric) {
nextPolygon(x(i), y(i), x(i+step), y(i+step), x(i+2*step), y(i+2*step), th);
}
else {
nextPolygon(i, f(i), i+step, f(i+step), i+2*step, f(i+2*step), th);
}
}
}
}
My wrapper code:
include <2dgraphing.scad>;
include <3dplot.scad>;
function z(x,y) = pow(x,2)+pow(y,2); //function used in 3dplot
function r(theta) = cos(4*theta); //function used in 2dgraph
module Petals () {
difference () {
union () { //everything to add
intersection () {
3dplot([-4,4],[-4,4],[50,50],-2.5);
scale([20, 20, 20]) linear_extrude(height=0.35)
2dgraph([0, 720], 0.1, steps=160, polar=true);
}
}
union () { //everything to subtract
}
}
}
Petals();
And all is well and dandy with the world when I render the world's most computationally expensive petals.
[Here I would post an image but since this is my first post I do not have the pre-requisite 10 reputation points]
However, now I want to subtract the excess from the bottom of the petals. So I could use a 3D plot with a steeper function and a lower starting point and subtract that from the original 3D plot.
So in the same program I want to use two different functions for two different uses of the 3Dplot module.
I tried modifying 3dplot and my code to do so:
Modified 3dplot:
module 3dplot(x_range=[-10, +10], y_range=[-10,10], grid=[50,50], z_min=-5, dims=[80,80], input_function)
{
dx = ( x_range[1] - x_range[0] ) / grid[0];
dy = ( y_range[1] - y_range[0] ) / grid[1];
// The translation moves the object so that its center is at (x,y)=(0,0)
// and the underside rests on the plane z=0
scale([dims[0]/(max(x_range[1],x_range[0])-min(x_range[0],x_range[1])),
dims[1]/(max(y_range[1],y_range[0])-min(y_range[0],y_range[1])),1])
translate([-(x_range[0]+x_range[1])/2, -(y_range[0]+y_range[1])/2, -z_min])
union()
{
for ( x = [x_range[0] : dx : x_range[1]] )
{
for ( y = [y_range[0] : dy : y_range[1]] )
{
polyhedron(points=[[x,y,z_min], [x+dx,y,z_min], [x,y,input_function(x,y)], [x+dx,y,input_function(x+dx,y)],
[x+dx,y+dy,z_min], [x+dx,y+dy,input_function(x+dx,y+dy)]],
faces=prism_faces_1);
polyhedron(points=[[x,y,z_min], [x,y,input_function(x,y)], [x,y+dy,z_min], [x+dx,y+dy,z_min],
[x,y+dy,input_function(x,y+dy)], [x+dx,y+dy,input_function(x+dx,y+dy)]],
faces=prism_faces_2);
}
}
}
}
Modified my code:
include <2dgraphing.scad>;
include <3dplot.scad>;
function z1(x,y) = pow(x,2)+pow(y,2); //function used in 3dplot
function z2(x,y) = pow(pow(x,2)+pow(y,2),1.5)-1; //function to be subtracted out
function r(theta) = cos(4*theta); //function used in 2dgraph
module Petals () {
difference () {
union () { //everything to add
intersection () {
3dplot([-4,4],[-4,4],[50,50],-2.5);
scale([20, 20, 20]) linear_extrude(height=0.35)
2dgraph([0, 720], 0.1, steps=160, polar=true, input_function=z1);
}
}
union () { //everything to subtract
3dplot([-4,4],[-4,4],[50,50],-2.5,input_function=z2);
}
}
}
Petals();
I received the following error: WARNING: Ignoring unknown function 'input_function'.
So how do I go about making passing in the function as a parameter?
I have not written in any functional language before this, but it is my understanding from the OpenSCAD User Manual that "Since version 2015.03, Variables can now be assigned in any scope." So I should be able to change the value of input_function for each run of 3dplot, just like the variables within 3dplot. Am I interpreting this incorrectly?
And as an optional side question: is there a clear way with OpenSCAD to achieve my current objectives without creating a massive computational load during the rendering process?
I have spent a decent enough amount of time trying to solve this problem that I am posting this lengthy question, and I apologize if I have glazed over a simple existing solution. I very much appreciate anyone willing to help.
The solution provided by is T.P. became obsolete in 2019. Now, yes, you can pass a function as a parameter to an OpenSCAD module or function.
OpenSCAD has Function Literals since 2019. These are Functions you can assign to variables and pass as parameters to other functions. I.e.,
Example:
sqr = function (x) x*x;
function g(x, f) = f(x+1);
echo(sqr(3));
echo(g(3, sqr));
Output:
ECHO: 9
ECHO: 16
Explanation: sqr
was defined as a function literal whose value is the anonymous function (also known as lambda)function(x) x*x
; and g
was defined as an old-style function that takes two parameters: a number and a function literal.
Functions are now becoming first-class citizens. You can have an algebra of functions in OpenSCAD, i.e. functions which take functions as parameters, and return other functions.
Example: function composition
f = function (x) x*x;
g = function(x) x + 1;
function comp(F, G) = function(x) F(G(x));
echo(comp(f,g)(3));
Output:
ECHO: 16
Explanation: f
and g
are function literals whose values are anonymous functions.
comp
is a function takes two function literals, F
and G
as parameters, and returns an anonymous function that is the composition F•G of F and G.
Since comp(f, g)
is a function, we can call it with an argument: comp(f, g)(3)
evaluates to f(g(3))
, which is 16.
Note: this is an equivalent definition of the function comp
as a function literal:
comp = function(F, G) function(x) F(G(x));
The value of comp
is an anonymous function that takes two function literals as parameters, and returns an anonymous function.
Example: Numeric Calculus (integration and differentiation) in OpenSCAD
sum = function(L) L*[for(i=L) 1];
S = function(F, dt=0.01) function(x) sum([for (t=[0:dt:x]) F(t) * dt]);
D = function(F, h=0.01) function(x) (F(x+h) - F(x))/h;
f = function (t) t*t;
echo(str("3 squared is ", f(3)));
echo(D(f)(5));
echo(S(f)(2));
echo(S(D(f))(3));
echo(D(S(f))(3));
Output:
ECHO: "5 squared is 9"
ECHO: 10.01
ECHO: 2.6867
ECHO: 9.0601
ECHO: 9.0601
Explanation:
D
is the numeric differential operator that, given a function F returns a numeric approximation of the derivative F' (with precision specified by h
). When F(t)=t², F'(t) = 2t, so F'(5)=10≈10.01.
sum
is a function that computes the sum of elements of a list or a range in OpenSCAD by using dot product. S
is an operator that takes a function F
, and returns a function that computes the integral of F
on the interval [0, x] numerically. When F(t)=t², ∫₀ˣF(t) dt = x³/3, so ∫₀²F(t) dt = 8/3 = 2.666... ≈ 2.6867.
The Fundamental Theorem of Calculus holds numerically: the composition the integral of the derivative of F is (approximately) equal to ΔF: ∫₀³F'(t)dt = F(3)-F(0) = 9 ≈ 9.0601 - and vice versa: (∫₀ˣF(t)dt)'(3) = F(3) = 9 ≈ 9.0601.
Note: the integral character ∫ is just a streched-out letter S, standing for sum. Alas, unicode characters in function names aren't supported in OpenSCAD as of 2025.
Example: higher order functions - compositions of operators
Using the definitions above, the following is valid:
echo(comp(D,S)(f)(7));
Output:
ECHO: 49.1401
Explanation:
comp
was defined as composition of functions, and passing in operators (functions that operate on functions) is kosher. So, comp(D,S)
is a composition of the differential operator and the integral operators (a function that takes a function as a parameter).
comp(D,S)(f)
is therefore a function, and can be called on a number: comp(D,S)(f)(7)
Now, we know that differentiation and integration "cancel each other" out by the FTC (the composition of operators is trivial); so numerically, comp(D,S)(f) ≈ f
.
Putting it all together, we have comp(D,S)(f)(7) = 49.1401 ≈ 49 = f(7)
You can pass functions as arguments to modules just as you would to functions.
Example:
module translateByF(F){
for (t=[0:0.01:1])
translate(F(t))
cube(0.1);
}
sqrpos = function(t) [t, t*t, 0];
translateByF(sqrpos);
Output:
Explanation: translateByF
is a module which applies a function (passed as a parameter) to a fixed range (equally spaced points on the interval 0..1), and translates a small cube by the positions returned by the function. sqrpos
is a function on the interval [0, 1] that defines a parametric curve: a planar parabola.
Note: modules in OpenSCAD can take named arguments of OpenSCAD types(numbers, strings, etc - and now, function literals), and indexed parameters in the children()
array that refer to geometry that you can pass into the module to operate on.
We can generalize the above example to make a module that "draws" a parametric curve in space with some other shape that we specify later:
Example:
module translateByF(F){
for (t=[0:0.01:1])
translate(F(t))
children();
}
helixF = function(t) [cos(360*3*t), sin(360*3*t), 5*t];
translateByF(helixF)
sphere(0.1);
Output:
Example: functional arguments default parameter value
Function literal arguments can have default values just like any other:
module translateConnect(posF, n = 100, I=[0,1],
sampleF = function(i, n, I) let(t=i/n) I[0] * (1-t) + I[1]*t)
{
samplePosF = function(i) posF(sampleF(i, n, I));
for (i=[0:n-1])
hull(){
translate(samplePosF(i))
children();
translate(samplePosF(i+1))
children();
}
}
knotF = function(t) [cos(360*3*t)*(3+cos(360*5*t)), sin(360*3*t)*(3+cos(360*5*t)), sin(360*5*t)];
translateConnect(knotF, 200)
sphere(0.1, $fn=9);
Output:
Explanation:
We build a solid shape by placing geometry given by children()
(in this case, a small sphere) along a parametric curve given by posF
on the interval I=[0,1]
. We take 200 samples from that interval.
The function that computes the i'th sample out of n from an interval I is sampleF
, and, by default, it is an anonymous function that samples an interval evenly.
The position of the i' th sphere is computed by the function samplePosF
, which plugs the i'th sample computed by sampleF
into the position function posF
.
Spheres placed into i'th and (i+1)st positions are then connected with hull()
to give a continuous curve.
Since modules are also a kind of function (that takes geometry inputs as well as named arguments), it would be natural to ask if you can have anonymous modules that you can assign to variables, i.e. module literals.
This is outside the scope of this question; but the answer is - this feature is in active development; see Module Literals WIP documentation.
OpenSCAD Functions (see: "Function Literals").