Skip to main content Link Search Menu Expand Document (external link)

3D Printing & Scanning

Table of contents
  1. 3D Printing & Scanning
    1. 3D Printing
      1. Group Assignments for 3D Printing
      2. Design a Model Best Suitable for 3D Printing
        1. Gyroid and Minimal Surfaces
      3. Minimal Surface Design
      4. Marching Cubes to Create Minimal Surfaces
      5. Intersecting with an Input Shape
      6. Final Design of Gyroid-Intersected Spotted Cow
      7. 3D Printing the Gyroid-Intersected Spotted Cow
      8. 3D Printing Results PhotoBooth
      9. Fun Facts
      10. Code Snippets
    2. 3D Scanning
      1. 3D Scanning Results – Wooden Dog
      2. 3D Scanning Results – Mannequin
    3. Acknowledgement

3D Printing

Group Assignments for 3D Printing

Test the design rules for your 3D printer(s).

Using the 3D printer (Stratasys F120 FDM printer and Prusa i3 SLA printer), we tested the design rules (overhang, clearance with supports; angle, overhang, bridging, wall thickness, dimensions, anisotropy, surface finish, and infill without supports) for 3D printing as shown in Week 3 of EECS Group Assignments.

Design a Model Best Suitable for 3D Printing

As argued by Anthony, we should design an object that could not be made subtractively. Anthony further explained that imagining we have an infinitely small hammer (primitive but yet powerful tool), what cannot be made using human hands within a reasonable amount of time? Echoing Anthony’s example of the twisted rook sample by Formlabs and Neil’s example of Lingdong’s Chinese Puzzzle Ball, I summarized two features best suitable for 3D printing.

  • Feature 1. Smooth and irregular surfaces.
  • Feature 2. Openwork structures inside.

Bearing these two features in mind, I searched a while for something really fascinating to me.

The first series of models caught my eyes were those by artist Bathsheba Grossman, who also had an SIGGRAPH exhibition in 2008. Three examples (Ora, Metatrino, and Quintrino) are shown below. Quintrino also had a very appealing application as the Quin table lamp. image

After struggling a bit trying to reimplementing the design (as implicitly shown in Acknowledgement), I decided to look into minimal surfaces, which is equally fascinating but more practical considering limited time budget. More important reason is that Bathsheba stated in her FAQ that she doesn’t want anyone to reverse engineering her design because those are of copyright.

Gyroid and Minimal Surfaces

Gyroid is also among Bathsheba’s art collection. The design behind it is minimal surface, which is a surface that locally minimizes its area (energy). Geometrically speaking, it’s equivalent to having zero mean curvature. Two intuitions made me even more fascinated by minimal surfaces. One physical intuition is that minimal surfaces are the most stable surfaces, in the sense that nature optimizes energy to form the shape of minimal surfaces on soap films. Another materials science intuition is that gyroids can form one of the strongest, lightest materials in 3D graphene. Therefore, it can be also used as the infill structure for 3D printing. image

Minimal Surface Design

Therefore, inspired by Lingdong’s voxel-to-mesh solution using marching cubes and a nice C example of Schwartz’s Diamond-surface and Primitive-surface, I decided to write a simple code to generate minimal surfaces. Since, for example, the gyroid or Schoen-Gyroid surface is simply defined as the surface of the following algebraic equation:

\[\sin X \cos Y + \sin Y \cos Z + \sin Z \cos X = 0\,,\]

where $X=2\alpha\pi x, Y=2\beta\pi y, Z=2\gamma\pi z$, and $\alpha, \beta, \gamma$ are constants related to the unit cell size in the $x, y, z$ directions.

Since this equation only defines a surface without any thickness and would be impossible to print, I followed this MSLattice paper and used simple uniform relative density to generate sheet-networks minimal surfaces (or triply periodic minimal surfaces, TPMS, to be exact). That is for the above gyroid equation, we set a threshold value $t$ to define the thickness of the sheet-networks:

\[|\sin X \cos Y + \sin Y \cos Z + \sin Z \cos X| \le t\,.\]

As shown in the code, I also implemented the rest of seven types of minimal surfaces, as shown in MSLattice paper. The following image is also from Figure 1 of MSLattice paper. image

Marching Cubes to Create Minimal Surfaces

After getting the minimal surfaces in voxel representation, as it’s intuitive to implement using matrix-based operations in Python, I used the marching_cubes() function skimage.measure.marching_cubes() provided by scikit-image to generate the mesh representation from voxels.

Intersecting with an Input Shape

In order to make the 3D model a bit more interesting and echoing the idea that gyroid can be an efficient infill structure, I decided to intersect the minimal surface with a simple input shape.

Final Design of Gyroid-Intersected Spotted Cow

Here is the final deisng of intersecting the simple input shape (spotted cow from Keenan Crane) with the designed gyroid minimal surface.

3D Printing the Gyroid-Intersected Spotted Cow

