binpress

Creating an Octahedron Sphere in Unity

creating-octahedron-sphere-unity

We often use meshes to represent 3D objects, approximating surfaces with a collection of triangles. This works fine when all lines are straight, but becomes troublesome when curves are involved. A cube is easy. A sphere? Not so much.

Consider Unity’s default sphere mesh. Its topology is known as a UV sphere. It’s basically a rectangular grid that’s wrapped around a sphere and compressed into a single point at the poles. Thus the vertical lines have constant longitude and the horizontal lines have constant latitude.

01-sphere-perspective
01-sphere-top
01-sphere-side

This kind of mesh is relatively easy to create and matches how textures for spheres are often stored. However, it uses a lot of triangles at the poles and it looks quite different when viewed from above than when viewed from a side. Ideally, the triangles would be distributed evenly across the sphere’s surface and would look the same no matter what side you view it from.

An alternative way to create a sphere is to start with one of the platonic solids, which are all more symmetrical than a UV sphere. A tetrahedron, cube, octahedron, dodecahedron, or icosahedron. Their vertices are all placed at the same distance from their center, so in that sense they are all approximations of a sphere. Then if you split all their faces into smaller faces and push the new vertices created that way to the same distance from its center, you end up with a better approximation of a sphere. Further subdividing results in more triangles and a better approximation.

The more faces you start with, the better the distribution of the triangles. So a tetrahedron is the worst candidate to begin with, while a icosahedron is the best. The octahedron falls somewhere in the middle and strikes a balance between quality and complexity, so we’ll use that one.

Octahedron

To consider the creation of an octahedron sphere in isolation, let’s create a static OctahedronSphereCreator class whose only job is to create our mesh. Give it a static Createmethod with parameters to specify how many subdivisions you want and what radius the sphere should have.

  1. using UnityEngine;
  2.  
  3. public static class OctahedronSphereCreator {
  4.  
  5.     public static Mesh Create (int subdivisions, float radius) {
  6.         return null;
  7.     }
  8. }

And to test our mesh, let’s create an OctahedronSphereTester component that simply creates the mesh when it awakens and assigns it to its MeshFilter.

  1. using UnityEngine;
  2.  
  3. [RequireComponent(typeof(MeshFilter))]
  4. public class OctahedronSphereTester : MonoBehaviour {
  5.  
  6.     public int subdivisions = 0;
  7.     public float radius = 1f;
  8.  
  9.     private void Awake () {
  10.         GetComponent<MeshFilter>().mesh = OctahedronSphereCreator.Create(subdivisions, radius);
  11.     }
  12. }

Now make a new game object with this component, and also give it a MeshRenderer and a default material.

02-game-object

Of course, nothing shows up yet when entering play mode. Let’s change the Create method so it produces an octahedron instead of retuning null.

  1.         Vector3[] vertices = {
  2.             Vector3.down,
  3.             Vector3.forward,
  4.             Vector3.left,
  5.             Vector3.back,
  6.             Vector3.right,
  7.             Vector3.up
  8.         };
  9.  
  10.         int[] triangles = {
  11.             0, 1, 2,
  12.             0, 2, 3,
  13.             0, 3, 4,
  14.             0, 4, 1,
  15.  
  16.             5, 2, 1,
  17.             5, 3, 2,
  18.             5, 4, 3,
  19.             5, 1, 4
  20.         };
  21.  
  22.         if (radius != 1f) {
  23.             for (int i = 0; i < vertices.Length; i++) {
  24.                 vertices[i] *= radius;
  25.             }
  26.         }
  27.  
  28.         Mesh mesh = new Mesh();
  29.         mesh.name = "Octahedron Sphere";
  30.         mesh.vertices = vertices;
  31.         mesh.triangles = triangles;
  32.         return mesh;

This creates an octahedron by first defining its bottom four triangles, moving between the forward, left, back, and right vertices, then adding the top four triangles in the same order.

