Function grapher


There are a function f(x, y) and a given region in the xy-plane. If we can create and print a model, we'll be able to touch the function physically. Will mathematical functions be interesting?

Function grapher

It's not difficult when modeling is based on programming. There are several ways to do that. Which way would be best depends on what your requirement is.

The simplest way

Placing a small cube at every point [x, y, f(x, y)] is the simplest way. If every cube is small enough, all cubes will compose the function graph. The code is easy to understand as well.

function f(x, y) = (pow(y,2)/pow(2, 2))-(pow(x,2)/pow(2, 2));

min_value = -3;
max_value = 3;
resolution = 0.5;
thickness = 1;

for(x = [min_value:resolution:max_value]) {
    for(y = [min_value:resolution:max_value]) {
        translate([x, y, f(x, y)]) 
            linear_extrude(thickness, center = true)
                square(resolution, center = true);
    }
}

Function grapher

In the above code, resolution determines what the next x or y is. A smaller resolution creates a smoother graph but takes more time to render. The exported model file is larger, too. The figure below using resolution = 0.01. It takes more than one minute to show the preview on my computer.

Function grapher

It's a trade-off between resolution and smoothness for a model. Even in the way introduced later, you also have to balance these two opposing qualities. When creating a smoother graph, however, the above way costs more indeed.

Introducing polyhedron

If you want a smoother graph but a relatively lower cost, try the built-in polyhdedron module. In the document “Line“, we talked about the polygon module which can create a multiple sided shape from a list of x, y coordinates. You may think the polyhdedron module is a 3D version of the polygon module. The polyhdedron module can be used to create any regular or irregular shape.

Even the polyhdedron module is like a 3D version of the polygon module, it's more complex than using the polygon module. Simply put, you have to know and index every vertex of the polyhedron. The official document of polyhedron gives a simple example. To generate cube([ 10, 7, 5 ]), you have to index it's eight vertices.

Function grapher

Then, figure out the indices used by each face.

Function grapher

After that, invoke polyhedron with the vertices and the vector of faces.

CubePoints = [
  [  0,  0,  0 ],  //0
  [ 10,  0,  0 ],  //1
  [ 10,  7,  0 ],  //2
  [  0,  7,  0 ],  //3
  [  0,  0,  5 ],  //4
  [ 10,  0,  5 ],  //5
  [ 10,  7,  5 ],  //6
  [  0,  7,  5 ]]; //7

CubeFaces = [
  [0,1,2,3],  // bottom
  [4,5,1,0],  // front
  [7,6,5,4],  // top
  [5,6,2,1],  // right
  [6,7,3,2],  // back
  [7,4,0,3]]; // left

polyhedron( CubePoints, CubeFaces );

A simple example, right? What will happen if we change the z value of the vertex 6 from 5 to 7?

CubePoints = [
  [  0,  0,  0 ],  //0
  [ 10,  0,  0 ],  //1
  [ 10,  7,  0 ],  //2
  [  0,  7,  0 ],  //3
  [  0,  0,  5 ],  //4
  [ 10,  0,  5 ],  //5
  [ 10,  7,  7 ],  //6
  [  0,  7,  5 ]]; //7

CubeFaces = [
  [0,1,2,3],  // bottom
  [4,5,1,0],  // front
  [7,6,5,4],  // top
  [5,6,2,1],  // right
  [6,7,3,2],  // back
  [7,4,0,3]]; // left

polyhedron( CubePoints, CubeFaces );

Function grapher

