The purpose of this instructable is to model a ping-pong ball that can be easily
3d printed while keeping a low weight for playability. I got the inspiration for
this project from the deceptor concept from finnish designer janne kyttanen

One drawback of current 3d printing techniques is that they typically don't allow
a wall thickness under 0.8mm, which would result in a heavy and thus not
practical ball (standard 40mm balls weigh 2.7 grams).
To mitigate that issue, i chose to use a geodome-like structure, which has a high
strength to weight ratio.

We will walk through the steps required to model such a ball in the following
sections. The only tool we'll be using for this purpose is OpenJSCAD,
a powerful online script-based 3d modeler. You need to be familiar with
javascript syntax to make the most of this tutorial.

## Step 1: Getting Familiar With OpenJSCAD

Let's get started!

The code editor on the right should display the following code :

```function main() {
return union( 	difference(cube({size: 3, center: true}),
sphere({r:2, center: true})),
intersection(sphere({r: 1.3, center: true}),
cube({size: 2.1, center: true}))
).translate([0,0,1.5]).scale(10);
}```

This script here generates the model shown in the 3d view. Notice that what gets
displayed is what you return from the main function. You can play with that code
if you wish, for example by modifying the sphere radius or cube size. For a
detailed description of the all available geometric primitives and transformations,
refer to the OpenJSCAD User Guide.

To see the result, regenerate the model by pressing Shift + Enter.

Next we'll start modelling our ping-pong ball.

## Step 2: A Basic Ping-pong Ball

We start with the external envellope of the ball, which is a sphere with a 40mm diameter.

```function main(){
var ballDiameter = 40;
var sphereRes = 50;

var outerSphere = CSG.sphere({
center: [0, 0, 0],
radius: ballDiameter/2.,    // must be scalar
resolution: sphereRes        // optional
});

return outerSphere;
}```

After generating the model (Shift + Enter), you should see a sphere like in the picture above.

But wait, what we have here is a solid sphere, which would result in a pretty
heavy ball! To model an actual ping pong ball, we need to hollow out the
inside of our sphere.
To do that, we will use a boolean operation called a Difference, which is done
in OpenJSCAD by calling the difference function with two 3d objects as parameters.

Basic ping-pong ball script

```function main(){
var ballDiameter = 40;
var wallThickness = 1.0;
var sphereRes = 50;

var outerSphere = CSG.sphere({
center: [0, 0, 0],
radius: ballDiameter/2.,    // must be scalar
resolution: sphereRes        // optional
});

var innerSphere = CSG.sphere({
center: [0, 0, 0],
radius: ballDiameter/2. - wallThickness ,    // must be scalar
resolution: sphereRes        // optional
});

var cuttingCube = CSG.cube({
center: [0, -ballDiameter/2., 0],
});

var ball = difference(outerSphere, innerSphere);
return difference(ball, cuttingCube);
}```

I used a cube to cut the sphere in half so you can better see that the ball is
indeed hollow. A wall thickness of 1mm ensures printability as well as solidity,
especially as we'll later add holes all over the surface of the ball.

## Step 3: Geodesic Spheres

Now we'll look into how to subdivide a sphere into a determined number of
triangles of approximately similar shape. The method described here is
often used to build polygonal approximations of spheres, such as detailed
in this OpenGL tutorial : http://www.opengl.org.ru/docs/pg/0208.html.
In our case, this will be the building step to generate the holes that will
transfrom our regular ball into a geodeome-like one.

First we start with a regular isocahedron , which has twenty triangular faces, and
the interesting property that each of its vertices lie on a sphere.
Then for every triangle, we split each of the three edges at the middle, which
gives us three additional vertices which we use to subdivide said triangle
into four smaller triangles. The three vertices are then translated outwards
so they lie on the sphere's surface.