While the octahedron will now show up when entering play mode, it will have weird shading. This is because we haven’t given it normals yet. Right now, we can simply use the vertices directly as normals, if we do so before setting them to the desired radius. But in general, we’d have to normalize all vertices after subdividing our mesh. So let’s add a Normalize method that does this and also fills an array of normals at the same time.

  1.     private static void Normalize (Vector3[] vertices, Vector3[] normals) {
  2.         for (int i = 0; i < vertices.Length; i++) {
  3.             normals[i] = vertices[i] = vertices[i].normalized;
  4.         }
  5.     }

We have to call this method before applying the radius and allocate the normals array before that. Oh, and don’t forget to assign it to the mesh.

  1.         Vector3[] normals = new Vector3[vertices.Length];
  2.         Normalize(vertices, normals);
  3.  
  4.         if (radius != 1f) {
  5.             for (int i = 0; i < vertices.Length; i++) {
  6.                 vertices[i] *= radius;
  7.             }
  8.         }
  9.  
  10.         Mesh mesh = new Mesh();
  11.         mesh.name = "Octahedron Sphere";
  12.         mesh.vertices = vertices;
  13.         mesh.normals = normals;
  14.         mesh.triangles = triangles;
  15.         return mesh;
02-octahedron

Now we have an octahedron that’s shaded as if it were a sphere. The next step is to define the texture coordinates. We’ll use the same longitude-latitude mapping as used by Unity’s sphere. While this is not a great way to texture a sphere, it’s easy and a lot of textures are available in this format. To see what it looks like, create a material with a test image like this one.

02-uv-test

And here it is applied to Unity’s UV sphere. It will always be messed up at the poles. You’ll either have discontinuities or extreme distortion, as shown below. You can also download it here.

02-uv-sphere

As you can see, there’s a seam where opposite sides of the texture join. We’ll place this seam in the forward direction, so that when looking at our mesh along the Z axis we’ll see the middle of the texture.

We can convert vertex positions into texture coordinates by using inverse trigonometric functions. The vertical coordinate is easiest, it’s simply asin(y) / Ï€ + ½. The horizontal case is slightly more complex, atan2(x, z) / -2Ï€. This actually produces a range that goes from 0 to ½ on the left side, then flips to -½ and goes back to 0 on the right side. So we have to add 1 if we end up with a negative result.

  1.     private static void CreateUV (Vector3[] vertices, Vector2[] uv) {
  2.         for (int i = 0; i < vertices.Length; i++) {
  3.             Vector3 v = vertices[i];
  4.             Vector2 textureCoordinates;
  5.             textureCoordinates.x = Mathf.Atan2(v.x, v.z) / (-2f * Mathf.PI);
  6.             if (textureCoordinates.x < 0f) {
  7.                 textureCoordinates.x += 1f;
  8.             }
  9.             textureCoordinates.y = Mathf.Asin(v.y) / Mathf.PI + 0.5f;
  10.             uv[i] = textureCoordinates;
  11.         }
  12.     }

Now that we have a convenient CreateUV method, we can call it in Create after the vertices have been normalized and later assign the coordinates to our mesh.

  1.         Vector3[] normals = new Vector3[vertices.Length];
  2.         Normalize(vertices, normals);
  3.  
  4.         Vector2[] uv = new Vector2[vertices.Length];
  5.         CreateUV(vertices, uv);
  6.  
  7.         if (radius != 1f) {
  8.             for (int i = 0; i < vertices.Length; i++) {
  9.                 vertices[i] *= radius;
  10.             }
  11.         }
  12.  
  13.         Mesh mesh = new Mesh();
  14.         mesh.name = "Octahedron Sphere";
  15.         mesh.vertices = vertices;
  16.         mesh.normals = normals;
  17.         mesh.uv = uv;
  18.         mesh.triangles = triangles;
  19.         return mesh;
02-octahedron-uv

