functional-programming3dopenscad

How do I pass a function as a parameter to an OpenSCAD module?


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.


Solution

  • Passing a function as parameter is currently not possible. Also generating a huge number of small objects (e.g. the polyhedrons in the 3dplot module) will make the model rendering very slow. For this specific use-case there are other options to generate the model.

    The new list generation features available with the latest OpenSCAD versions allows to generate a single polyhedron based on a function.

    See the 3d-functions.scad in the demo repository. This plots the function f(x, y).