The Simple, Flexible, Octasphere
18 Dec 2019
When choosing from the Platonic solids, careful you must be. ~ Yoda
Two popular ways of tessellating spheres are the UV sphere (left) and icosphere (middle). The UV sphere is a parametric surface, typically generated with a double-nested loop over θ and φ. The icosphere is generated by starting with a regular icosahedron (the 20-sided Platonic solid) and applying multiple subdivisions. While icospheres are slightly more complex to create than UV spheres, they do not suffer from irregular faces.
In this post I’m championing the octasphere (right). This is subdivided from a mere 8-sided polyhedron, and is arguably less visually pleasing than the icosphere. However it’s easy to directly generate an octasphere to any level of detail parametrically, without actual subdivision. Moreover its 8-way symmetry is useful in a number of ways. For example, it can be generalized to include rounded cuboids like the ones below.
Octaspheres can be parameterized with plain old lat-long mapping whereas icospheres force you to use an alternative texture mapping scheme. Note however that octaspheres suffer from undersampling near the poles when the geometric LOD is low. If this is a problem, you might want to consider using octahedral mapping instead of lat-long mapping.
Direct Tessellation
To make an octasphere, you can tessellate one-eighth of the shape (i.e. the mesh patch that arises from a single triangle in the octahedron), then make 7 copies at various rotations.
To leverage parametric evaluation instead of subdivision, simply march along the sequence of geodesic lines that stretch between two patch boundaries. This is shown in blue in the above image.
The following Python snippet generates the vertices for a single patch.
def tessellate_octasphere_patch(num_subdivisons):
n = 2**num_subdivisions + 1
num_verts = n * (n + 1) // 2
verts = []
for i in range(n):
theta = pi * 0.5 * i / (n - 1)
point_a = [0, sin(theta), cos(theta)]
point_b = [cos(theta), sin(theta), 0]
num_segments = n - 1 - i
geodesic_verts = compute_geodesic(point_a, point_b, num_segments)
verts = verts + geodesic_verts
assert len(verts) == num_verts
return verts
In the above snippet, compute_geodesic
generates a sequence of surface points between point_a
and point_b
, inclusive. In general this is a hard problem but for spheres it’s easy:
def compute_geodesic(point_a, point_b, num_segments):
angle_between_endpoints = math.acos(np.dot(point_a, point_b))
rotation_axis = np.linalg.norm(np.cross(point_a, point_b))
point_list = [point_a]
if num_segments == 0:
return point_list
dtheta = angle_between_endpoints / num_segments
for point_index in range(1, num_segments):
theta = point_index * dtheta
q = quaternion.create_from_axis_rotation(rotation_axis, theta)
point_list.append(quaternion.apply_to_vector(q, point_a))
point_list.append(point_b)
return point_list
To finish making the actual sphere, make 7 copies of the patch at various orientations. Be sure to use rotation rather than mirroring, since the latter would cause inconsistent triangle winding.
Generalized Octaspheres
If you connect the 8 corner patches with degenerate triangles, you can translate them away from each other to create various useful shapes. Try playing with the sliders to see what I mean. To have an effect, the width / height / depth sliders must be more than twice the corner radius. If the canvas looks blank, try refreshing the page.
Source code for creating generalized octasphere meshes using direct parametric evaluation is available at the links below.