We have now wrapped the texture around our octahedron, but it doesn’t look very good. There are two problems. First, because the polar vertices don’t rotate along with the other vertices the texture gets twisted into a circle, just like with Unity’s UV sphere. To prevent this, we have to use four vertices per pole so we rotate them along with their triangles. This means we have to change our vertices and triangles in Create.

  1.         Vector3[] vertices = {
  2.             Vector3.down, Vector3.down, Vector3.down, Vector3.down,
  3.             Vector3.forward,
  4.             Vector3.left,
  5.             Vector3.back,
  6.             Vector3.right,
  7.             Vector3.up, Vector3.up, Vector3.up, Vector3.up
  8.         };
  9.  
  10.         int[] triangles = {
  11.             0, 4, 5,
  12.             1, 5, 6,
  13.             2, 6, 7,
  14.             3, 7, 4,
  15.  
  16.              8, 5, 4,
  17.              9, 6, 5,
  18.             10, 7, 6,
  19.             11, 4, 7
  20.         };

Then we have to manually adjust the horizontal coordinates of the polar vertices after we finish our loop in CreateUV. As they should match the horizontal middle of the triangles they’re part of, they’re offset by â…› and increase in steps of ¼.

  1.         uv[vertices.Length - 4].x = uv[0].x = 0.125f;
  2.         uv[vertices.Length - 3].x = uv[1].x = 0.375f;
  3.         uv[vertices.Length - 2].x = uv[2].x = 0.625f;
  4.         uv[vertices.Length - 1].x = uv[3].x = 0.875f;
02-octahedron-polar

We have now undone the twisting, at the cost of introducing discontinuities, as part of the texture is skipped in between triangles.

The second problem is that the texture is flipped in the fourth quadrant, because the texture wraps all the way back to zero. To solve this, we have to duplicate the forward vertex, creating a seam that is 0 at the left side and 1 at the right side. So our vertex and triangle code changes once again.

  1.         Vector3[] vertices = {
  2.             Vector3.down, Vector3.down, Vector3.down, Vector3.down,
  3.             Vector3.forward,
  4.             Vector3.left,
  5.             Vector3.back,
  6.             Vector3.right,
  7.             Vector3.forward,
  8.             Vector3.up, Vector3.up, Vector3.up, Vector3.up
  9.         };
  10.  
  11.         int[] triangles = {
  12.             0, 4, 5,
  13.             1, 5, 6,
  14.             2, 6, 7,
  15.             3, 7, 8,
  16.  
  17.              9, 5, 4,
  18.             10, 6, 5,
  19.             11, 7, 6,
  20.             12, 8, 7
  21.         };

Now we have to update CreateUV so it assigns the correct coordinates to the right side of the seam. Assuming the vertices are placed by circling around the sphere, we can detect that we passed a seam by comparing X coordinates. If we get the same twice, we just started a new triangle row, which means that the previous vertex was the right side of the seam. Then all we have to do is set the horizontal texture coordinate of that vertex to 1. Here’s the complete code of CreateUV.

  1.     private static void CreateUV (Vector3[] vertices, Vector2[] uv) {
  2.         float previousX = 1f;
  3.         for (int i = 0; i < vertices.Length; i++) {
  4.             Vector3 v = vertices[i];
  5.             if (v.x == previousX) {
  6.                 uv[i - 1].x = 1f;
  7.             }
  8.             previousX = v.x;
  9.             Vector2 textureCoordinates;
  10.             textureCoordinates.x = Mathf.Atan2(v.x, v.z) / (-2f * Mathf.PI);
  11.             if (textureCoordinates.x < 0f) {
  12.                 textureCoordinates.x += 1f;
  13.             }
  14.             textureCoordinates.y = Mathf.Asin(v.y) / Mathf.PI + 0.5f;
  15.             uv[i] = textureCoordinates;
  16.         }
  17.         uv[vertices.Length - 4].x = uv[0].x = 0.125f;
  18.         uv[vertices.Length - 3].x = uv[1].x = 0.375f;
  19.         uv[vertices.Length - 2].x = uv[2].x = 0.625f;
  20.         uv[vertices.Length - 1].x = uv[3].x = 0.875f;
  21.     }
