How to get a smoothed Icosphere procedurally

Hello,

I’m making a planet generator and to build the sphere procedurally I decided to use an Icosphere because the triangles that compose it all have the same size which is I think the best option for this kind of project to after implement a level of detail system ( please tell me if I’m wrong and if there is a best solution )

So I followed this tutorial [here][1] to build the sphere, it works very well but I don’t get a smooth sphere even with a lot of triangles.


For exemple here is the Icosphere with 5120 triangles which is a lot I think compare to unity sphere which is smooth for 760 triangles. I guess when I increase the number of triangles I get a better result but I don’t think it’s a good solution to have more than 100k triangles for a sphere which is not even smooth.

[191310-capture-decran-2022-01-17-a-195954.png*_|191310]


So is there a solution to smooth triangles or to simply have a smooth sphere like the unity sphere with an Icosphere ?

Thanks !

edit Copied from Answer:

I’m sorry I thought my code will be very too long to be posted. So here it is, I understand what you said and normally I normalize the points in the GetVertexToUnitSphere function in the Utility class which is called for each triangle instance, but I’m still learning the mesh system so I guess a part of my code could be wrong

using System;
using System.Collections;
using System.Collections.Generic;
using UnityEngine;

[Serializable]
public class CelestialShapeSettings
{
    [Range(0, 10)]
    public int minDepth = 6;
    public float radius = 10;
    public NoiseLayer[] noiseLayers;
}

public class CelestialShape
{

    CelestialShapeSettings settings;

    Mesh mesh;

    Vector3[] vertices;
    List<Triangle> triangles;

    NoiseFilter[] noiseFilters;


    public CelestialShape(Mesh mesh, CelestialShapeSettings settings)
    {
        this.mesh = mesh;
        this.settings = settings;

        noiseFilters = new NoiseFilter[settings.noiseLayers.Length];
        for (int i = 0; i < noiseFilters.Length; i++)
        {
            noiseFilters _= new NoiseFilter(settings.noiseLayers*.settings);*_

}

SetupVertices();
}

private void SetupVertices()
{
if (vertices == null)
{
// goldenRatio
float t = (1.0f + (float)Math.Sqrt(5.0)) / 2.0f;

vertices = new Vector3[] {
// Trace the four vertices of a Golden rectangle [R1]
Utility.GetVertexToUnitSphere(new Vector3(-1, t, 0)),
Utility.GetVertexToUnitSphere(new Vector3(1, t, 0)),
Utility.GetVertexToUnitSphere(new Vector3(-1, -t, 0)),
Utility.GetVertexToUnitSphere(new Vector3(1, -t, 0)),

// Trace the four verices of a Golden rectangle orthagonal to the last [R2]
Utility.GetVertexToUnitSphere(new Vector3(0, -1, t)),
Utility.GetVertexToUnitSphere(new Vector3(0, 1, t)),
Utility.GetVertexToUnitSphere(new Vector3(0, -1, -t)),
Utility.GetVertexToUnitSphere(new Vector3(0, 1, -t)),

// Trace the four verices of a Golden rectangle orthagonal to the last two [R3]
Utility.GetVertexToUnitSphere(new Vector3(t, 0, -1)),
Utility.GetVertexToUnitSphere(new Vector3(t, 0, 1)),
Utility.GetVertexToUnitSphere(new Vector3(-t, 0, -1)),
Utility.GetVertexToUnitSphere(new Vector3(-t, 0, 1))
};
}
}

private void SetupTriangles()
{
triangles = new List {
// 5 faces around point 0
new Triangle(vertices[0], vertices[11], vertices[5]),
new Triangle(vertices[0], vertices[5], vertices[1]),
new Triangle(vertices[0], vertices[1], vertices[7]),
new Triangle(vertices[0], vertices[7], vertices[10]),
new Triangle(vertices[0], vertices[10], vertices[11]),

// 5 adjacent faces
new Triangle(vertices[1], vertices[5], vertices[9]),
new Triangle(vertices[5], vertices[11], vertices[4]),
new Triangle(vertices[11], vertices[10], vertices[2]),
new Triangle(vertices[10], vertices[7], vertices[6]),
new Triangle(vertices[7], vertices[1], vertices[8]),

// 5 faces around point 3
new Triangle(vertices[3], vertices[9], vertices[4]),
new Triangle(vertices[3], vertices[4], vertices[2]),
new Triangle(vertices[3], vertices[2], vertices[6]),
new Triangle(vertices[3], vertices[6], vertices[8]),
new Triangle(vertices[3], vertices[8], vertices[9]),

// 5 adjacent faces
new Triangle(vertices[4], vertices[9], vertices[5]),
new Triangle(vertices[2], vertices[4], vertices[11]),
new Triangle(vertices[6], vertices[2], vertices[10]),
new Triangle(vertices[8], vertices[6], vertices[7]),
new Triangle(vertices[9], vertices[8], vertices[1])
};
}

public void GenerateMesh()
{
SetupTriangles();
RecurseUniformally(settings.minDepth);
BuildMesh();
}

private void BuildMesh()
{
int trianglesLength = triangles.Count;
int arraysLength = trianglesLength * 3;

Vector3[] meshVertices = new Vector3[arraysLength];
int[] meshTriangles = new int[arraysLength];
for (int i = 0; i < trianglesLength; i++)
{
int aBaseIndex = i * 3;
Triangle t = triangles*;*

meshVertices[aBaseIndex] = PointOnSphere(t.v1);
meshVertices[aBaseIndex + 1] = PointOnSphere(t.v2);
meshVertices[aBaseIndex + 2] = PointOnSphere(t.v3);

meshTriangles[aBaseIndex] = aBaseIndex;
meshTriangles[aBaseIndex + 1] = aBaseIndex + 1;
meshTriangles[aBaseIndex + 2] = aBaseIndex + 2;
}

mesh.Clear();
mesh.vertices = meshVertices;
mesh.triangles = meshTriangles;
mesh.RecalculateNormals();
}

private Vector3 PointOnSphere(Vector3 p)
{
return p * settings.radius;
}

private void RecurseUniformally(int depth)
{
for (int i = 0; i < depth; i++)
{
List toRemove = new List();
int tLength = triangles.Count;

for (int ti = 0; ti < tLength; ti++)
{
var t = triangles[ti];
if (t.isVisible)
RecurseTriangle(t);
toRemove.Add(t);
}

foreach (var t in toRemove)
{
triangles.Remove(t);
}
}
}

private void RecurseTriangle(Triangle triangle)
{
triangles.AddRange(triangle.Recurse());
}

private class Triangle
{
/*
* v2
* *
* * *
* * *
* adj1 * d2 * adj2
* a ----- b
* * | d4 | *
* * | | *
* * d1 | | d3 *
* v1 * * * * c * * * * v3
*
* adj3
*
*/

public Vector3 v1;
public Vector3 v2;
public Vector3 v3;

// Adjacents triangles
public Triangle adj1;
public Triangle adj2;
public Triangle adj3;

// Descendants triangles, or null
Triangle desc1;
Triangle desc2;
Triangle desc3;
Triangle desc4;

// Parent triangle, or null
Triangle parent;

public bool isVisible = true;

// current recursive depth
int depth;

public Triangle(Vector3 v1, Vector3 v2, Vector3 v3)
{
this.v1 = v1;
this.v2 = v2;
this.v3 = v3;
}
public Triangle(Vector3 v1, Vector3 v2, Vector3 v3, int depth, Triangle parent)
{
this.v1 = v1;
this.v2 = v2;
this.v3 = v3;
this.depth = depth;
this.parent = parent;
}

public Triangle[] Recurse()
{
int newDepth = depth + 1;

Vector3 a = Utility.GetVertexToUnitSphere(Utility.GetMidpoint(v1, v2));
Vector3 b = Utility.GetVertexToUnitSphere(Utility.GetMidpoint(v2, v3));
Vector3 c = Utility.GetVertexToUnitSphere(Utility.GetMidpoint(v1, v3));

desc1 = new Triangle(v1, a, c, newDepth, this);
desc2 = new Triangle(a, v2, b, newDepth, this);
desc3 = new Triangle(c, b, v3, newDepth, this);
desc4 = new Triangle(b, c, a, newDepth, this);

// Adjacencies

isVisible = false;

return new Triangle[] { desc1, desc2, desc3, desc4 };
}
}

private static class Utility
{
// Get a vector to the unit sphere.
public static Vector3 GetVertexToUnitSphere(Vector3 v)
{
return v.normalized;
}

// Find the midpoint between two 3D points.
public static Vector3 GetMidpoint(Vector3 p1, Vector3 p2)
{
return Vector3.Lerp(p1, p2, .5f);
}
}
}
*[1]: https://superhedral.com/2020/05/17/the-icospherical-world-model-problem-specification-and-preliminary-data-structure/*_
*