I used Sindoh 3DWOX 1 SLA printer to print the model. And put the model side down, which turned out to be a bad decision and made the left side of the printed object in a bad shape. And it literally took almost 10 hours to print the model of size ~10cm x 10cm x 4cm. I do not want to waste more machine time to reprint it, but next time I’ll just put the model right standing up.

image
Sindoh 3DWOX 1 SLA printer
image
Estimated time of 9 hours 53 mins

A time-lapse video of 3D printing the gyroid-intersected spotted cow showing in action:

3D Printing Results PhotoBooth

image
Left side of the Gyroid-intersected spotted cow (before sanding)
image
Left side of the Gyroid-intersected spotted cow (after sanding)
image
Printing Gyroid-intersected spotted cow (neck position)
image
Right side of the Gyroid-intersected spotted cow
image
Printed Metatron (before removing dissolvable materials) using Stratasys F120 FDM printer
image
Printed Metatron (after removing dissolvable materials) using Stratasys F120 FDM printer

image
3D Printed Gyroid-intersected spotted cow and Metatron.

Fun Facts

image
Failed to use MSLattice after successful installation and configuration on Ubuntu 22.04.
image
Failed to compile and use libfive even without Guile bindings

image
GitHub CoPilot is smart enough to know exactly what I want to code from previous lines and current comment line.

Code Snippets

gen_minimal_surfaces.py

[view on GitHub Gist]

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
''' 
Initial code from [MaSMaker](https://github.com/CONMAD-CIDESIMX/MaSMaker)
Reference: Tenorio-Suárez, M. I., Gómez-Ortega, A., Canales, H., Piedra, S., & Pérez-Barrera, J. (2022). MaSMaker: An open-source, portable software to create and integrate maze-like surfaces into arbitrary geometries. SoftwareX, 19, 101203.
Adapted by [Yang Liu](yangliu.mit.edu) in HTM(A)A, Fall 2022.
'''
import os
import trimesh
import pycork
import numpy as np
from pathlib import Path
from numpy import sin, cos, pi
from skimage.measure import marching_cubes

# Triply periodic minimal surface (TPMS) Algebraic Equations
def tpms_equation(x, y, z, a, t, type="gyroid"):
    cox = cos(2.0*pi*x/a)
    siy = sin(2.0*pi*y/a)
    coy = cos(2.0*pi*y/a)
    siz = sin(2.0*pi*z/a)
    coz = cos(2.0*pi*z/a)
    six = sin(2.0*pi*x/a)

    co2x = cos(4.0*pi*x/a)
    co2y = cos(4.0*pi*y/a)
    co2z = cos(4.0*pi*z/a)
    si2x = sin(4.0*pi*x/a)
    si2y = sin(4.0*pi*y/a)
    si2z = sin(4.0*pi*z/a)

    if type == "gyroid":
        # Gyroid surface [Schoen-Gyroid]: sin X cos Y + sin Y cos Z + sin Z cos X = c
        return (six*coy + siy*coz + siz*cox)**2 - t**2
    elif type == "diamond":
        # Diamond surface [Schwarz-Diamond]: cos X cos Y cos Z −sin X sin Y sin Z = c
        return (cox*coy*coz - six*siy*siz)**2 - t**2
    elif type == "primitive":
        # Primitive surface [Schwarz-Primitive]: cos X + cos Y + cos Z = c
        return (cox + coy + coz)**2 - t**2
    elif type == "iwp":
        # I-WP surface [Schwarz-IWP]: 2*(cos X cos Y + cos Y cos Z + cos Z cos X) - (cos 2X + cos 2Y + cos 2Z) = c
        return (2*(cox*coy + coy*coz + coz*cox) - (co2x + co2y + co2z))**2 - t**2
    elif type == "neovius":
        # Neovius surface [Neovius]: 3*(cos X + cos Y + cos Z) + 4*cos X cos Y cos Z = c
        return (3*(cox + coy + coz) - 4*cox*coy*coz)**2 - t**2
    elif type == "s":
        # S surface [Fischer Koch S]: cos 2X sin Y cos Z + cos X cos 2Y sin Z + sin X cos Y cos 2Z = c
        return (co2x*siy*coz + cox*co2y*siz + six*coy*co2z)**2 - t**2
    elif type == "frd":
        # F-RD surface [Schoen-FRD]: 4(cos X cos Y cos Z) − (cos 2 X cos 2Y + cos 2Y cos 2Z + cos 2Z cos 2X ) = c
        return (4*cox*coy*coz - (co2x*co2y + co2y*co2z + co2z*co2x))**2 - t**2
    elif type == "pmy":
        # PMY surface [PMY]: 2 cos X cos Y cos Z + sin 2X sin Y + sin X sin 2Z + sin 2Y sin Z = c
        return (2*cox*coy*coz + si2x*siy + six*si2z + si2y*siz)**2 - t**2