02-octahedron-seam

Subdivided Octahedron

To subdivide our octahedron means we replace each of its triangles with four smaller triangles. Each time we do this, the number of vertices and triangles increases. So one triangle turns into 4^s or 2^(2s) triangles, where s is the number of subdivision steps. As we start with eight triangles, our subdivided octahedron will end up with 2^(2s + 3) triangles.

The number of vertices is a little more complicated. First consider the resolution r of one octahedron face, which determines how many triangle rows it has. As we start with a single row and double this amount with each subdivision, r = 2^s. Now consider a single quadrant of our octahedron, which consists of two triangles that share one edge. Without subdivisions, it has a total of 1 + 2 + 1 = 4vertices. Subdividing once increases this to 1 + 2 + 3 + 2 + 1 = 9 vertices, the next step increases to 1 + 2 + 3 + 4 + 5 + 4 + 3 + 2 + 1 = 25 vertices, and so on. This means that one quadrant requires (r + 1)^2 vertices. The entire octahedron would have four times as many vertices, except that three of the quadrants share one vertex per row, of which there are 2r – 1. So the total number of vertices is 4(r + 1)^2 – 3(2r – 1). Here are the totals up to seven subdivision steps.

  1. 32 triangles, 27 vertices.
  2. 128 triangles, 79 vertices.
  3. 512 triangles, 279 vertices.
  4. 2,048 triangles, 1,063 vertices.
  5. 8,192 triangles, 4,167 vertices.
  6. 32,768 triangles, 16,519 vertices.
  7. 131,072 triangles, 65,799 verticles.

Because Unity uses 16-bit integers to index mesh data, it isn’t able to handle seven subdivisions in a single mesh. So let Create begin by clamping the amount of subdivisions and log a warning when an unsupported amount is requested.

  1.         if (subdivisions < 0) {
  2.             subdivisions = 0;
  3.             Debug.LogWarning("Octahedron Sphere subdivisions increased to minimum, which is 0.");
  4.         }
  5.         else if (subdivisions > 6) {
  6.             subdivisions = 6;
  7.             Debug.LogWarning("Octahedron Sphere subdivisions decreased to maximum, which is 6.");
  8.         }

It’s now time to remove the fixed vertex and triangle array and replace them with array declarations with the appropriate size. Because computer numbers are binary we can use the bit shifting operation 1 << n to compute 2^n. We then leave the filling of the arrays to a new method, CreateOctahedron. We pass it the arrays and the resolution, which is all it should need.

  1.         int resolution = 1 << subdivisions;
  2.         Vector3[] vertices = new Vector3[(resolution + 1) * (resolution + 1) * 4 -
  3.             (resolution * 2 - 1) * 3];
  4.         int[] triangles = new int[(1 << (subdivisions * 2 + 3)) * 3];
  5.         CreateOctahedron(vertices, triangles, resolution);

Because subdividing our octahedron is a recursive problem, we could solve it with a recursive approach. But we can use an iterative approach as well, by layering counter-clockwise circles of triangle strips from bottom to top. This matches how our other methods expect vertices to be ordered, so let’s do that.

We’ll use an integer v to keep track of our current vertex index and increase it each time we add another vertex. So every vertex assignment should look like vertices[v++] = followed by some value. The first four bottom vertices are easy.

  1.     private static void CreateOctahedron (Vector3[] vertices, int[] triangles, int resolution) {
  2.         int v = 0;
  3.  
  4.         for (int i = 0; i < 4; i++) {
  5.             vertices[v++] = Vector3.down;
  6.         }
  7.     }

Let’s now focus on one octahedron face first, the one that connects the forward and left main directions. In fact, let’s only consider the vertices along its left edge, the one going from bottom to forward. We already have the bottom vertex, what’s left is to add one vertex per triangle row until we reach the top of the face.

  1.         for (int i = 1; i <= resolution; i++) {
  2.             float progress = (float)i / resolution;
  3.             vertices[v++] = Vector3.Lerp(Vector3.down, Vector3.forward, progress);
  4.         }

