A friend wondered if there was someway to 3D print some of my curves/plots. Well, the ones so far have really been purely 2D images. But the gnarly versions do have a 3D look because of the the overlapping plots. But, I couldn’t see any obvious way to convert that to a truly 3D image. Nor how to add/generate surfaces to facilitate 3D printing.

But I decided to see what I could come up with. Turns out not much. But, I figured I’d make an effort to document what I did or didn’t did.

Basic Curve in 3D

This turned out to be more difficult than I expected. Mostly because of my lack of the necessary mathematical knowledge. But I did manage to get something working. A real hack, but I could at least generate some simple 3D curves. Though you really need to rotate them to see that.

The Plane Algorithm

Calling it an algorithm might be a bit pretensious. More like a methodology.

What I was hoping to do is just move my circles from 2D to 3D and let them generate a curve in 3D space. That however required me to sort out 2D planes in a 3D space. Each circle would be drawn on its own plane. I was also hoping to generate these planes mostly perpendicular to each other. Or at least to the previous one. With the current state of my mathematical skills, easier said than done.

Plane Equation

Two define that plane you need at a minimum a point on the plane and a vector normal to the plane.

Let's assume we have a point $$v = (v_1, v_2, v_3)$$ and a vector $$A = (a, b, c)$$ normal to the plane containing \(v\), then a point \(p = (x, y, z)\) is in the plane if: $$ax + by + cz = av_1 + bv_2 + cv_3$$

This is most often written as $$ax + by +cz = d$$

Set of Planes

Sadly, I couldn’t sort out any function that would generate the points and vectors I would need to generate a series of consecutively perpendicular planes. So, I resorted to generating them manually and hard coding a few into my test module.

I started by picking an arbitrary set of values for \((a, b, c, d)\) and then picking a set of values \((x, y, z)\) that satisfied the equation to get a point on the plane. The first set was \((7, 5, 3, 5)\) and \((1, 2, -4)\). I put these into two lists of lists. I.E.

plns = [[7, 5, 3, 5]]
nbr_p = len(plns)
u_vects = [[1, 2, -4]]

Now to get the set of values for a plane perpendicular to this first one, I sorted a vector that was perpendicular to the first (i.e. the dot product of the two would be \(0\). Looking at the first planes equation, \((1, 1, -4)\) should produce a dot product of \(0\). I.E. \( 7*1 + 5*1 + 3*-4 \) should equal \(0\). Which it does. I set \(d = 0\) and then the point \((2, 2, 1)\) would be on the plane. In the end I ended up with:

plns = [[7, 5, 3, 5], [1, 1, -4, 0], [4, 1, -4, 0], [1, 1, -1, 0], [2, 4, 3, 0], [2, 2, 4, 0]]
nbr_p = len(plns)
u_vects = [[1, 2, -4], [2, 2, 1], [1, 0, 1], [1, 1, 2], [1, -5, 6], [-4, 2, 1]]

There were others, but these are the ones I settled on for further testing. I wrote a little code to get the angles between those four planes. Where theta 01 means the angle between planes 0 and 1 (the numbers being indices into the plns array.) They’re not all sequentially perpendicular, but…

cos(theta01) = 0.0, theta 01: 90.0
cos(theta02) = 0.4013, theta 02: 66.340527057341
cos(theta03) = 0.5704, theta 03: 55.22187631876442
cos(theta04) = 0.8765, theta 04: 28.77699973254362
cos(theta05) = 0.8066, theta 05: 36.234940012571556
cos(theta12) = 0.8616, theta 12: 30.503291748716187
cos(theta13) = 0.8165, theta 13: 35.264050375332566
cos(theta14) = -0.2626, theta 14: 105.22439304642435
cos(theta15) = -0.5774, theta 15: 125.26787950374793
cos(theta23) = 0.9045, theta 23: 25.243974563819982
cos(theta24) = 0.0, theta 24: 90.0
cos(theta25) = -0.2132, theta 25: 102.30994665430188
cos(theta34) = 0.3216, theta 34: 71.24028626819404
cos(theta35) = 0.0, theta 35: 90.0
cos(theta45) = 0.9097, theta 45: 24.536073378029034

And here’s a very simple look at those planes.

3D plot of the planes defined above

Getting a Circle on a Plane

Now, to get a circle on one of those planes is a bit more work. Lots of searching, reading and cogitating. In the end I settled on:

def get_circle(pln, u_v, rd, frq, ppt):
  pnd = np.linalg.norm(pln[:3])
  pn = pln[:3] / pnd
    
  efgn = np.linalg.norm(u_v)
  ev = u_v / efgn
  axb = pn * ev

  t = np.linspace(0, 2*np.pi, ppt) * frq

  x = rd * np.cos(t) * ev[0] + rd * np.sin(t) * axb[0]
  y = rd * np.cos(t) * ev[1] + rd * np.sin(t) * axb[1]
  z = rd * np.cos(t) * ev[2] + rd * np.sin(t) * axb[2]

  return x, y, z

Where pln is the tuple defining the plane equation, u_v is a vector perpendicular to the plane, rd is the circles radius, frq is the rotational frequency for the circle and ppt is the number of plotting points to use. We normalize our vectors. ev is a unit vector from the center of the circle to a point on the circumference. pn is the normal of a vector perpendicular to the plane. Sounds much simpler than the time and effort it took me to sort it out.

3D Curve

From here I proceeded pretty much as before. Some random number of circles. Random radii and frequencies for each. For each circle I also choose a plane to draw it on at random from the set of available planes. Each row in the resulting data array just added the latest circles result to that for the previous circle. The last row in the array is our curve.

Something like:

c_plns = [np.random.randint(0, nbr_p) for _ in range(n_whl)]

pxs, pys, pzs = [], [], []
px, py, pz = get_circle(plns[c_plns[0]], u_vects[c_plns[0]], r_wds[0], freqs[0], t_pts)
# ax.plot(px, py, pz)
pxs.append(px)
pys.append(py)
pzs.append(pz)

for i in range(1, n_whl):
  xs, ys, zs = get_circle(plns[c_plns[i]], u_vects[c_plns[i]], r_wds[i], freqs[i], t_pts)
  # ax.plot(xs, ys, zs)
  pxs.append(np.add(pxs[i-1], xs))
  pys.append(np.add(pys[i-1], ys))
  pzs.append(np.add(pzs[i-1], zs))

p_spiro = ax.plot(pxs[-1], pys[-1], pzs[-1], lw=ln_w)

Now because I had trouble visualizing a 3D curve rendered in 2D, I used animation to rotate the curve around some angle relative to the x-y axis. Didn’t want to include the animation in the post. But here’s a couple of examples you can download if you wish.

I did try a gnarly plot or two. But nothing special and it took forever to save the animation to a file. And, I wasn’t using a lot of data points. Still took best part of 15 minutes to save the one below. So I very quickly gave up on them.

Maybe Done

I was also going to look at generating surfaces on the curves in this post. But, at the moment I am thinking we’ve covered more than enough ground for this one. Though who knows what will happen before I publish it.

Until next time, enjoy coding in 3D space.

Resources