The vertices indexed from 4 to 7 are not on the same face. OpenSCAD will try to slice the rectangle into triangles automatically in this situation because three points must compose a face. (That's why you need a three-legged stool on the uneven ground.) There's no warning message when you render the model.

However, multiple polyhedron operations may generate a warning message. (It seems that OpenSCAD will merge some calculation.)

PolySet has nonplanar faces. Attempting alternate construction

For example:

points = [
    [0, 0, 1], [1, 0, 2], 
    [1, 1, 4], [0, 1, 1], 
    [0, 0, 2], [1, 0, 3], 
    [1, 1, 5], [0, 1, 2]
];

points2 = [ 
    [1, 0, 2], [2, 0, 2], 
    [2, 1, 0], [1, 1, 4], 
    [1, 0, 3], [2, 0, 3], 
    [2, 1, 1], [1, 1, 5],     
];

faces = [
      [0,1,2,3],  // bottom
      [4,5,1,0],  // front
      [7,6,5,4],  // top
      [5,6,2,1],  // right
      [6,7,3,2],  // back
      [7,4,0,3]   // left
]; 

polyhedron(points, faces);
polyhedron(points2, faces);

If you delete one of the polyhedron operation in the above code and render, "PolySet has nonplanar faces" won't appear; however, rendering both causes the message. To avoid this problem, we may try to slice a face into triangles by ourselves.

Slicing a face into triangles

For function graphs, it's easy to slice a face into triangles because we are increasing the value of x, y constantly. For example, given the points below.

Function grapher

For any point, we may take its right and upper-right points to compose a triangle.

Function grapher

And, we may take its upper and upper-right points to compose a triangle, too.

Function grapher

Now, we slice a rectangle into two triangles. We can continue this process until slicing all rectangles.

Function grapher

For all [x, y, f(x, y)] points, we group them into groups of three. For each f(x, y) of a triangle, we can use f(x, y) - thickness to create a bottom face. After that, we can use the polyhedron module to draw it.

Function grapher

Follow the same process for all triangles, you can create a function graph.

points = [
    [[0, 0, 1], [1, 0, 2], [2, 0, 2], [3, 0, 3]],
    [[0, 1, 1], [1, 1, 4], [2, 1, 0], [3, 1, 3]],
    [[0, 2, 1], [1, 2, 3], [2, 2, 1], [3, 2, 3]],
    [[0, 3, 1], [1, 3, 3], [2, 3, 1], [3, 3, 3]]
];

thickness = 1;

faces = [
    [0, 1, 2],
    [3, 4, 5],
    [0, 1, 4, 3],
    [1, 2, 5, 4],
    [2, 0, 3, 5]
];
z_offset = [0, 0, -thickness];

for(yi = [0:len(points) - 2]) {
    for(xi = [0:len(points[yi]) - 2]) {
        tri1_top = [
            points[yi][xi], 
            points[yi][xi + 1], 
            points[yi + 1][xi + 1]
        ];
        tri1_bottom = [
            tri1_top[0] + z_offset, 
            tri1_top[1] + z_offset, 
            tri1_top[2] + z_offset
        ];

        tri2_top = [
            points[yi][xi], 
            points[yi + 1][xi + 1], 
            points[yi + 1][xi]
        ];
        tri2_bottom = [
            tri2_top[0] + z_offset, 
            tri2_top[1] + z_offset, 
            tri2_top[2] + z_offset
        ];

        polyhedron(
            points = concat(tri1_top, tri1_bottom), 
            faces = faces
        );

        polyhedron(
            points = concat(tri2_top, tri2_bottom), 
            faces = faces
        );
    }
}

The built-in concat module returns a vector containing the arguments of the given vectors. The preview is as below.

Function grapher

But, rendeing it will generate a warning message.

WARNING: Object may not be a valid 2-manifold and may need repair! 

Basically, it's not the problem of our code. The polyhedron should be “closed” explicitly by making every adjacent face really “attached” to each other. It's the problem of floating point precision when calculating. The problem causes non-attached faces. One simple way to solve the problem is using the hull operation. The code below also shows to use a mathematical function to draw a graph.

function f(x, y) = (pow(y,2)/pow(2, 2))-(pow(x,2)/pow(2, 2));

min_value = -3;
max_value = 3;
resolution = 0.5;
thickness = 1;

points = [
    for(y = [min_value:resolution:max_value])
        [
            for(x = [min_value:resolution:max_value]) 
                [x, y, f(x, y)]
        ]
];

faces = [
    [0, 1, 2],
    [3, 4, 5],
    [0, 1, 4, 3],
    [1, 2, 5, 4],
    [2, 0, 3, 5]
];
z_offset = [0, 0, -thickness];

for(yi = [0:len(points) - 2]) {
    for(xi = [0:len(points[yi]) - 2]) {
        tri1_top = [
            points[yi][xi], 
            points[yi][xi + 1], 
            points[yi + 1][xi + 1]
        ];
        tri1_bottom = [
            tri1_top[0] + z_offset, 
            tri1_top[1] + z_offset, 
            tri1_top[2] + z_offset
        ];

        tri2_top = [
            points[yi][xi], 
            points[yi + 1][xi + 1], 
            points[yi + 1][xi]
        ];
        tri2_bottom = [
            tri2_top[0] + z_offset, 
            tri2_top[1] + z_offset, 
            tri2_top[2] + z_offset
        ];

        // hull them to avoid non-attached faces
        hull() polyhedron(
                points = concat(tri1_top, tri1_bottom), 
                faces = faces
            );

         hull() polyhedron(
                points = concat(tri2_top, tri2_bottom), 
                faces = faces
            );
    }
}

It's smoother than the previous graph created by cubes, right? And, the rendering speed is fast, too.

Function grapher

You may try to improve this example. For example, if you want to draw the mesh of a function graph, how can you do?

Function grapher

In my example code, we slice each rectangle from the bottom-left to the upper-right. How about providing an option to slice from the upper-left to the bottom-right?

Function grapher