Introduction: 3D Modeling SPACESHIP EARTH ...without CAD Software
This tutorial will explain my process for 3D modeling Spaceship Earth, the 180 ft tall aluminum-clad geodesic sphere at EPCOT Center in Florida [1], without the use of CAD software.
Wait a minute, did you say “without CAD software”?
I did. The 3D-print in the title photo came from a computer-model made without the use of a solid-modeling CAD program. Not Fusion 360, not FreeCAD, and not even OpenSCAD!
How?
I wrote a small C program that calculates and outputs an STL file of Spaceship Earth. I used no APIs or outside libraries (except for the C standard library, of course). This tutorial explains the design—and the important concepts that went into the design—of that program. My hope is to explain things well enough for you to intuitively understand how the program works. That being said, this isn’t a tutorial on programming; to fully understand what’s going on, you’ll need at least some prior programming experience (any language is fine), as well as some familiarity with vectors.
The STL model of Spaceship Earth is available to download right here; feel free to 3D print it, even if you don't read the rest of this tutorial :) . If you are interested in how the program works, keep reading. The source code is available to download at the end.
[Spaceship Earth photo] photo: By Katie Rommel-Esham, CC BY-SA 3.0 us, [https://commons.wikimedia.org/w/index.php?curid=2987359]
[1] [https://en.wikipedia.org/wiki/Spaceship_Earth_(Epcot)]
Attachments
Step 1: The Geometry of Spaceship Earth
At first glance, the surface of Spaceship Earth seems to consist of perfect, uniform triangles. However, this is not the case; if it were, Spaceship Earth would be flat![1] Each triangle is precisely distorted to make the structure spherical. These distortions cause some subtle inconsistencies in the pattern of triangles; by analyzing the inconsistencies, we can reverse-engineer the pattern.
If you look closely at the sphere, ignoring the pyramid-like points for now, you’ll notice that while most of the triangles form little hexagon-groups of six, some of them form little pentagon-groups of 5. Try expanding these small pentagons outward and you’ll notice that the surface of Spaceship Earth can be entirely covered by several large, identical pentagons. Exactly 12 pentagons will cover the sphere, and they do so in the shape of a Regular Dodecahedron [2].
A regular dodecahedron will be the starting point for the program. From there,to reconstruct Spaceship Earth, our program will need to divide each pentagon into a number of triangles.
Before designing a process for making the divisions, let’s consider our goal here: the objective of our program is to output a 3D model of Spaceship Earth, preferably one that can be 3D printed. But how exactly are 3D models made? More precisely, how does one describe a three-dimensional shape in a way unambiguous to a computer?
The answer lies in the STL file format.
[Regular dodecahedron illustration] By Created by en:User:Cyp and copied from the English Wikipedia. - not stated, CC BY-SA 3.0, [https://commons.wikimedia.org/w/index.php?curid=38668]
[1] Equilateral triangles form a regular tiling, which is naturally flat. [https://en.wikipedia.org/wiki/Euclidean_tilings_by_convex_regular_polygons#Regular_tilings]
Step 2: The STL File Format
If you’ve ever 3D-printed anything, you’ll recognize the STL file as what you give to a slicer to produce g-code for your printer. Normally you would export one from a CAD program or download one from a 3D print repository, but our program will generate one from scratch.
An STL file describes the outer surface of a 3D shape using triangles. Any surface can be approximated by a mesh consisting solely of triangles—even curved ones, if you use enough triangles (that is, thousands and thousands of triangles). Lucky for us, Spaceship Earth already consists of nothing but triangles; we can use the STL format to exactly describe—not approximate—the geometry of Spaceship Earth.
The reason for using only triangles to describe a 3D shape (as opposed to more representative geometry) is that triangles are easy to mathematically describe on their own. This in turn makes the STL file format very simple. In fact, here’s the whole format:
solid name facet normal ni nj nk outer loop vertex v1x v1y v1z vertex v2x v2y v2z vertex v3x v3y v3z endloop endfacet [... repeat for each triangle] endsolid name
[1] Each facet (everything from “facet” to “endfacet” above) represents one triangle. An STL file is simply a long list of these facets; the order of which does not matter. What does matter is that, when put together, the facets form an enclosed mesh with no holes. The STL format won’t stop you from making impossible shapes; but if you give such a shape to a 3D print slicing program, you’ll get errors. If we’re careful in designing our Spaceship Earth program, this won’t be an issue.
There are two parts to each triangle-facet: a normal vector (“normal ni nj nk”) and a list of three vertices (listed between “outer loop” and “end loop”). The normal vector is explained more in the next section; in short, it identifies which direction the “outside” surface of the triangle is. The 3 vertices are just the 3 points of a triangle. Each point gets an xyz coordinate, and together the points fully describe the size, shape, and location of a particular triangle in three-dimensional space.
The key things to understand about STL files are:
1. An STL file is just a list of triangles.
2. A triangle is 3 points.
3. “Making” a 3D model in the STL format means: calculate the points, group them into triangles, and print them to a text file.
Notice what’s apparently left out of the STL file: it makes no mention of “edges” or “surfaces” or “solid objects”. The STL format has no concept of what those things are; all it knows is “points” and “groups of points”. That information alone is enough to define an object that can be printed into the real world. Therefore, the primary function of our program will be to create and group points.
[STL model diagram] By User:LaurensvanLieshout - File:STL-file.jpg, CC BY-SA 3.0, [https://commons.wikimedia.org/w/index.php?curid=34722631]
[1] The Wikipedia was my main source for understanding how STL files work, and is also the source of this format example [https://en.wikipedia.org/wiki/STL_(file_format)]
Step 3: Vectors
The obvious way to deal with points in a program would be to create a little “point” struct data type [1] consisting of 3 numbers: x, y, and z coordinates. But, for reasons not yet clear, it might be helpful to generalize this “point” struct into a “vector” struct [2].
Now, if you aren’t familiar with the concept of a vector, you’ll want to read up on it before continuing [3]. I can’t explain it here; this tutorial is already long enough :) . In short, a vector is a geometric concept consisting of a magnitude (length) and a direction [4]. It is a man-made tool, useful for representing the physical world in a way easy to analyze with math.
Our “vector” struct looks very similar to the previous “point” struct. It too contains 3 values corresponding to x, y, and z. However, instead of saying “this point is located at xyz”, the vector says “this vector points x amount in the x-direction, y amount in the y direction, and z amount in the z direction”. In terms of code, the “vector” struct looks identical to what would have been the “point” struct; the difference is in how we interpret the meaning of it.
To represent a point with our new “vector” struct, we can create an instance of the struct and give it the same x, y, and z values as its xyz coordinate. Now, instead of calling our point a “point”, well call it a “position vector”; and instead of saying “Our point is located at xyz.”, we’ll say “This position vector points from the origin (0, 0, 0) to our point (x, y, z).”. Once again, none of this is reflected in the code of the program; it’s all about how we think about what the code means in the real world.
The critical reason for representing “points” as “position vectors” is that we’ll be using vectors in other ways in our program; by representing everything as a vector, the program is more simple.
Before continuing to the main portion of the program, we’ll first need to write a few general-purpose functions for dealing with vectors:
vector midpoint(vector A, vector B)
- Takes two points (position vectors). Returns a new point equidistant to the two given points.
vector scale(vector A, double Length)
- Takes a vector and a value. Returns a new vector with length of the given value and pointing in the direction of the given vector.
vector normal(vectors[A, B, C, ...])
- Takes three points (position vectors). Returns a unit vector (a vector of length 1) with direction perpendicular to the plane containing the given points. It works by making two vectors out of the 3 points and taking the cross-product. A property of cross-products is that the resultant vector is always perpendicular to the to the two vectors it was calculated from. To turn the resultant vector into a unit vector, we can use the scale() function from before.
If you haven’t programmed or worked with vectors before, this section likely didn’t make much sense. But even without understanding the math I think the next sections are still interesting, so keep reading.
[1] ...or object, or class, or some other grouping of data (depending on the programming language).
[2] Our “vector” struct has no relation to the C++ vector<> data type.
[3] Khan Academy seems to have good instruction material on vectors. Vectors and notation: [https://www.khanacademy.org/a/vectors-and-notation-mvc]. Cross-products: [https://www.khanacademy.org/a/cross-products-mvc].
Step 4: Subdividing Spaceship Earth
The diagram above illustrates how our program will construct Spaceship Earth. It starts with the regular dodecahedron from earlier, and systematically divides each pentagonal surface into triangles. Then, it takes each of those triangles and divides them further into more triangles, repeatedly. This is achieved through several program functions; the results of one function will be passed as input to the next. The following sections describe this process in more detail.
Step 5: Starting With a Dodecahedron
Since the desired output of our program is essentially a bunch of points, lets start by representing the dodecahedron in our program as a collection of points. The Wikipedia entry on regular dodecahedrons says:
The following Cartesian coordinates define the 20 vertices of a regular dodecahedron centered at the origin and suitably scaled and oriented:
(±1, ±1, ±1)
(0, ±ϕ, ±1/ϕ)
(±1/ϕ, 0, ±ϕ)
(±ϕ, ±1/ϕ, 0)
where ϕ = (1 + √5)/2 is the golden ratio (also written τ) ≈ 1.618. The edge length is 2/ϕ = √5 − 1. The circumradius is √3. [1] [2]
Unfortunately, I could not find an elegant way of organizing these vertices in the program. I took a brute-force approach instead, by declaring position vectors for each of the 20 vertices and carefully assembling them into 12 arrays (one for each pentagonal face) of 5 vertices each. It wasn’t easy or pretty, but it works.
At this point, we have a list of pentagons, each with 5 points. Note that radius of the points as given is √3. To make later calculations simpler, we should adjust each point to sit on a sphere of radius 1 [3]. To reduce the distance from √3 to 1, run each point through the scale() function from earlier.
[Dodecahedron diagram] By Another Matt - Self-drawn diagram, CC0, [https://commons.wikimedia.org/w/index.php?curid=27601166]
[1] [https://en.wikipedia.org/wiki/Regular_dodecahedron]
[2] There must be a way of mathematically deriving these coordinates, but I don’t know what that way is and it’s likely not simple. Please excuse the hand-waving.
[3] 3D print slicers can handle scaling the model up as needed. This won't affect the proportions of the model.
Step 6: Subdividing the Pentagons
Looking back at our geometric analysis of Spaceship Earth, remember that at the center of each large pentagon was a small cluster of 5 triangles. Drawing lines from this center to each vertex of the big pentagon neatly subdivides the large pentagon into 5 large triangles. Now, how do we program this?
First, let’s find that center of the pentagon. To do this, we can use the property that the cross-section of a sphere is always a circle, and the vector pointing from the origin to the center of that circle is always perpendicular (see diagram above) [1] [2]. The 5 points of one of our pentagons all lie on one of these circular cross-sections. If we apply the normal() function from earlier to the 5 points of the pentagon, we'll get a vector that—if viewed as a position vector from the origin—points directly to the center of the pentagon.
Take the 5 points, along with the calculated center point, and arrange them into 5 arrays of 3 points (one for each triangle).
To do this for each of the pentagonal faces, we’ll write a function called subdiv_pentagon() that will accept an array of points for one pentagon and generate arrays of points for each of the 5 resultant triangles. The function will pass these triangles to the next function, subdiv_triangle().
[Sphere diagram] By Ag2gaeh - Own work, CC BY-SA 4.0, [https://commons.wikimedia.org/w/index.php?curid=67458129]
[1] [https://en.wikipedia.org/wiki/Sphere#Circles]
[2] [https://en.wikipedia.org/wiki/Circle_of_a_sphere]
Step 7: Subdividing the Triangles
The subdiv_triangle() function takes 3 points as input, representing a triangle. It divides the triangle into 4 smaller triangles by finding the midpoint of each edge, using the scale() function to project the midpoint onto the sphere, and rearranging the points into 4 arrays of 3 points (one array for each triangle).
The large triangles coming from subdiv_pentagon() need to be divided into smaller triangles a total of 3 times. This can be done with recursion. In addition to the set of 3 points, this function will also accept a "number of divisions" parameter. When the function runs, it decreases this number by 1 and passes each of the four generated triangles to another instance of the function. This repeats until the number of divisions requested is zero; the generated triangles are then passed to the next function, peak_triangle().
Step 8: Forming the Triangle Peaks
This function works similarly to the subdiv_pentagon() function from earlier (use the normal() function to get the center). The only difference is that the center point needs to be scaled out a little past 1 to create the peaks. I couldn't find an exact scale length to use from my research on Spaceship Earth, but through trial and error I found that a value of 1.025 looks close to the amount used in the real structure.
The resulting 3 triangles are each passed to the STL output function, print_triangle().
Step 9: STL Output Function
print_triangle() accepts 3 points as input and prints out the triangle's information in the STL format. Instead of messing around with opening and closing files, the program prints the contents of the STL file onto standard output. If you run the program from a Unix command line shell (like the Linux or MacOS terminal application), you can redirect the output of the program to a file by running "./program >filename". If you run the program from an IDE or on another operating system, you'll have to modify it to generate a file itself.
Step 10: 3D Printing the Model
This is a challenging model to print, due to its severe overhangs; I used a Prusa MINI to print the model, and it seemed to handle it well.
For slicer settings, I used the 0.20mm Speed PLA profile on Prusa Slicer to begin, and made modifications to reduce the print time from 12hr to 8hr 30min. However, it seems my optimizations were too aggressive, because the top of the print ended up with small pinhole defects; the top layers weren't thick enough. It wasn't too bad though, and I was happy with the overall result.
Step 11: Remarks
Thoughts about the project, in no particular order:
Some time before doing this project, I attempted to model Spaceship Earth using SolidWorks. It was not an enjoyable process. Even strategies like pulling calculated values from a spreadsheet were not enough to effectively manage the complexity of the model. Moreover, the model felt very fragile, as if any further modifications would break the model and render it unfixable. Full featured solid modeling software is a necessity for most 3D printing projects; however, due to the mathematical nature of this design, I needed a more precise, manual approach.
Even though the program I wrote works just fine, it doesn't feel like it should. Where did the 3D model come from? There were no extrusions, no revolutions, no cuts; only calculations and some text output at the end. That's not 3D modeling! To a CAD user its not; but to a CAD program, 3D modeling is exactly that: lots and lots of calculations.
While I did spend a good amount of time researching and thinking about the problem of 3D modeling Spaceship Earth, I only spent about 5 hours one afternoon writing the program (start to finish). By far, I spent the most time writing this tutorial; explaining things is difficult!
The first time I ran the program, it produced the wad of triangles show above. 17 or so bug-fixes later, the program worked.
There are likely programming languages better suited than C for this project (having to constantly declare and initialize arrays to group points together was a bit cumbersome; maybe Lisp instead?). However, the manual nature of C seemed appropriate for manually trying to 3D model something.
The title photo is a real photo. I was able to make it look like it was floating by using a curved poster board as the background, shining a light on it from below, and—surprisingly—by turning the photo upside-down afterwards. I tweaked the colors a bit, but other than that I didn't manipulate the photo.
Attached below is the source code for my program (I would have bundled them into a ZIP file, but Instructables does not support zip files). If something in this tutorial doesn't make sense, please ask in the comments. I'll edit the tutorial to clarify things as needed.