Of course, we need more than one vertex per row. We need entire lines of vertices, their length increasing with each step until they reach the other edge.

  1.         for (int i = 1; i <= resolution; i++) {
  2.             float progress = (float)i / resolution;
  3.             Vector3 from, to;
  4.             vertices[v++] = from = Vector3.Lerp(Vector3.down, Vector3.forward, progress);
  5.             to = Vector3.Lerp(Vector3.down, Vector3.left, progress);
  6.             v = CreateVertexLine(from, to, i, v, vertices);
  7.         }

The convenient CreateVertexLine we just thought up is a simple method that adds vertices as it moves between two points and returns the next available vertex index.

  1.     private static int CreateVertexLine (
  2.         Vector3 from, Vector3 to, int steps, int v, Vector3[] vertices
  3.     ) {
  4.         for (int i = 1; i <= steps; i++) {
  5.             vertices[v++] = Vector3.Lerp(from, to, (float)i / steps);
  6.         }
  7.         return v;
  8.     }

To have CreateOctahedron add triangles we also have to keep track of the current triangle index, just like we’re doing for vertices. We will be placing triangles strips in between two rows of vertices, so it’s convenient if we also keep track of the first vertex index of the previous row. We name it vBottom because it defines the bottom of the triangle strip we’re adding.

We’ll also assume we have a CreateLowerStrip method to take care of creating a triangle strip for a bottom octahedron face. We invoke it before actually defining the vertices it will use, because at that point we know the starting indices of the bottom and top vertex rows that it needs.

  1.         int v = 0, vBottom = 0, t = 0;
  2.  
  3.         for (int i = 0; i < 4; i++) {
  4.             vertices[v++] = Vector3.down;
  5.         }
  6.  
  7.         for (int i = 1; i <= resolution; i++) {
  8.             float progress = (float)i / resolution;
  9.             Vector3 from, to;
  10.             vertices[v++] = from = Vector3.Lerp(Vector3.down, Vector3.forward, progress);
  11.             to = Vector3.Lerp(Vector3.down, Vector3.left, progress);
  12.             t = CreateLowerStrip(i, v, vBottom, t, triangles);
  13.             v = CreateVertexLine(from, to, i, v, vertices);
  14.             vBottom = v - 1 - i;
  15.         }

If we only consider the bottom row, there’s only one downward-pointing triangle that CreateLowerStrip has to worry about.

  1.     private static int CreateLowerStrip (int steps, int vTop, int vBottom, int t, int[] triangles) {
  2.         triangles[t++] = vBottom;
  3.         triangles[t++] = vTop - 1;
  4.         triangles[t++] = vTop;
  5.         return t;
  6.     }

However, this gives us only one triangle per row. Each step before the last should add two more triangles, switching between pointing downward and pointing upward, and incrementing both vertex indices.

  1.     private static int CreateLowerStrip (int steps, int vTop, int vBottom, int t, int[] triangles) {
  2.         for (int i = 1; i < steps; i++) {
  3.             triangles[t++] = vBottom;
  4.             triangles[t++] = vTop - 1;
  5.             triangles[t++] = vTop;
  6.  
  7.             triangles[t++] = vBottom++;
  8.             triangles[t++] = vTop++;
  9.             triangles[t++] = vBottom;
  10.         }
  11.         triangles[t++] = vBottom;
  12.         triangles[t++] = vTop - 1;
  13.         triangles[t++] = vTop;
  14.         return t;
  15.     }

Now you should be able to see the first face of the subdivided octahedron. You might need to rotate the view to find it.

03-1-subdivision
03-2-subdivisions

