If you want to render a sphere in 3D, for example in OpenGL or DirectX, it is often a good idea to use a subdivided icosahedron. That often works better than the “UVSphere”, which means simply tesselating a sphere by longitude and latitude. The triangles in an icosphere are a lot more evenly distributed over the final sphere. Unfortunately, the easiest way, it seems, is to generate such a sphere is to do that in a 3D editing program. But to load that into your application requires a 3D file format parser. That’s a lot of overhead if you really need just the sphere, so doing it programmatically is preferable.

At this point, many people will just settle for the UVSphere since it is easy to generate programmatically. Especially since generating the sphere as an indexed mesh without vertex-duplicates further complicates the problem. But it is actually not much harder to generate the icosphere!

Here I’ll show some C++ code that does just that.

# C++ Implementation

We start with a hard-coded indexed-mesh representation of the icosahedron:

struct Triangle { Index vertex[3]; }; using TriangleList=std::vector<Triangle>; using VertexList=std::vector<v3>; namespace icosahedron { const float X=.525731112119133606f; const float Z=.850650808352039932f; const float N=0.f; static const VertexList vertices= { {-X,N,Z}, {X,N,Z}, {-X,N,-Z}, {X,N,-Z}, {N,Z,X}, {N,Z,-X}, {N,-Z,X}, {N,-Z,-X}, {Z,X,N}, {-Z,X, N}, {Z,-X,N}, {-Z,-X, N} }; static const TriangleList triangles= { {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} }; }

Now we iteratively replace each triangle in this icosahedron by four new triangles:

Each edge in the old model is subdivided and the resulting vertex is moved on to the unit sphere by normalization. The key here is to not duplicate the newly created vertices. This is done by keeping a lookup of the edge to the new vertex it generates. Note that the orientation of the edge does not matter here, so we need to normalize the edge direction for the lookup. We do this by forcing the lower index first. Here’s the code that either creates or reused the vertex for a single edge:

using Lookup=std::map<std::pair<Index, Index>, Index>; Index vertex_for_edge(Lookup& lookup, VertexList& vertices, Index first, Index second) { Lookup::key_type key(first, second); if (key.first>key.second) std::swap(key.first, key.second); auto inserted=lookup.insert({key, vertices.size()}); if (inserted.second) { auto& edge0=vertices[first]; auto& edge1=vertices[second]; auto point=normalize(edge0+edge1); vertices.push_back(point); } return inserted.first->second; }

Now you just need to do this for all the edges of all the triangles in the model from the previous interation:

TriangleList subdivide(VertexList& vertices, TriangleList triangles) { Lookup lookup; TriangleList result; for (auto&& each:triangles) { std::array<Index, 3> mid; for (int edge=0; edge<3; ++edge) { mid[edge]=vertex_for_edge(lookup, vertices, each.vertex[edge], each.vertex[(edge+1)%3]); } result.push_back({each.vertex[0], mid[0], mid[2]}); result.push_back({each.vertex[1], mid[1], mid[0]}); result.push_back({each.vertex[2], mid[2], mid[1]}); result.push_back({mid[0], mid[1], mid[2]}); } return result; } using IndexedMesh=std::pair<VertexList, TriangleList>; IndexedMesh make_icosphere(int subdivisions) { VertexList vertices=icosahedron::vertices; TriangleList triangles=icosahedron::triangles; for (int i=0; i<subdivisions; ++i) { triangles=subdivide(vertices, triangles); } return{vertices, triangles}; }

There you go, a customly subdivided icosphere!

# Performance

Of course, this implementation is not the most runtime-efficient way to get the icosphere. But it is decent and very simple. Its performance depends mainly on the type of lookup used. I used a `map`

instead of an `unordered_map`

here for brevity, only because there’s no premade hash function for a std::pair of indices. In pratice, you would almost always use a hash-map or some kind of spatial structure, such as a grid, which makes this method a lot tougher to compete with. And certainly feasible for most applications!

# The general pattern

The lookup-or-create pattern used in this code is very useful when creating indexed-meshes programmatically. I’m certainly not the only one who discovered it, but I think it needs to be more widely known. For example, I’ve used it when extracting voxel-membranes and isosurfaces from volumes. It works very well whenever you are creating your vertices from some well-defined parameters. Usually, it’s some tuple that describes the edge you are creating the vertex on. This is the case with marching cubes or marching tetrahedrons. It can, however, also be grid coordinates if you sparsely generate vertices on a grid, for example when meshing heightmaps.

Great post. I’ve looked at the Andreas Kahler’s implementation as well. The only problem is the approach, while is valid, is not efficient at all for multi-threaded use case. I’m looking to perform this in a multi-threaded system. Is there any research done on this that you can point out to?

Thank you! I do not know about any research, but maybe a lock-free hash map works well for your case. Other than that, the usual approach is simply splitting the geometry up and merging the seams later.

Thank you for sharing this knowledge, I´ve implemented in JS with p5 library, hope you enjoy it:

https://www.openprocessing.org/sketch/711796

nice code snippet so i can draw an icosphere without truely getting the geometry

the only puzzle now is which direction is it facing, actually that’s an odd way up to place it

Thank you. I am trying to compile this code on Linux, Ubuntu 20.04, GCC g++ 9.4, but it gives some errors, for instance, in line:

>> using VertexList=std::vector;

“v3” is not defined.

Then, “Index” and “VertexList” type are not defined. There are some more errors, but these are the most important ones. Thank you!

This is not complete code. You are going to need your own 3D vector type instead of v3 that supports a couple of functions such as operator+ and normalize.