```function main(){
var X = 0.525731112119133606;
var Z = 0.850650808352039932;
var vdata = [
[-X, 0.0, Z], [ X, 0.0, Z ], [ -X, 0.0, -Z ], [ X, 0.0, -Z ],
[ 0.0, Z, X ], [ 0.0, Z, -X ], [ 0.0, -Z, X ], [ 0.0, -Z, -X ],
[ Z, X, 0.0 ], [ -Z, X, 0.0 ], [ Z, -X, 0.0 ], [ -Z, -X, 0.0 ]
]; // isocahedron vertex coordinates
var tindices = [
[0, 4, 1], [ 0, 9, 4 ], [ 9, 5, 4 ], [ 4, 5, 8 ], [ 4, 8, 1 ],
[ 8, 10, 1 ], [ 8, 3, 10 ], [ 5, 3, 8 ], [ 5, 2, 3 ], [ 2, 7, 3 ],
[ 7, 10, 3 ], [ 7, 6, 10 ], [ 7, 11, 6 ], [ 11, 0, 6 ], [ 0, 1, 6 ],
[ 6, 1, 10 ], [ 9, 0, 11 ], [ 9, 11, 2 ], [ 9, 2, 5 ], [ 7, 2, 11 ]
]; // isocahedron triangles

return polyhedron({
points: vdata,
triangles: tindices
}```

Next we write the subdivision fonction for a unit sphere approximation :

The subdvide function

```/* fn 	subdivide(v1, v2, v3, addPolyCb, depth)
* brief subdivides a triangle into 4 smaller ones
* params 	v1, v2, v3 : triangle vertices
*		depth : number of remaining subdivision iterations
*/
function subdivide(v1, v2, v3, addPolyCb, depth)
{
if(depth == 0) {
return;
}

var v12 = v1.plus(v2).unit(); // middle of v1v2 edge projected on the unit sphere
var v23 = v2.plus(v3).unit();
var v31 = v3.plus(v1).unit();
var newDepth = depth - 1;
}```

The subdivision process works recursively; starting from the 20 isocahedron
faces, each step of the subdivision multiplies the total number of faces by 4.
Consequently, a depth of 1 generates an 80 faces polyhedron.

We can now generate a sphere approximation by calling the subdivide function
iteratively over the isocahedron faces as follows :

80 faces geodesic sphere script

```function main()
{
var ballDiameter = 40;
var polygons = []; // list of polygons
{
polygons.push(new CSG.Polygon([
new CSG.Vertex(new CSG.Vector3D(v3.x,v3.y,v3.z)),
new CSG.Vertex(new CSG.Vector3D(v2.x,v2.y,v2.z)),
new CSG.Vertex(new CSG.Vector3D(v1.x,v1.y,v1.z))
]));
}

var unitSphereApprox = CSG.fromPolygons(polygons);

}

// subdivides a unit isocahedron n=depth times and calls a callback for each created face
{
var X = 0.525731112119133606;
var Z = 0.850650808352039932;
var vdata = [
[-X, 0.0, Z], [ X, 0.0, Z ], [ -X, 0.0, -Z ], [ X, 0.0, -Z ],
[ 0.0, Z, X ], [ 0.0, Z, -X ], [ 0.0, -Z, X ], [ 0.0, -Z, -X ],
[ Z, X, 0.0 ], [ -Z, X, 0.0 ], [ Z, -X, 0.0 ], [ -Z, -X, 0.0 ]
];
var tindices = [
[0, 4, 1], [ 0, 9, 4 ], [ 9, 5, 4 ], [ 4, 5, 8 ], [ 4, 8, 1 ],
[ 8, 10, 1 ], [ 8, 3, 10 ], [ 5, 3, 8 ], [ 5, 2, 3 ], [ 2, 7, 3 ],
[ 7, 10, 3 ], [ 7, 6, 10 ], [ 7, 11, 6 ], [ 11, 0, 6 ], [ 0, 1, 6 ],
[ 6, 1, 10 ], [ 9, 0, 11 ], [ 9, 11, 2 ], [ 9, 2, 5 ], [ 7, 2, 11 ]
];

// iterate over isocahedron triangles
for(var i = 0; i < 20; i++)
subdivide(  new CSG.Vector3D(vdata[tindices[i][0]]),
new CSG.Vector3D(vdata[tindices[i][1]]),
new CSG.Vector3D(vdata[tindices[i][2]]),
} ```