We actually want to complete a full circle each step instead of filling only one quadrant. To be able to easily loop over all four directions, create a static array to hold them.

  1.     private static Vector3[] directions = {
  2.         Vector3.left,
  3.         Vector3.back,
  4.         Vector3.right,
  5.         Vector3.forward
  6.     };

Instead of adding a vertex line and triangle strip only once per row, we actually have to do this four times. We have to advance vBottom per quadrant to keep it aligned. We have to add i - 1 to it, except for the first row where it has to be 1 not 0, as those vertices are duplicated. After completing each circle we can simply reset vBottom by subtracting that circle’s length from the current vertex index.

  1.         for (int i = 1; i <= resolution; i++) {
  2.             float progress = (float)i / resolution;
  3.             Vector3 from, to;
  4.             vertices[v++] = to = Vector3.Lerp(Vector3.down, Vector3.forward, progress);
  5.             for (int d = 0; d < 4; d++) {
  6.                 from = to;
  7.                 to = Vector3.Lerp(Vector3.down, directions[d], progress);
  8.                 t = CreateLowerStrip(i, v, vBottom, t, triangles);
  9.                 v = CreateVertexLine(from, to, i, v, vertices);
  10.                 vBottom += i > 1 ? (i - 1) : 1;
  11.             }
  12.             vBottom = v - 1 - i * 4;
  13.         }

Now the entire bottom hemisphere should be generated.

03-bottom

To include the top hemisphere, add a similar loop below the one for the bottom hemisphere. In this case we’ll be decreasing the number of steps per row, so we iterate in the opposite direction. Also, because the top four vertices are special, we won’t include the top row, so there’s no special case for vBottom either.

  1.         for (int i = resolution - 1; i >= 1; i--) {
  2.             float progress = (float)i / resolution;
  3.             Vector3 from, to;
  4.             vertices[v++] = to = Vector3.Lerp(Vector3.up, Vector3.forward, progress);
  5.             for (int d = 0; d < 4; d++) {
  6.                 from = to;
  7.                 to = Vector3.Lerp(Vector3.up, directions[d], progress);
  8.                 t = CreateUpperStrip(i, v, vBottom, t, triangles);
  9.                 v = CreateVertexLine(from, to, i, v, vertices);
  10.                 vBottom += i + 1;
  11.             }
  12.             vBottom = v - 1 - i * 4;
  13.         }

The CreateUpperStrip method basically acts as the opposite of CreateLowerStrip.

  1.     private static int CreateUpperStrip (int steps, int vTop, int vBottom, int t, int[] triangles) {
  2.         triangles[t++] = vBottom;
  3.         triangles[t++] = vTop - 1;
  4.         triangles[t++] = ++vBottom;
  5.         for (int i = 1; i <= steps; i++) {
  6.             triangles[t++] = vTop - 1;
  7.             triangles[t++] = vTop;
  8.             triangles[t++] = vBottom;
  9.  
  10.             triangles[t++] = vBottom;
  11.             triangles[t++] = vTop++;
  12.             triangles[t++] = ++vBottom;
  13.         }
  14.         return t;
  15.     }