path = "./common-3d-test-models/data/"  # https://github.com/alecjacobson/common-3d-test-models
name = "spot.obj"
# name = "bimba.obj"

path_out = path + "output/"
Path(path_out).mkdir(parents=True, exist_ok=True)

# [0] Minimal Surfaces Parameters
type = "gyroid"  # Minimal surfaces type - "gyroid" / 
# type = "s"  # Minimal surfaces type
cell_size = 2e-4  # [m] Unit cell size
sheet_thickness = 0.7  # "t" value for the gyroid equation. Level-set value for the gyroid surface   
resolution = 20 # Resolution of a single unit cell. Number of voxels [initially 20]                    
res = resolution*1j 
# Size correction and number of units
cell_size = 1000 * cell_size  # Factor to get the correct size

# [1] Input Geometry
meshg=trimesh.load_mesh(path+name)

xmax=meshg.bounds[:,0][1]
xmin=meshg.bounds[:,0][0]
ymax=meshg.bounds[:,1][1]
ymin=meshg.bounds[:,1][0]
zmax=meshg.bounds[:,2][1]
zmin=meshg.bounds[:,2][0]

cenx = (xmax + xmin) / 2
ceny = (ymax + ymin) / 2
cenz = (zmax + zmin) / 2
centro = np.array([cenx, ceny, cenz])

meshg.vertices -= centro

total_l = (xmax-xmin)+1.2
total_w = (ymax-ymin)+1.2
total_h = (zmax-zmin)+1.2

geometry_input=trimesh.exchange.off.export_off(meshg, digits=3)
with open(path+"geometry_input.off", "w") as text_file:
    text_file.write("%s" % geometry_input)

nx = total_l / cell_size  
ny = total_w / cell_size 
nz = total_h / cell_size 
  
# [2] Marching Cubes to Create Minimal surfaces
X, Y, Z = np.mgrid[0:total_l:res * nx, 0:total_w:res * ny, 0:total_h:res * nz]
vol = tpms_equation(X, Y, Z, cell_size, sheet_thickness, type)
verts, faces, normals, values = marching_cubes(vol, 0, spacing=(total_l / (int(nx * resolution)-1), total_w / (int(ny * resolution)-1), total_h / (int(nz * resolution)-1)))  #
meshf = trimesh.Trimesh(vertices=verts[:], faces=faces[:], vertex_normals=normals[:])
meshf.vertices -= meshf.centroid        
meshoff=trimesh.exchange.off.export_off(meshf, digits=3)
with open(path+"minimal_surfaces.off", "w") as text_file:
    text_file.write("%s" % meshoff)


# [3] Boolean Intersection using Pycork
meshA = trimesh.load_mesh(path+"geometry_input.off", process=False)
meshB = trimesh.load_mesh(path+"minimal_surfaces.off", process=False)
vertsA = meshA.vertices
trisA = meshA.faces
vertsB = meshB.vertices
trisB = meshB.faces
vertsD, trisD = pycork.intersection(vertsA, trisA, vertsB, trisB)
meshC = trimesh.Trimesh(vertices=vertsD, faces=trisD, process=False)
meshC.export(path_out + Path(name).stem + "_tpms.stl")
os.remove(path + "geometry_input.off")
os.remove(path + "minimal_surfaces.off")

# [4] Output Minimal Surfaces Properties
area = round((meshC.area),4) # [mm^2] Total surface 
vol  = round((meshC.volume),4) # [mm^2] Total volume 
RD   = round(meshC.volume/meshA.volume*100, 4) # [%] Volume fraction 

print("Total surface [mm^2]: ", area)
print("Total volume  [mm^2]: ", vol)
print("Volume fraction  (%): ", RD)

3D Scanning

I used the RevoPoint POP 2 3D Scanner to scan a wooden dog with flexible joints and the mannequin model provided by the scanner package.

A time-lapse video of 3D scanning a wooden dog with flexible joints is shown below:

3D Scanning Results – Wooden Dog

3D Scanning Results – Mannequin

There are artifacts in the Mannequin result simply because I put my hand near the object during 3D scanning. I was hoping the fusing process would be smart enough to remove accidentally appearing objects (vs. consistant objects). It turns out not, it just remembers every reasonably large portion of scanned point clouds as pointed out by Anthony. The Wooden dog result is actually good with small holes on the head and bottom. I just manually removed some meshes not belonging to the object.

Acknowledgement

Thanks to Bathsheba Grossman for providing the nice Metatron model online (not available anymore, accessible using web archive), Neil and Anthony for referring me to useful tools for making algorithmic designs, Lingdong for reminding me of the voxel-to-mesh solution using marching cubes and even writing a nice C example of Schwartz’s Diamond-surface and Primitive-surface, my architecture friend Ziyu for referring me to minimal surfaces with a couple of nice tutorials, our geometry processing labmate Paul for pointing me to intriguing directions to model Bathsheba’s art series (which I failed to do because of limited time and experience), and Dave for helping with initial use of 3D printers and scanners.