Introduction: 3D Printed Ping-Pong Ball
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
(read about it here : 3d printed ping pong table + paddles ).
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!
First start OpenJSCAD by clicking on the following link : OpenJSCAD
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; }
You can find more info on generating spheres with OpenJSCAD in the user guide here.
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], radius: [ballDiameter/2, ballDiameter/2, ballDiameter/2] }); 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.
Isocahedron OpenJSCAD script
function main(){ var X = 0.525731112119133606; var Z = 0.850650808352039932; var radius = 20; 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 }).scale([radius, radius, radius]); }
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 * addPolyCb : callback for add polygon notification * depth : number of remaining subdivision iterations */ function subdivide(v1, v2, v3, addPolyCb, depth) { if(depth == 0) { addPolyCb(v1, v2, v3); 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; subdivide(v1, v12, v31, addPolyCb, newDepth); subdivide(v2, v23, v12, addPolyCb, newDepth); subdivide(v3, v31, v23, addPolyCb, newDepth); subdivide(v12, v23, v31, addPolyCb, newDepth); }
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 ballRadius = ballDiameter/2.; var polygons = []; // list of polygons addPolyCb = function(v1, v2, v3) { 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)) ])); } createGeodesicSphere(addPolyCb, 1); var unitSphereApprox = CSG.fromPolygons(polygons); return unitSphereApprox.scale([ballRadius, ballRadius, ballRadius]); } // subdivides a unit isocahedron n=depth times and calls a callback for each created face function createGeodesicSphere(addPolyCb, depth) { 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]]), addPolyCb, depth); }
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
area at the bottom left corner of your OpenJSCAD browser window.
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 function createPrism(sphereTri, radius, 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 addPolyCb = function(v1, v2, v3) { sphereTris.push([ [v1.x, v1.y, v1.z], [v2.x, v2.y, v2.z], [v3.x, v3.y, v3.z] ]); } createGeodesicSphere(addPolyCb, 1); 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) { addPolyCb(v1, v2, v3); return; } var v12 = v1.plus(v2).unit(); var v23 = v2.plus(v3).unit(); var v31 = v3.plus(v1).unit(); var newDepth = depth - 1; subdivide(v1, v12, v31, addPolyCb, newDepth); subdivide(v2, v23, v12, addPolyCb, newDepth); subdivide(v3, v31, v23, addPolyCb, newDepth); subdivide(v12, v23, v31, addPolyCb, newDepth); } function createGeodesicSphere(addPolyCb, depth) { 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]]), addPolyCb, depth); }
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 ballRadius = ballDiameter/2.; 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], radius: ballRadius, // must be scalar resolution: sphereRes // optional }); var innerSphere = CSG.sphere({ center: [0, 0, 0], radius: ballRadius - wallThickness , // must be scalar resolution: sphereRes // optional }); var ball = outerSphere; var geoSphereTris = []; // list of geosphere triangles addPolyCb = function(v1, v2, v3) { geoSphereTris.push([ [v1.x, v1.y, v1.z], [v2.x, v2.y, v2.z], [v3.x, v3.y, v3.z] ]); } createGeodesicSphere(addPolyCb, subdivisionDepth); // 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

Participated in the
3D Printing Contest
17 Comments
Question 4 years ago
"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)."
Are there speciality carbon filaments for the printer that woudl produce a lighter weight object with same wall thickness?
Answer 4 years ago
That would definitely help.
By how much depends on the density of carbon fiber filament vs regular PLA filament.
8 years ago
I'm also interested to know that. How differently does it behave from a regular ping pong ball?
Reply 7 years ago
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.
8 years ago on Introduction
Could you please post the STL file?
Reply 8 years ago on Introduction
You can now find the links at the end of the intructable.
Reply 8 years ago on Introduction
Hi,
I will post it on thingiverse later today and link it back here, along with the version with support structures for FDM printers.
8 years ago on Introduction
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 "make pattern". The default first option for "make pattern" is "tiled tubes". Change this to "dual edges". Interested to know if it prints well. Great instructable :)
Reply 8 years ago on Introduction
Thanks!
Didn't know about meshmixer. Will give it a try whenever I get the time.
thanks for the tip.
8 years ago
Very cool, though I'm doubtful of the lower air resistance claim
Reply 8 years ago on Introduction
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.
8 years ago on Introduction
hello
instead of up to 180mm it up as a cm (instead of 18)
everything else is good T ° C, speed, X, Y
thank you
8 years ago on Introduction
j'ai un problème de configuration de ma Prusai3 MELZI
au lieu de monter de 180mm elle ne monte que d'un cm (au lieu de 18)
tout le reste est bon T°C, vitesse, axe X,Y
8 years ago on Introduction
Very interesting!
Have you played ping pong with this? Does it react completely different than a normal ball? I'm intrigued! :)
Reply 8 years ago on Introduction
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.
Still an interesting experience; it does force you to adapt your game.
8 years ago
making a dropshot will be impossible
8 years ago
I agree with seamster, does it bounce well? :D