I'm making a game and I need a way to generate a sphere mesh in Unity for whatever reason. It needs to be generated by code and not be a pre-made sphere or a sphere primitive as I need to edit in generation later on.
1 Answer
So, I've already done some research on this and I've found an Icosphere generation code on Unity Wiki and it's not terribly hard to find, but still I wanted to share what I have on the matter, since it took me an afternoon or so to find and modify this.
First up is the Icosphere generation code, you just need to have this in a script but not on any objects. This is just a public API for your other scripts to use. I've added a few things here and there in this code but it's not too different from what the wiki page provides.
using System.Collections;
using System.Collections.Generic;
using UnityEngine;
public static class IcoSphere
{
private struct TriangleIndices
{
public int v1;
public int v2;
public int v3;
public TriangleIndices(int v1, int v2, int v3)
{
this.v1 = v1;
this.v2 = v2;
this.v3 = v3;
}
}
// return index of point in the middle of p1 and p2
private static int getMiddlePoint(int p1, int p2, ref List<Vector3> vertices, ref Dictionary<long, int> cache, float radius)
{
// first check if we have it already
bool firstIsSmaller = p1 < p2;
long smallerIndex = firstIsSmaller ? p1 : p2;
long greaterIndex = firstIsSmaller ? p2 : p1;
long key = (smallerIndex << 32) + greaterIndex;
int ret;
if (cache.TryGetValue(key, out ret))
{
return ret;
}
// not in cache, calculate it
Vector3 point1 = vertices[p1];
Vector3 point2 = vertices[p2];
Vector3 middle = new Vector3
(
(point1.x + point2.x) / 2f,
(point1.y + point2.y) / 2f,
(point1.z + point2.z) / 2f
);
// add vertex makes sure point is on unit sphere
int i = vertices.Count;
vertices.Add(middle.normalized * radius);
// store it, return index
cache.Add(key, i);
return i;
}
public static void Create(GameObject gameObject)
{
MeshFilter filter = gameObject.GetComponent<MeshFilter>();
Mesh mesh = filter.mesh;
mesh.Clear();
Vector3[] vertices = gameObject.GetComponent<MeshFilter>().mesh.vertices;
List<Vector3> vertList = new List<Vector3>();
Dictionary<long, int> middlePointIndexCache = new Dictionary<long, int>();
int index = 0;
int recursionLevel = 3;
float radius = 1f;
// create 12 vertices of a icosahedron
float t = (1f + Mathf.Sqrt(5f)) / 2f;
vertList.Add(new Vector3(-1f, t, 0f).normalized * radius);
vertList.Add(new Vector3(1f, t, 0f).normalized * radius);
vertList.Add(new Vector3(-1f, -t, 0f).normalized * radius);
vertList.Add(new Vector3(1f, -t, 0f).normalized * radius);
vertList.Add(new Vector3(0f, -1f, t).normalized * radius);
vertList.Add(new Vector3(0f, 1f, t).normalized * radius);
vertList.Add(new Vector3(0f, -1f, -t).normalized * radius);
vertList.Add(new Vector3(0f, 1f, -t).normalized * radius);
vertList.Add(new Vector3(t, 0f, -1f).normalized * radius);
vertList.Add(new Vector3(t, 0f, 1f).normalized * radius);
vertList.Add(new Vector3(-t, 0f, -1f).normalized * radius);
vertList.Add(new Vector3(-t, 0f, 1f).normalized * radius);
// create 20 triangles of the icosahedron
List<TriangleIndices> faces = new List<TriangleIndices>();
// 5 faces around point 0
faces.Add(new TriangleIndices(0, 11, 5));
faces.Add(new TriangleIndices(0, 5, 1));
faces.Add(new TriangleIndices(0, 1, 7));
faces.Add(new TriangleIndices(0, 7, 10));
faces.Add(new TriangleIndices(0, 10, 11));
// 5 adjacent faces
faces.Add(new TriangleIndices(1, 5, 9));
faces.Add(new TriangleIndices(5, 11, 4));
faces.Add(new TriangleIndices(11, 10, 2));
faces.Add(new TriangleIndices(10, 7, 6));
faces.Add(new TriangleIndices(7, 1, 8));
// 5 faces around point 3
faces.Add(new TriangleIndices(3, 9, 4));
faces.Add(new TriangleIndices(3, 4, 2));
faces.Add(new TriangleIndices(3, 2, 6));
faces.Add(new TriangleIndices(3, 6, 8));
faces.Add(new TriangleIndices(3, 8, 9));
// 5 adjacent faces
faces.Add(new TriangleIndices(4, 9, 5));
faces.Add(new TriangleIndices(2, 4, 11));
faces.Add(new TriangleIndices(6, 2, 10));
faces.Add(new TriangleIndices(8, 6, 7));
faces.Add(new TriangleIndices(9, 8, 1));
// refine triangles
for (int i = 0; i < recursionLevel; i++)
{
List<TriangleIndices> faces2 = new List<TriangleIndices>();
foreach (var tri in faces)
{
// replace triangle by 4 triangles
int a = getMiddlePoint(tri.v1, tri.v2, ref vertList, ref middlePointIndexCache, radius);
int b = getMiddlePoint(tri.v2, tri.v3, ref vertList, ref middlePointIndexCache, radius);
int c = getMiddlePoint(tri.v3, tri.v1, ref vertList, ref middlePointIndexCache, radius);
faces2.Add(new TriangleIndices(tri.v1, a, c));
faces2.Add(new TriangleIndices(tri.v2, b, a));
faces2.Add(new TriangleIndices(tri.v3, c, b));
faces2.Add(new TriangleIndices(a, b, c));
}
faces = faces2;
}
mesh.vertices = vertList.ToArray();
List<int> triList = new List<int>();
for (int i = 0; i < faces.Count; i++)
{
triList.Add(faces[i].v1);
triList.Add(faces[i].v2);
triList.Add(faces[i].v3);
}
mesh.triangles = triList.ToArray();
mesh.uv = new Vector2[vertices.Length];
Vector3[] normales = new Vector3[vertList.Count];
for (int i = 0; i < normales.Length; i++)
normales[i] = vertList[i].normalized;
mesh.normals = normales;
mesh.RecalculateBounds();
mesh.RecalculateTangents();
mesh.RecalculateNormals();
//mesh.Optimize();
}
}
Now we create the generator script. The method that I provide is by no means the best way to do this (especially since Unity itself complains that generating meshes in the editor will lead to memory leaks). Anyway, here's the generator script:
using System.Collections;
using System.Collections.Generic;
using UnityEngine;
public class GeneratePlanet : MonoBehaviour
{
public Material planetMaterial = ;
public float planetSize = 1f;
GameObject planet;
Mesh planetMesh;
Vector3[] planetVertices;
int[] planetTriangles;
MeshRenderer planetMeshRenderer;
MeshFilter planetMeshFilter;
MeshCollider planetMeshCollider;
public void CreatePlanet()
{
CreatePlanetGameObject();
//do whatever else you need to do with the sphere mesh
RecalculateMesh();
}
void CreatePlanetGameObject()
{
planet = new GameObject();
planetMeshFilter = planet.AddComponent<MeshFilter>();
planetMesh = planetMeshFilter.mesh;
planetMeshRenderer = planet.AddComponent<MeshRenderer>();
//need to set the material up top
planetMeshRenderer.material = planetMaterial;
planet.transform.localScale = new Vector3(planetSize, planetSize, planetSize);
IcoSphere.Create(planet);
}
void RecalculateMesh()
{
planetMesh.RecalculateBounds();
planetMesh.RecalculateTangents();
planetMesh.RecalculateNormals();
}
}
Now at this point, you can just put the CreatePlanet()
method inside the Start()
method and slap this script on a game object, and you'll have a "planet" that is made in the Icosphere style. This differs greatly from the Unity's own primitive, which is made by six faces that are bent until they make a sphere (essentially a modified cube).
Anyway, if you want to use this as I do, then you can use this next script to create a button on the object itself so that you can create it in the editor (again, not recommended by the Unity editor itself):
using UnityEngine;
using System.Collections;
using UnityEditor;
[CustomEditor(typeof(GeneratePlanet))]
public class GeneratePlanetEditor : Editor
{
public override void OnInspectorGUI()
{
DrawDefaultInspector();
GeneratePlanet myScript = (GeneratePlanet)target;
if (GUILayout.Button("Create Planet"))
{
myScript.CreatePlanet();
}
}
}
The end result should be a good Icosphere with the shown vertices. (The vertex spheres are just drawn by gizmos on a simple loop).
The advantage of Icosphere is that its vertices are almost equidistant from each other and they're all almost the same distance from the center of the sphere. The disadvantage is that, if you want to make a more detailed sphere, you need quite a lot more vertices and that's going to be a bigger performance hit than some other methods.
Hope y'all find this as useful as I did!