Well, you haven’t shared any of your code so we have no idea how you generate your vertex normals because those are the important things to make it “look” smooth. Though for a sphere around the local center at (0,0,0) the normal is simply the vertext position normalized. Since we see the edges in your image, that’s most likely not what you’re doing. We also don’t know if and how much of the vertices you have actually shared between the triangles. Keep in mind if you’re planning of texturing the sphere you will need a UV seam somewhere, otherwise you would have one triangle strip having its texture coordinates flipped. So at least at the seam you would need duplicated vertices.

edit

Ok since you have posted your code we can see the problem. You create several completely unconnected triangles and you don’t specify any normals yourself. While it is possible to have all triangles disconnected (so no shared vertices at all), however you can not use RecalculateNormals in that case. Each triangle is on its own. So when the normals for the vertices of that triangle are calculated, they would be perpendicular to that triangle face and you get that flat-shading you’re seeing at the moment. If the vertices would be shared, RecalculateNormals would work “better”. Though at UV seams the automatic calculation would still fail. You need to provide the normals yourself. As I already said the normal for each vertex position is equal to the position, just normalized. Since your positions are already normalized, they are literally the same. That means you don’t even need a seperate “normals” array or List as usual but you can simply use the same array for the normals ^^. So instead of doing this:

    mesh.Clear();
    mesh.vertices = meshVertices;
    mesh.triangles = meshTriangles;
    mesh.RecalculateNormals();

you just do this:

    mesh.Clear();
    mesh.vertices = meshVertices;
    mesh.normals = meshVertices;
    mesh.triangles = meshTriangles;

Make sure you remove the “RecalculateNormals” call.

Of course, if you may desire to scale the sphere in the mesh / vertices itself, you should use a seperate array for the normals (which should be normalized) and the positions (which do not need to be normalized).