To run this script, copy and paste the code in your favorite code editor, and save
the file with the .jscad extension. Then just drag & drop the file in the recangular

## Step 4: Drilling Triangular Holes

In this section we'll look into how to generate a pattern of prisms from the geodesic sphere faces, that we'll then substract from our ball from Step 2 to generate the actual holes.

First we need to create prisms from the geodesic sphere triangles.
To do so, we first create a smaller triangle within each triangle of the geodesic
sphere by offsetting its edges inward by a fixed distance and computing
the intersection points of the translated edges which make up the vertices
of the new triangle (see picture above).

To generate the prism, I used the solidFromSlicesmethod to extrude the current triangle (i1 i2 i3) orthogonally to its surface.

The createPrism function

```// generates a prism from a unit sphere triangle, a radius, and an offset
{// compute the coordinates of the vertices of the input triangle
var v1 = new CSG.Vector3D(scalar_mul(sphereTri[0], radius));
var v2 = new CSG.Vector3D(scalar_mul(sphereTri[1], radius));
var v3 = new CSG.Vector3D(scalar_mul(sphereTri[2], radius));

// make plane base (v1, x, y)
var xAxis = v2.minus(v1).unit();
var v13 = v3.minus(v1);
var yAxis = v13.minus(xAxis.times(v13.dot(xAxis))).unit();

// retrieve 2d coordinates of the triangle vertices in that base
var v1_2d = new CSG.Vector2D(0, 0);
var v2_2d = new CSG.Vector2D(v2.minus(v1).dot(xAxis), 0);
var v3_2d = new CSG.Vector2D(v3.minus(v1).dot(xAxis), v3.minus(v1).dot(yAxis));

// get the middle of each segment in the plane
var v12_2d = v2_2d.minus(v1_2d);
var v23_2d = v3_2d.minus(v2_2d);
var v31_2d = v1_2d.minus(v3_2d);

// get unit vector perpendicular to i1i2 segment in the plane
var ortho12 = new CSG.Vector2D(-v12_2d.y, v12_2d.x).unit();
if(v3_2d.minus(v1_2d).dot(ortho12) <0)
ortho12 = ortho12.times(-1);

var ortho23 = new CSG.Vector2D(-v23_2d.y, v23_2d.x).unit();
if(v1_2d.minus(v2_2d).dot(ortho23) <0)
ortho23 = ortho23.times(-1);

var ortho31 = new CSG.Vector2D(-v31_2d.y, v31_2d.x).unit();
if(v2_2d.minus(v1_2d).dot(ortho31) <0)
ortho31 = ortho31.times(-1);

// translate all tri segments inward by the same offset
var s12b = translate_segment([v1_2d, v2_2d], ortho12.times(offset));
var s23b = translate_segment([v2_2d, v3_2d], ortho23.times(offset));
var s31b = translate_segment([v3_2d, v1_2d], ortho31.times(offset));

// compute intersection points of translated segments in the plane
var i1 = intersect(s12b, s23b);
var i2 = intersect(s23b, s31b);
var i3 = intersect(s31b, s12b);

var i1_3d = v1.plus(xAxis.times(i1.x)).plus(yAxis.times(i1.y));
var i2_3d = v1.plus(xAxis.times(i2.x)).plus(yAxis.times(i2.y));
var i3_3d = v1.plus(xAxis.times(i3.x)).plus(yAxis.times(i3.y));

// create a polygon from the intersection points
var tri =  new CSG.Polygon([
new CSG.Vertex(i1_3d),
new CSG.Vertex(i2_3d),
new CSG.Vertex(i3_3d)
]);

var zAxis = tri.plane.normal;
return tri.solidFromSlices({
numslices: 2,              // amount of slices
loop: false,            // final CSG is closed by looping (start = end) like a torus
callback: function(t,slice) {
// echo("t:" + t)
return this.translate( scalar_mul([zAxis.x, zAxis.y, zAxis.z], 4*t) );
}
}).translate(scalar_mul([zAxis.x, zAxis.y, zAxis.z], -2));
}

// multiplies a 3d vector by a scalar
function scalar_mul(v, c)
{
return [c*v[0], c*v[1], c*v[2]];
}

// translates a 2d segment by a 2d vector
function translate_segment(seg, vec)
{
s0b = seg[0].plus(vec);
s1b = seg[1].plus(vec);

return [s0b, s1b];
}

// computes the intersection of 2 2d segments
function intersect(s1, s2)
{
var p = s1[0];
var q = s2[0];
var r = s1[1].minus(p);
var s = s2[1].minus(q);

var u = q.minus(p).cross(r)/r.cross(s);
var t = q.minus(p).cross(s)/r.cross(s);

if(r.cross(s) != 0 && u >= 0 && u<= 1 && t >= 0 && t<= 1)
return p.plus(r.times(t));
return null;
}```