Right now, only the top row is still missing, which is solved by adding a small loop at the end of CreateOctahedron. Here is the entire method.

  1.     private static void CreateOctahedron (Vector3[] vertices, int[] triangles, int resolution) {
  2.         int v = 0, vBottom = 0, t = 0;
  3.  
  4.         for (int i = 0; i < 4; i++) {
  5.             vertices[v++] = Vector3.down;
  6.         }
  7.  
  8.         for (int i = 1; i <= resolution; i++) {
  9.             float progress = (float)i / resolution;
  10.             Vector3 from, to;
  11.             vertices[v++] = to = Vector3.Lerp(Vector3.down, Vector3.forward, progress);
  12.             for (int d = 0; d < 4; d++) {
  13.                 from = to;
  14.                 to = Vector3.Lerp(Vector3.down, directions[d], progress);
  15.                 t = CreateLowerStrip(i, v, vBottom, t, triangles);
  16.                 v = CreateVertexLine(from, to, i, v, vertices);
  17.                 vBottom += i > 1 ? (i - 1) : 1;
  18.             }
  19.             vBottom = v - 1 - i * 4;
  20.         }
  21.  
  22.         for (int i = resolution - 1; i >= 1; i--) {
  23.             float progress = (float)i / resolution;
  24.             Vector3 from, to;
  25.             vertices[v++] = to = Vector3.Lerp(Vector3.up, Vector3.forward, progress);
  26.             for (int d = 0; d < 4; d++) {
  27.                 from = to;
  28.                 to = Vector3.Lerp(Vector3.up, directions[d], progress);
  29.                 t = CreateUpperStrip(i, v, vBottom, t, triangles);
  30.                 v = CreateVertexLine(from, to, i, v, vertices);
  31.                 vBottom += i + 1;
  32.             }
  33.             vBottom = v - 1 - i * 4;
  34.         }
  35.  
  36.         for (int i = 0; i < 4; i++) {
  37.             triangles[t++] = vBottom;
  38.             triangles[t++] = v;
  39.             triangles[t++] = ++vBottom;
  40.             vertices[v++] = Vector3.up;
  41.         }
  42.     }

Now the entire subdivided octahedron should show up when you enter play mode. Although the texture is of course different when viewed from above and from the front, the shape of the mesh is identical.

03-top
03-side

Texturing and Tangents

So how does the octahedron sphere perform when used for something practical, for example to represent a planet? Here’s an example texture for a planet, both a diffuse with specular map and a normal map. You only see the water because the alpha channel is used for the specular strength, which is set to 1 for water and 0 for land. You can download larger versions here.

planet-diffuse-specular
planet-normals

Here’s our sphere with a diffuse material, specular material, and bumped specular material. A directional light was added as well to get more interesting lighting.

04-diffuse-only
04-specular-only
04-without-tangents

The bumped specular version looks wrong, because we haven’t given our mesh tangent vectors yet, which are needed to work with normal maps. So let’s add a CreateTangents method.

The tangents are simply unit-length vectors that point along the horizontal texture direction as it is wrapped around our sphere. So they should follow flat counter-clockwise circles per vertex row. We can generate that from vertex positions by taking their X and Z coordinates, normalizing them to get the horizontal direction from the center, then rotating that 90 degrees counter-clockwise. The rotation can be done by simply converting (x, 0, z) into (-z, 0, x).

  1.     private static void CreateTangents (Vector3[] vertices, Vector4[] tangents) {
  2.         for (int i = 0; i < vertices.Length; i++) {
  3.             Vector3 v = vertices[i];
  4.             v.y = 0f;
  5.             v = v.normalized;
  6.             Vector3 tangent;
  7.             tangent.x = -v.z;
  8.             tangent.y = 0f;
  9.             tangent.z = v.x;
  10.             tangents[i] = tangent;
  11.         }
  12.     }

However, Unity uses 4D vectors to define tangents. The fourth component is used to determine how the normal and tangent should be combined to construct a 3D space in which the normal map is placed. It can be either 1 or -1. Because of the order in which Unity combines the normal and tangent, we have to use -1.

  1.     private static void CreateTangents (Vector3[] vertices, Vector4[] tangents) {
  2.         for (int i = 0; i < vertices.Length; i++) {
  3.             Vector3 v = vertices[i];
  4.             v.y = 0f;
  5.             v = v.normalized;
  6.             Vector4 tangent;
  7.             tangent.x = -v.z;
  8.             tangent.y = 0f;
  9.             tangent.z = v.x;
  10.             tangent.w = -1f;
  11.             tangents[i] = tangent;
  12.         }
  13.     }

