/ src / libraries / geodesic_sphere.scad
geodesic_sphere.scad
  1  // from https://www.thingiverse.com/thing:1484333
  2  // public domain license
  3  // same syntax and semantics as built-in sphere, so should be a drop-in replacement
  4  // it's a bit slow for large numbers of facets
  5  module geodesic_sphere(r=-1, d=-1) {
  6    // if neither parameter specified, radius is taken to be 1
  7    rad = r > 0 ? r : d > 0 ? d/2 : 1;
  8  
  9    pentside_pr = 2*sin(36);  // side length compared to radius of a pentagon
 10    pentheight_pr = sqrt(pentside_pr*pentside_pr - 1);
 11    // from center of sphere, icosahedron edge subtends this angle
 12    edge_subtend = 2*atan(pentheight_pr);
 13  
 14    // vertical rotation by 72 degrees
 15    c72 = cos(72);
 16    s72 = sin(72);
 17    function zrot(pt) = [ c72*pt[0]-s72*pt[1], s72*pt[0]+c72*pt[1], pt[2] ];
 18  
 19    // rotation from north to vertex along positive x
 20    ces = cos(edge_subtend);
 21    ses = sin(edge_subtend);
 22    function yrot(pt) = [ ces*pt[0] + ses*pt[2], pt[1], ces*pt[2]-ses*pt[0] ];
 23  
 24    // 12 icosahedron vertices generated from north, south, yrot and zrot
 25    ic1 = [ 0, 0, 1 ];  // north
 26    ic2 = yrot(ic1);    // north and +x
 27    ic3 = zrot(ic2);    // north and +x and +y
 28    ic4 = zrot(ic3);    // north and -x and +y
 29    ic5 = zrot(ic4);    // north and -x and -y
 30    ic6 = zrot(ic5);    // north and +x and -y
 31    ic12 = [ 0, 0, -1]; // south
 32    ic10 = yrot(ic12);  // south and -x
 33    ic11 = zrot(ic10);  // south and -x and -y
 34    ic7 = zrot(ic11);   // south and +x and -y
 35    ic8 = zrot(ic7);    // south and +x and +y
 36    ic9 = zrot(ic8);    // south and -x and +y
 37  
 38    // start with icosahedron, icos[0] is vertices and icos[1] is faces
 39    icos = [ [ic1, ic2, ic3, ic4, ic5, ic6, ic7, ic8, ic9, ic10, ic11, ic12 ],
 40      [ [0, 2, 1], [0, 3, 2], [0, 4, 3], [0, 5, 4], [0, 1, 5],
 41        [1, 2, 7], [2, 3, 8], [3, 4, 9], [4, 5, 10], [5, 1, 6],
 42        [7, 6, 1], [8, 7, 2], [9, 8, 3], [10, 9, 4], [6, 10, 5],
 43        [6, 7, 11], [7, 8, 11], [8, 9, 11], [9, 10, 11], [10, 6, 11]]];
 44  
 45    // now for polyhedron subdivision functions
 46  
 47    // given two 3D points on the unit sphere, find the half-way point on the great circle
 48    // (euclidean midpoint renormalized to be 1 unit away from origin)
 49    function midpt(p1, p2) =
 50      let (midx = (p1[0] + p2[0])/2, midy = (p1[1] + p2[1])/2, midz = (p1[2] + p2[2])/2)
 51      let (midlen = sqrt(midx*midx + midy*midy + midz*midz))
 52      [ midx/midlen, midy/midlen, midz/midlen ];
 53  
 54    // given a "struct" where pf[0] is vertices and pf[1] is faces, subdivide all faces into
 55    // 4 faces by dividing each edge in half along a great circle (midpt function)
 56    // and returns a struct of the same format, i.e. pf[0] is a (larger) list of vertices and
 57    // pf[1] is a larger list of faces.
 58    function subdivpf(pf) =
 59      let (p=pf[0], faces=pf[1])
 60      [ // for each face, barf out six points
 61        [ for (f=faces)
 62            let (p0 = p[f[0]], p1 = p[f[1]], p2=p[f[2]])
 63              // "identity" for-loop saves having to flatten
 64              for (outp=[ p0, p1, p2, midpt(p0, p1), midpt(p1, p2), midpt(p0, p2) ]) outp
 65        ],
 66        // now, again for each face, spit out four faces that connect those six points
 67        [ for (i=[0:len(faces)-1])
 68          let (base = 6*i)  // points generated in multiples of 6
 69            for (outf =
 70            [[ base, base+3, base+5],
 71            [base+3, base+1, base+4],
 72            [base+5, base+4, base+2],
 73            [base+3, base+4, base+5]]) outf  // "identity" for-loop saves having to flatten
 74        ]
 75      ];
 76  
 77    // recursive wrapper for subdivpf that subdivides "levels" times
 78    function multi_subdiv_pf(pf, levels) =
 79      levels == 0 ? pf :
 80      multi_subdiv_pf(subdivpf(pf), levels-1);
 81  
 82    // subdivision level based on $fa:
 83    // level 0 has edge angle of edge_subtend so subdivision factor should be edge_subtend/$fa
 84    // must round up to next power of 2.
 85    // Take log base 2 of angle ratio and round up to next integer
 86    ang_levels = ceil(log(edge_subtend/$fa)/log(2));
 87  
 88    // subdivision level based on $fs:
 89    // icosahedron edge length is rad*2*tan(edge_subtend/2)
 90    // actually a chord and not circumference but let's say it's close enough
 91    // subdivision factor should be rad*2*tan(edge_subtend/2)/$fs
 92    side_levels = ceil(log(rad*2*tan(edge_subtend/2)/$fs)/log(2));
 93  
 94    // subdivision level based on $fn: (fragments around circumference, not total facets)
 95    // icosahedron circumference around equator is about 5 (level 1 is exactly 10)
 96    // ratio of requested to equatorial segments is $fn/5
 97    // level of subdivison is log base 2 of $fn/5
 98    // round up to the next whole level so we get at least $fn
 99    facet_levels = ceil(log($fn/5)/log(2));
100  
101    // $fn takes precedence, otherwise facet_levels is NaN (-inf) but it's ok
102    // because it falls back to $fa or $fs, whichever translates to fewer levels
103    levels = $fn ? facet_levels : min(ang_levels, side_levels);
104  
105    // subdivide icosahedron by these levels
106    subdiv_icos = multi_subdiv_pf(icos, levels);
107  
108    scale(rad)
109    polyhedron(points=subdiv_icos[0], faces=subdiv_icos[1]);
110  }