Making use of the createPrism function above, whe can now generate the full
hole pattern by iterating over the geodesic sphere triangles :

Hole pattern script

```function main(){
var ballDiameter = 40; // mm
var segmentWidth = 2; // mm

var sphereTris = []; // holds list of geodesic sphere triangles
{
sphereTris.push([
[v1.x, v1.y, v1.z],
[v2.x, v2.y, v2.z],
[v3.x, v3.y, v3.z]
]);
}

var holePattern;
for(j=0; j!=sphereTris.length; ++j)
{
var prism =  createPrism(sphereTris[j], ballDiameter/2., segmentWidth/2.);
if(j==0)
{
holePattern = prism;
}
else
holePattern = holePattern.union(prism);
}

return holePattern;
}

function subdivide(v1, v2, v3, addPolyCb, depth)
{
if(depth == 0) {
return;
}

var v12 = v1.plus(v2).unit();
var v23 = v2.plus(v3).unit();
var v31 = v3.plus(v1).unit();
var newDepth = depth - 1;
}

var X = 0.525731112119133606;
var Z = 0.850650808352039932;
var vdata = [
[-X, 0.0, Z], [ X, 0.0, Z ], [ -X, 0.0, -Z ], [ X, 0.0, -Z ],
[ 0.0, Z, X ], [ 0.0, Z, -X ], [ 0.0, -Z, X ], [ 0.0, -Z, -X ],
[ Z, X, 0.0 ], [ -Z, X, 0.0 ], [ Z, -X, 0.0 ], [ -Z, -X, 0.0 ]
];
var tindices = [
[0, 4, 1], [ 0, 9, 4 ], [ 9, 5, 4 ], [ 4, 5, 8 ], [ 4, 8, 1 ],
[ 8, 10, 1 ], [ 8, 3, 10 ], [ 5, 3, 8 ], [ 5, 2, 3 ], [ 2, 7, 3 ],
[ 7, 10, 3 ], [ 7, 6, 10 ], [ 7, 11, 6 ], [ 11, 0, 6 ], [ 0, 1, 6 ],
[ 6, 1, 10 ], [ 9, 0, 11 ], [ 9, 11, 2 ], [ 9, 2, 5 ], [ 7, 2, 11 ]
];

for(var i = 0; i < 20; i++)
subdivide(    new CSG.Vector3D(vdata[tindices[i][0]]),
new CSG.Vector3D(vdata[tindices[i][1]]),
new CSG.Vector3D(vdata[tindices[i][2]]),
}```

## Step 5: Putting It All Together

This is where we combine the results of steps 2 and 4 to achieve the desired
outcome. Using the functions createGeodesicSphere and createPrism
encoutered in the previous steps, the end result is obtained by first substracting
each prism from the ball, and then hollowing out the ball as seen in Step 2.
I chose a subdivision depth of 2, which means our ball has 320 holes, which
is the best fit for a 3d print of that size.

