I’m working on a little planet simulator project, and I wanted to use a Fibonacci spiral sphere to generate the planet mesh. I like that it isn’t a square grid and somewhat organic, but I’m having trouble UV-mapping the procedural mesh. I not only have a jagged looking tear in the texture, but it also stretches the texture in a strange way, as shown in the picture below. I’d really like to generate textures for use later on, so I want to try to fix this now.

My code uses this library (using GK;) to triangulate the sphere’s mesh. I’ve included the entire code that generates the Fibonacci Spiral Sphere below and the Sphere script below that.
I start by using the updatePoints() function to generate the vertices of the Fibonacci Spiral Sphere. It calls the Fibonacci_Spiral_Sphere() function in the FibonacciPoints3D script, which generates the vertices and triangles for the mesh. I also add the final vertex and the triangles needed to close the sphere at the bottom using stitchMesh().
As you can see in the Fibonacci script, I’ve tried a few different projections to try to find a UV that works. The Stereographic projection is used for triangle calculation but isn’t useful for UV projection. I’ve tried Mercator and icosphere projections, but neither has given good results.
I think I can fix the jagged line and weird poles by following this example. My question is this: Are there any resources about procedurally UV unwrapping a Fibonacci spiral sphere? Or should I just give up and go back to Isospheres?
using System;
using System.Collections;
using System.Collections.Generic;
using System.Collections.Specialized;
using UnityEngine;
using GK;
namespace FP
{
public class FibonacciPoints3D
{
private static float gr = (Mathf.Sqrt(5) + 1) / 2; // golden ratio = 1.6180339887
private static float ga = (2f - gr) * (2f * Mathf.PI); // golden angle = 2.3999632297
private DelaunayCalculator Dc;
public DelaunayTriangulation Dt;
public List<Vector3> Points3D;
public List<Vector2> Points2D;
public List<Vector3> PointsM;
public float GoldenRatio { get { return gr; } }
public float GoldenAngle { get { return ga; } }
//public List<Vector3> Points3D {get { return Points3D; } }
public FibonacciPoints3D()
{
Points3D = new List<Vector3>();
Points2D = new List<Vector2>();
PointsM = new List<Vector3>();
Dc = new DelaunayCalculator();
Dt = null;
}
public List<Vector3> Fibonacci_Spiral_Sphere(int num_Points3D, float r)
{
List<Vector3> positions = new List<Vector3>();
for (int i = 0; i < num_Points3D; i++)
{
float lat = Mathf.Asin((float)-1.0 + (float)2.0 * (float)i / (num_Points3D + 1));
float lon = ga * (float)i;
float x = Mathf.Cos(lon) * Mathf.Cos(lat);
float y = Mathf.Sin(lon) * Mathf.Cos(lat);
float z = Mathf.Sin(lat);
Vector3 pos = (new Vector3(x, y, z) * r);
positions.Add(RotateX_90(pos));
}
Points3D = positions;
Stereograph_Project_Sphere(r);
Dt = Dc.CalculateTriangulation(Points2D);
return positions;
}
private Vector3 RotateX_90(Vector3 vec)
{
float x = 1*vec.x;
float y = -1*vec.z;
float z = 1*vec.y;
return new Vector3(x,y,z);
}
public List<Vector2> Stereograph_Project_Sphere(float radius)
{
List<Vector2> positions = new List<Vector2>();
for (int i = 0; i < Points3D.Count; i++)
{
positions.Add(Stereograph_Projection(Points3D(i), radius));
}
Points2D = positions;
return positions;
}
public Vector2 Stereograph_Projection(Vector3 point3, float radius)
{
float x = point3.x / (point3.y + radius);
float y = point3.z / (point3.y + radius);
return new Vector2(y, x);
}
public Vector3 Reverse_Stereograph_Projection(Vector2 point2)
{
float x = (2 * point2.x) / (1f + Mathf.Pow(point2.x, 2) + Mathf.Pow(point2.y, 2));
float y = (2 * point2.y) / (1f + Mathf.Pow(point2.x, 2) + Mathf.Pow(point2.y, 2));
float z = (-1f + Mathf.Pow(point2.x, 2) + Mathf.Pow(point2.y, 2)) /
(1f + (1 * Mathf.Pow(point2.x, 2)) + (1 * Mathf.Pow(point2.y, 2)));
return new Vector3(x, y, z);
}
public List<Vector3> Mercator_Project_Sphere(Vector3 PrimeMeridian, float radius, Vector2 scale)
{
List<Vector3> positions = new List<Vector3>();
float yMin = 0;
float yMax = 0;
float xMin = 0;
float xMax = 0;
for(int i = 0; i < Points3D.Count; i++)
{
positions.Add(Mercator_Projection(Points3D(i), PrimeMeridian, radius));
if(positions(i).y < yMin) yMin = positions(i).y;
if(positions(i).y > yMax) yMax = positions(i).y;
if(positions(i).x < xMin) xMin = positions(i).x;
if(positions(i).x > xMax) xMax = positions(i).x;
}
//UnityEngine.Debug.Log("y " + yMin + ":" + yMax + " | " + xMin + ":" + xMax);
positions(0) = new Vector3(0f * scale.x, 1f * scale.y, 0);
for(int i = 1; i < Points3D.Count; i++)
{
float x = ((positions(i).x + xMin) / (xMax - xMin)) * scale.x;
float y = ((positions(i).y - yMin) / (yMax - yMin)) * scale.y;
positions(i) = new Vector3(x, y, 0);
}
PointsM = positions;
return positions;
}
public Vector3 Mercator_Projection(Vector3 point3, Vector3 PrimeMeridian, float radius)
{
///This Mercator Projection uses the provided Prime Meridian (i.e. the
///transform.right of the parent object.
float PM = Mathf.Atan2(PrimeMeridian.z, PrimeMeridian.x);
float lat = Mathf.Asin(point3.y / radius);
float lon = Mathf.Atan2(point3.z, point3.x);
//Debug.Log(point3 + " | " + lat + " " + lon);
float x = lon - PM;
float y = Mathf.Log(Mathf.Tan(lat) + 1/Mathf.Cos(lat));
return new Vector3(x, y, 0);
}
public List<Vector2> UV_Project_Sphere()
{
List<Vector2> positions = new List<Vector2>();
for(int i = 0; i < Points3D.Count; i++)
{
positions.Add(UV_Projection(Points3D(i)));
}
return positions;
}
public Vector2 UV_Projection(Vector3 point3)
{
float u = 0.5f + ((Mathf.Atan2(point3.z, point3.x)) / 2 * Mathf.PI);
float v = 0.5f - ((Mathf.Asin(point3.y)) / Mathf.PI);
return new Vector2(u, v);
}
}
}
Below is the Sphere code.
using System.Collections;
using System.Collections.Generic;
using UnityEngine;
using System.Collections.Specialized;
using System;
using UnityEditor;
using System.Diagnostics;
using FP;
using WV;
using System.Security.Cryptography;
using System.Runtime.InteropServices;
(RequireComponent(typeof(MeshFilter)))
public class Sphere : MonoBehaviour
{
public bool MapProjection = true;
public int numberOfPoints = 10;
private int m_numberOfPoints = 10;
public float pointsRadius = 1;
private float m_pointsRadius = 1;
private FibonacciPoints3D Fp;
public WeatherVoxels Wv;
(HideInInspector)
public Vector3() vertices;
(HideInInspector)
public int() triangles;
private Mesh mesh;
public Transform Map;
public Vector2 scale;
public int select = 0;
// Start is called before the first frame update
void Start()
{
Fp = new FibonacciPoints3D();
vertices = new Vector3(numberOfPoints);
triangles = new int(numberOfPoints*3);
mesh = new Mesh();
GetComponent<MeshFilter>().mesh = mesh;
mesh.name = "Planet";
updatePoints(numberOfPoints, pointsRadius);
stitchMesh(ref triangles, ref vertices);
makeMesh(triangles, vertices);
Wv = new WeatherVoxels(vertices, triangles);
}
// Update is called once per frame
void Update()
{
if (numberOfPoints != m_numberOfPoints || pointsRadius != m_pointsRadius)
{
updatePoints(numberOfPoints, pointsRadius);
//UnityEngine.Debug.Log("Points Changed");
stitchMesh(ref triangles, ref vertices);
makeMesh(triangles, vertices);
Wv = new WeatherVoxels(vertices, triangles);
}
}
void updatePoints(int numPoints, float radius)
{
if (numPoints < 50)
{
numPoints = 50;
}
if(radius <= 0.01f)
{
radius = 0.01f;
}
m_numberOfPoints = numPoints;
m_pointsRadius = radius;
Fp.Fibonacci_Spiral_Sphere(numPoints, radius);
Fp.Mercator_Project_Sphere(transform.right, radius, scale);
vertices = Fp.Points3D.ToArray();
triangles = Fp.Dt.Triangles.ToArray();
}
void makeMesh(int() tris, Vector3() verts)
{
mesh.Clear();
mesh.vertices = verts;
mesh.triangles = tris;
mesh.RecalculateNormals();
List<Vector2> Mm = Fp.UV_Project_Sphere();
Vector2() M = new Vector2(verts.Length);
for(int i = 0; i < Mm.Count - 1; i++)
{
M(i) = Mm(i);
}
M(vertices.Length - 1) = new Vector2(
0.5f + ((Mathf.Atan2(vertices(vertices.Length - 1).z, vertices(vertices.Length - 1).x)) / (2 * Mathf.PI)),
0.5f - ((Mathf.Asin(vertices(vertices.Length - 1).y))/ (Mathf.PI))
);
mesh.uv = M;
}
void stitchMesh(ref int() tris, ref Vector3() verts)
{
//UnityEngine.Debug.Log("Before " + vertices.Length);
Vector3 sPole = new Vector3(0,
0 - pointsRadius, 0);
Vector3() newVerts = new Vector3(verts.Length + 1);
for(int i = 0; i < verts.Length; i++)
{
newVerts(i) = verts(i);
}
newVerts(newVerts.Length-1) = sPole;
verts = newVerts;
//UnityEngine.Debug.Log("After " + vertices.Length);
int() newTris = new int(tris.Length + 15);
for(int i = 0; i < tris.Length; i++)
{
newTris(i) = tris(i);
}
newTris(tris.Length) = verts.Length-2;
newTris(tris.Length + 1) = verts.Length-4;
newTris(tris.Length + 2) = verts.Length-1;
newTris(tris.Length + 3) = verts.Length-4;
newTris(tris.Length + 4) = verts.Length-6;
newTris(tris.Length + 5) = verts.Length-1;
newTris(tris.Length + 6) = verts.Length-6;
newTris(tris.Length + 7) = verts.Length-3;
newTris(tris.Length + 8) = verts.Length-1;
newTris(tris.Length + 9) = verts.Length-3;
newTris(tris.Length + 10) = verts.Length-5;
newTris(tris.Length + 11) = verts.Length-1;
newTris(tris.Length + 12) = verts.Length-5;
newTris(tris.Length + 13) = verts.Length-2;
newTris(tris.Length + 14) = verts.Length-1;
tris = newTris;
}
public List<Vector3> GetAdjascentPositions(int index, Vector3() verts, int() triangles)
{
List<int> tris = new List<int>();
List<Vector3> result = new List<Vector3>();
for (int j = 0; j < triangles.Length; j++)
{
if (triangles(j) == Mathf.FloorToInt(index))
{
switch (j % 3)
{
case (0):
if (!tris.Contains(triangles(j))) { tris.Add(triangles(j)); }
if (!tris.Contains(triangles(j + 1))) { tris.Add(triangles(j + 1)); }
if (!tris.Contains(triangles(j + 2))) { tris.Add(triangles(j + 2)); }
break;
case (1):
if (!tris.Contains(triangles(j - 1))) { tris.Add(triangles(j - 1)); }
if (!tris.Contains(triangles(j))) { tris.Add(triangles(j)); }
if (!tris.Contains(triangles(j + 1))) { tris.Add(triangles(j + 1)); }
break;
case (2):
if (!tris.Contains(triangles(j - 2))) { tris.Add(triangles(j - 2)); }
if (!tris.Contains(triangles(j - 1))) { tris.Add(triangles(j - 1)); }
if (!tris.Contains(triangles(j))) { tris.Add(triangles(j)); }
break;
default:
UnityEngine.Debug.LogError("wierd stuff happened while looking through triangles array.");
break;
}
}
}
foreach (int point in tris)
{
result.Add(transform.TransformPoint(verts(point)));
}
return result;
}
void OnDrawGizmos()
{
if(Fp != null){
for(int i=0; i<vertices.Length; i++)
{
if((i == select))
{
Gizmos.color = Color.white;
for(int j = 0; j < Wv.voxels(select).neighbors.Count; j++)
{
Gizmos.DrawWireSphere(transform.TransformPoint(Wv.voxels(select).neighbors(j).position)
, 0.02f);
}
}
else if(i > Fp.Points3D.Count - 6 && i < vertices.Length - 1)
{
Gizmos.color = Color.red;
}
else if(i == vertices.Length - 1)
{
Gizmos.color = Color.cyan;
}
else
{
Gizmos.color = Color.Lerp(Color.green, Color.blue,
((float)i / (float)Fp.Points3D.Count));
}
Gizmos.DrawSphere(transform.TransformPoint(
vertices(i)), 0.01f);
if(i < vertices.Length - 1)
{
if (MapProjection)
{
Gizmos.DrawSphere(Map.transform.position + new Vector3(
Fp.PointsM(i).x, Fp.PointsM(i).y, 0), 0.05f);
}
else
{
Gizmos.DrawSphere(Map.transform.position + new Vector3(
Fp.Points2D(i).x, Fp.Points2D(i).y, 0), 0.05f);
}
}
}
if (MapProjection)
{
Gizmos.DrawSphere(Map.transform.position + new Vector3(0, 0, 0), 0.05f);
}
}
}
}