As this approach does not work for the polar vertices, we once again define them separately, after the loop.

  1.         tangents[vertices.Length - 4] = tangents[0] = new Vector3(-1f, 0, -1f).normalized;
  2.         tangents[vertices.Length - 3] = tangents[1] = new Vector3(1f, 0f, -1f).normalized;
  3.         tangents[vertices.Length - 2] = tangents[2] = new Vector3(1f, 0f, 1f).normalized;
  4.         tangents[vertices.Length - 1] = tangents[3] = new Vector3(-1f, 0f, 1f).normalized;
  5.         for (int i = 0; i < 4; i++) {
  6.             tangents[vertices.Length - 1 - i].w = tangents[i].w = -1f;
  7.         }

All that’s left to do is have Create generate the tangents and add them to the mesh.

  1.         Vector4[] tangents = new Vector4[vertices.Length];
  2.         CreateTangents(vertices, tangents);
  3.  
  4.         if (radius != 1f) {
  5.             for (int i = 0; i < vertices.Length; i++) {
  6.                 vertices[i] *= radius;
  7.             }
  8.         }
  9.  
  10.         Mesh mesh = new Mesh();
  11.         mesh.name = "Octahedron Sphere";
  12.         mesh.vertices = vertices;
  13.         mesh.normals = normals;
  14.         mesh.uv = uv;
  15.         mesh.tangents = tangents;
  16.         mesh.triangles = triangles;
  17.         return mesh;

Now we can produce a properly shaded planet.

04-with-tangents

Comparing Spheres

So how does an octahedron sphere planet look compared to a UV sphere planet? Let’s first examine Unity’s UV sphere. It has 525 vertices and 760 triangles. It looks pretty good when viewed from the side, but its silhouette is a lot rougher when looking from above. And there is a large circular distortion visible at the pole.

04-uv-sphere-top
04-uv-sphere-front

Let’s compare it with an octahedron sphere that has been subdivided three times, which has 279 vertices and 512 triangles. That’s 37% and 67% of what the UV sphere has. It has a slightly better silhouette when viewed from above and looks a bit rougher when viewed from aside. The radial distortion is gone, instead we have a smaller diamond-shaped artifact.

04-oct-sphere-3-top
04-oct-sphere-3-front

What about if we use four subdivisions? In that case we have 1,063 (202%) vertices and 2,048 triangles (269%). This gives us a much better silhouette and the radius of the polar distortion has been roughly halved.

04-oct-sphere-4-top
04-oct-sphere-4-front

How many subdivision steps you really need depends on how large the planet appears. If you’re close you might need all six, while far away you can suffice with only two. You could use some LOD system to always pick the best option, either Unity Pro’s or a custom one.

Mesh Assets

So far we have been using a test component to generate our meshes in play mode, but it would be very convenient to have them available like any other mesh asset. You can use an editor script to save meshes as asset files. Here is a simple wizard that allows you to configure and save an octahedron sphere mesh in a Unity asset file. The script should to be placed in an Editor folder.

  1. using UnityEditor;
  2. using UnityEngine;
  3.  
  4. public class OctahedronSphereWizard : ScriptableWizard {
  5.  
  6.     [MenuItem("Assets/Create/Octahedron Sphere")]
  7.     private static void CreateWizard () {
  8.         ScriptableWizard.DisplayWizard<OctahedronSphereWizard>("Create Octahedron Sphere");
  9.     }
  10.  
  11.     [Range(0, 6)]
  12.     public int level = 6;
  13.     public float radius = 1f;
  14.  
  15.     private void OnWizardCreate () {
  16.         string path = EditorUtility.SaveFilePanelInProject(
  17.             "Save Octahedron Sphere",
  18.             "Octahedron Sphere",
  19.             "asset",
  20.             "Specify where to save the mesh.");
  21.         if (path.Length > 0) {
  22.             Mesh mesh = OctahedronSphereCreator.Create(level, radius);
  23.             MeshUtility.Optimize(mesh);
  24.             AssetDatabase.CreateAsset(mesh, path);
  25.             Selection.activeObject = mesh;
  26.         }
  27.     }
  28. }

Enjoy your octahedron spheres!

The finished project and mesh assets can be downloaded here.

Author: Jasper Flick

Scroll to Top