Final script

```function main(){
// definitions
var ballDiameter = 40;
var wallThickness = 1.0;
var segmentWidth = 1.1; // width of an individual link of the structure
var sphereRes = 33;
var subdivisionDepth = 2;

var outerSphere = CSG.sphere({
center: [0, 0, 0],
resolution: sphereRes        // optional
});

var innerSphere = CSG.sphere({
center: [0, 0, 0],
resolution: sphereRes        // optional
});

var ball = outerSphere;
var geoSphereTris = []; // list of geosphere triangles
{
geoSphereTris.push([
[v1.x, v1.y, v1.z],
[v2.x, v2.y, v2.z],
[v3.x, v3.y, v3.z]
]);
}

// successively substract each prism from the ball
for(j=0; j!=geoSphereTris.length; ++j)
{
var prism =  createPrism(geoSphereTris[j], ballRadius, segmentWidth/2.);
ball = difference(ball, prism);
}

ball = difference(ball, innerSphere); // hollow out the inside

return ball;
}```

Once the model is generated, you can export it directly in STL format using the
'Generate STL' button under the 3d view. You can now get the model printed
through your favorite 3d printing service or local store.

Warning : this model cannot be printed as-is on FDM printers, it would need an additional removable support structure, which is beyond the scope of this tutorial. However, it can be printed on SLS or SLA printers effortlessly.

Thank you for reading this instructable, I hope you gained something valuable from it.

You can find the resulting STL files here :

http://www.thingiverse.com/thing:859499

http://www.thingiverse.com/thing:859513 ( w/ support structure for FDM printers )

Best wishes,

Vincent

I'm also interested to know that. How differently does it behave from a regular ping pong ball?
<p>For indoor table tennis, you definitely want the traditional gas-filled ball, engineered to detailed specs. But a slightly heavier ball that catches less wind will be great outdoors - I am sure this will make a great outdoor table tennis ball. If you coat the outside with polyester resin I bet it will bounce better.</p>
<p>Could you please post the STL file?</p>
<p>You can now find the links at the end of the intructable.</p>
<p>Hi,<br>I will post it on thingiverse later today and link it back here, along with the version with support structures for FDM printers.</p>
<p>That is some awesome 3d code you've got. As an alternative, you could export the geodesic sphere from openjscad, import it to meshmixer, select &quot;make pattern&quot;. The default first option for &quot;make pattern&quot; is &quot;tiled tubes&quot;. Change this to &quot;dual edges&quot;. Interested to know if it prints well. Great instructable :)</p>
<p>Thanks!<br>Didn't know about meshmixer. Will give it a try whenever I get the time.<br>thanks for the tip.</p>
Very cool, though I'm doubtful of the lower air resistance claim
<p>Thanks; as for that claim, I got influenced by that designer guy, it's one of his selling points for his whole concept (alledgedly, his ball goes unnafected by wind). It might be true up to a certain speed, but you have a point, since I have no precise measure to back this claim, i will remove it to avoid any misconceptions about this project.</p>
<p>hello</p><div><div><div><div><div><div><div>I have a configuration problem in my Prusai3 MELZI<br><br> instead of up to 180mm it up as a cm (instead of 18)<br><br> everything else is good T &deg; C, speed, X, Y</div><div><p>thank you</p></div></div></div></div></div></div></div>
<p>j'ai un probl&egrave;me de configuration de ma Prusai3 MELZI</p><p>au lieu de monter de 180mm elle ne monte que d'un cm (au lieu de 18)</p><p>tout le reste est bon T&deg;C, vitesse, axe X,Y</p>
<p>Very interesting!</p><p>Have you played ping pong with this? Does it react completely different than a normal ball? I'm intrigued! :)</p>
<p>It acts quite different than a normal ball. It bounces slighlty less and spins less, but it's definetely playable. Also trajectories don't curve as much with spin.<br>Still an interesting experience; it does force you to adapt your game.</p>
making a dropshot will be impossible
I agree with seamster, does it bounce well? :D