Search Unity

  1. Welcome to the Unity Forums! Please take the time to read our Code of Conduct to familiarize yourself with the forum rules and how to post constructively.
  2. Dismiss Notice

Question Generating a hexgrid over an icospheres help

Discussion in 'Scripting' started by axxessdenied, Feb 3, 2021.

  1. axxessdenied

    axxessdenied

    Joined:
    Nov 29, 2016
    Posts:
    33
    I decided to take a crack at procedurally generating a sphere from an icosahedron and trying to create a hexgrid pattern over it.

    I've made decent progress since last night. But, I'm still figuring out how to work with meshes.
    For some reason some of the meshes for the hexgrids I create have the normals flipped around and don't get drawn properly since the camera thinks they are facing the wrong way.

    I've used these two pages as a starting point for the project :
    // http://blog.andreaskahler.com/2009/06/creating-icosphere-mesh-in-code.html
    // https://github.com/joeld42/hexplanet/

    I've attached the code and picture of what's going on.

    Any guidance in the right direction would be highly appreciated! CreatePolygon() is at the bottom which creates the mesh for each hexgrid.

    HexTile.cs
    Code (csharp):
    1.  
    2. using System.Collections.Generic;
    3. using UnityEngine;
    4.  
    5. //credit goes to Joel Davis https://github.com/joeld42/hexplanet/
    6.  
    7. //triangles that make up our pentagons and hexagons
    8. public class HexTri
    9. {
    10.     public HexTri(int A, int B, int C)
    11.     {
    12.         _hexA = A;
    13.         _hexB = B;
    14.         _hexC = C;
    15.         _neighbourAB = null;
    16.         _neighbourBC = null;
    17.         _neighbourCA = null;
    18.     }
    19.  
    20.     private readonly int _hexA;
    21.     private readonly int _hexB;
    22.     private readonly int _hexC;
    23.     private float _angle;
    24.  
    25.     private int _newVert;
    26.    
    27.     //connectivity, links to neighbours
    28.     //separated by edges
    29.     private HexTri _neighbourAB, _neighbourBC, _neighbourCA;
    30.  
    31.     public Vector3 GetCentre(List<HexTile> Tiles)
    32.     {
    33.         var centrePoint = Tiles[_hexA].Position;
    34.         centrePoint += Tiles[_hexB].Position;
    35.         centrePoint += Tiles[_hexC].Position;
    36.         centrePoint /= 3.0f;
    37.         return centrePoint;
    38.     }
    39.  
    40.     public int HexA => _hexA;
    41.     public int HexB
    42.     {
    43.         get => _hexB;
    44.         set => throw new System.NotImplementedException();
    45.     }
    46.  
    47.     public int HexC
    48.     {
    49.         get => _hexC;
    50.         set => throw new System.NotImplementedException();
    51.     }
    52.  
    53.     public HexTri NeighbourAB
    54.     {
    55.         get => _neighbourAB;
    56.         set => _neighbourAB = value;
    57.     }
    58.    
    59.     public HexTri NeighbourBC
    60.     {
    61.         get => _neighbourBC;
    62.         set => _neighbourBC = value;
    63.     }
    64.    
    65.     public HexTri NeighbourCA
    66.     {
    67.         get => _neighbourCA;
    68.         set => _neighbourCA = value;
    69.     }
    70. }
    71.  
    72. //hexTile, stores a list of HexTri it contains
    73. public class HexTile
    74. {
    75.     public HexTile(Vector3 p)
    76.     {
    77.         _position = p;
    78.         _terrain = Type.DESERT;
    79.         _normal = p.normalized;
    80.         _triangles = new List<HexTri>();
    81.     }
    82.  
    83.     public enum Type
    84.     {
    85.         WATER = 0,
    86.         DESERT = 1,
    87.         GRASSLAND= 2,
    88.         FOREST= 3,
    89.         MOUNTAIN = 4
    90.     }
    91.  
    92.     private Type _terrain;
    93.     private Vector3 _position;
    94.     private Vector3 _normal;
    95.     private readonly List<HexTri> _triangles;
    96.    
    97.     public Vector3 Position
    98.     {
    99.         get => _position;
    100.         set => _position = value;
    101.     }
    102.  
    103.     public Vector3 Normal
    104.     {
    105.         get => _normal;
    106.         set => _normal = value;
    107.     }
    108.  
    109.     public List<HexTri> HexTris => _triangles;
    110.  
    111.     public Type Terrain
    112.     {
    113.         get => _terrain;
    114.         set => _terrain = value;
    115.     }
    116. }
    117.  
    Planet.cs
    Code (csharp):
    1.  
    2. using System;
    3. using System.Collections.Generic;
    4. using System.Diagnostics;
    5. using UnityEngine;
    6. using Debug = UnityEngine.Debug;
    7. using Random = UnityEngine.Random;
    8. using Vector3 = UnityEngine.Vector3;
    9.  
    10. //based off work by Andreas Kahler and Joel Davis
    11. // http://blog.andreaskahler.com/2009/06/creating-icosphere-mesh-in-code.html
    12. // https://github.com/joeld42/hexplanet/
    13. //other references
    14. // https://en.wikipedia.org/wiki/Icosahedron
    15. // https://en.wikipedia.org/wiki/Truncated_icosahedron
    16. // https://samos.univ-paris1.fr/archives/WSOM05/papers/WSOM2005-11.pdf
    17. // https://experilous.com/1/blog/post/procedural-planet-generation
    18.  
    19. public class Planet : MonoBehaviour
    20. {
    21.     public Material material;
    22.     public float radius = 20.0f;
    23.     public int resolution = 3;
    24.     public Material tileMaterial;
    25.    
    26.     private GameObject _planetMesh;
    27.     private int _subdivisionLevel;
    28.     private readonly List<HexTile> _tileList = new List<HexTile>();
    29.     private List<HexTri> _triangleList = new List<HexTri>();
    30.  
    31.     private void Start()
    32.     {
    33.         var stopwatch = new Stopwatch();
    34.         stopwatch.Start();
    35.         InitializeIcosahedron();
    36.        
    37.         while (_subdivisionLevel < resolution)
    38.             Subdivide();
    39.        
    40.         GeneratePlanetMesh();
    41.         GenerateHexGridMesh();
    42.         stopwatch.Stop();
    43.         Debug.Log($"Start process took {stopwatch.Elapsed} seconds.");
    44.     }
    45.  
    46.     private void InitializeIcosahedron()
    47.     {
    48.         var t = (1.0f + Mathf.Sqrt (5.0f)) * 0.5f;//golden mean
    49.         _tileList.Add(new HexTile(new Vector3(-1, t, 0)));
    50.         _tileList.Add(new HexTile(new Vector3(1, t, 0)));
    51.         _tileList.Add(new HexTile(new Vector3(-1, -t,0)));
    52.         _tileList.Add(new HexTile(new Vector3(1, -t, 0)));
    53.         _tileList.Add(new HexTile(new Vector3(0, -1, t)));
    54.         _tileList.Add(new HexTile(new Vector3(0, 1, t)));
    55.         _tileList.Add(new HexTile(new Vector3(0, -1, -t)));
    56.         _tileList.Add(new HexTile(new Vector3(0, 1, -t)));
    57.         _tileList.Add(new HexTile(new Vector3(t, 0, -1)));
    58.         _tileList.Add(new HexTile(new Vector3(t, 0, 1)));
    59.         _tileList.Add(new HexTile(new Vector3(-t, 0, -1)));
    60.         _tileList.Add(new HexTile(new Vector3(-t, 0, 1)));
    61.        
    62.         _triangleList.Add(new HexTri(0, 11, 5));
    63.         _triangleList.Add(new HexTri(0, 5, 1));
    64.         _triangleList.Add(new HexTri(0, 1, 7));
    65.         _triangleList.Add(new HexTri(0, 7, 10));
    66.         _triangleList.Add(new HexTri(0, 10, 11));
    67.        
    68.         _triangleList.Add(new HexTri(1, 5, 9));
    69.         _triangleList.Add(new HexTri(5, 11, 4));
    70.         _triangleList.Add(new HexTri(11, 10, 2));
    71.         _triangleList.Add(new HexTri(10, 7, 6));
    72.         _triangleList.Add(new HexTri(7, 1, 8));
    73.        
    74.         _triangleList.Add (new HexTri(3, 9, 4));
    75.         _triangleList.Add (new HexTri(3, 4, 2));
    76.         _triangleList.Add (new HexTri(3, 2, 6));
    77.         _triangleList.Add (new HexTri(3, 6, 8));
    78.         _triangleList.Add (new HexTri(3, 8, 9));
    79.        
    80.         _triangleList.Add (new HexTri(4, 9, 5));
    81.         _triangleList.Add (new HexTri(2, 4, 11));
    82.         _triangleList.Add (new HexTri(6, 2, 10));
    83.         _triangleList.Add (new HexTri(8, 6, 7));
    84.         _triangleList.Add (new HexTri(9, 8, 1));
    85.        
    86.         Debug.Log($"Triangle Count is {_triangleList.Count}");
    87.         Debug.Log($"Tile Count is {_tileList.Count}");
    88.         //assign neighbours
    89.         FindNeighbours();
    90.     }
    91.    
    92.     private int GetMidPointIndex(IDictionary<int, int> Cache, int A, int B)
    93.     {
    94.         //create a key from the original indices
    95.         //store the smaller index in the upper two bytes of an integer
    96.         //this makes sure the function returns the same result
    97.         //at all times since we sort by the smaller index first
    98.         var smallerIndex = Mathf.Min(A, B);
    99.         var largerIndex = Mathf.Max(A, B);
    100.         var key = (smallerIndex << 16) + largerIndex;
    101.        
    102.         //check if the point exists
    103.         if (Cache.TryGetValue(key, out var ret))
    104.             return ret;
    105.  
    106.         //create new midpoint
    107.         var p1 = _tileList[A].Position;
    108.         var p2 = _tileList[B].Position;
    109.        
    110.         var middle = Vector3.Slerp(p1, p2, 0.5f).normalized * radius;
    111.  
    112.         ret = _tileList.Count;
    113.         _tileList.Add(new HexTile(middle));
    114.         Cache.Add(key, ret);
    115.  
    116.         return ret;
    117.     }
    118.  
    119.     private void Subdivide()
    120.     {
    121.         var midPointCache = new Dictionary<int, int>();
    122.  
    123.         var newHexTris = new List<HexTri>();
    124.         foreach (var hexTri in _triangleList)
    125.         {
    126.             var a = hexTri.HexA;
    127.             var b = hexTri.HexB;
    128.             var c = hexTri.HexC;
    129.            
    130.             //get or create midpoints between the original vertices
    131.             var ab = GetMidPointIndex(midPointCache, a, b);
    132.             var bc = GetMidPointIndex(midPointCache, b, c);
    133.             var ca = GetMidPointIndex(midPointCache, c, a);
    134.            
    135.             //original HexTri gets subdivided into 4 smaller HexTri
    136.             newHexTris.Add(new HexTri(a, ab, ca));
    137.             newHexTris.Add(new HexTri(b, bc, ab));
    138.             newHexTris.Add(new HexTri(c, ca, bc));
    139.             newHexTris.Add(new HexTri(ab, bc, ca));
    140.         }
    141.  
    142.         _triangleList = newHexTris;
    143.        
    144.         FindNeighbours();
    145.         ProjectToSphere();
    146.  
    147.         _subdivisionLevel++;
    148.        
    149.         Debug.Log($"Current triangles : {_triangleList.Count}");
    150.         Debug.Log($"Current number of tiles is : {_tileList.Count}");
    151.     }
    152.  
    153.     private void GeneratePlanetMesh()
    154.     {
    155.         //destroy existing object if it exists
    156.         if (_planetMesh)
    157.             Destroy(_planetMesh);
    158.  
    159.         _planetMesh = new GameObject("Planet Mesh");
    160.  
    161.         var surfaceRenderer = _planetMesh.AddComponent<MeshRenderer>();
    162.         surfaceRenderer.material = material;
    163.  
    164.         var terrainMesh = new Mesh();
    165.         var triangleCount = _triangleList.Count;
    166.         var vertexCount = triangleCount * 3;
    167.         var indices = new int[vertexCount];
    168.  
    169.         var vertices = new Vector3[vertexCount];
    170.         var normals = new Vector3[vertexCount];
    171.  
    172.         for (var i = 0; i < triangleCount; i++)
    173.         {      
    174.             var hexTri = _triangleList[i];
    175.  
    176.             indices[i * 3]     = i * 3;
    177.             indices[i * 3 + 1] = i * 3 + 1;
    178.             indices[i * 3 + 2] = i * 3 + 2;
    179.  
    180.             vertices[i * 3]     = _tileList[hexTri.HexA].Position;
    181.             vertices[i * 3 + 1] = _tileList[hexTri.HexB].Position;
    182.             vertices[i * 3 + 2] = _tileList[hexTri.HexC].Position;
    183.  
    184.             normals[i * 3]     = _tileList[hexTri.HexA].Normal;
    185.             normals[i * 3 + 1] = _tileList[hexTri.HexB].Normal;
    186.             normals[i * 3 + 2] = _tileList[hexTri.HexC].Normal;
    187.         }
    188.  
    189.         terrainMesh.vertices = vertices;
    190.         terrainMesh.normals = normals;
    191.         terrainMesh.SetTriangles(indices, 0);
    192.  
    193.         var terrainFilter = _planetMesh.AddComponent<MeshFilter>();
    194.         terrainFilter.mesh = terrainMesh;
    195.     }
    196.  
    197.     private static bool EdgeMatch(int A, int B, int OtherA, int OtherB, int OtherC)
    198.     {
    199.         return A==OtherA && B==OtherB ||
    200.                A==OtherB && B==OtherC ||
    201.                A==OtherC && B==OtherA ||
    202.                B==OtherA && A==OtherB ||
    203.                B==OtherB && A==OtherC ||
    204.                B==OtherC && A==OtherA;
    205.     }
    206.  
    207.     private void FindNeighbours()
    208.     {
    209.         foreach (var hexTile in _tileList)
    210.         {
    211.             hexTile.HexTris.Clear();
    212.         }
    213.        
    214.         foreach (var hexTri in _triangleList)
    215.         {
    216.             foreach (var hexTri2 in _triangleList)
    217.             {
    218.                 if (hexTri == hexTri2) continue;
    219.                 //neighbour A<->B edge
    220.                 if (EdgeMatch(hexTri.HexA, hexTri.HexB, hexTri2.HexA, hexTri2.HexB, hexTri2.HexC))
    221.                     hexTri.NeighbourAB = hexTri2;
    222.  
    223.                 //neighbour B<->C edge
    224.                 if (EdgeMatch(hexTri.HexB, hexTri.HexC, hexTri2.HexA, hexTri2.HexB, hexTri2.HexC))
    225.                     hexTri.NeighbourBC = hexTri2;
    226.                 //neighbour C<->A edge
    227.                 if (EdgeMatch(hexTri.HexC, hexTri.HexA, hexTri2.HexA, hexTri2.HexB, hexTri2.HexC))
    228.                     hexTri.NeighbourCA = hexTri2;
    229.             }
    230.  
    231.             _tileList[hexTri.HexA].HexTris.Add(hexTri);
    232.             _tileList[hexTri.HexB].HexTris.Add(hexTri);
    233.             _tileList[hexTri.HexC].HexTris.Add(hexTri);
    234.         }
    235.     }
    236.  
    237.     private HexTile.Type GetRandomTerrain(float TWater)
    238.     {
    239.         if (Random.value <= TWater) return HexTile.Type.WATER;
    240.         // return a land hex
    241.         var values = (HexTile.Type[])Enum.GetValues(typeof(HexTile.Type));
    242.         var t = (int)Random.value * values.Length + 1;
    243.         t = t > values.Length ? values.Length : t;
    244.         return values[t];
    245.     }
    246.  
    247.     private void ProjectToSphere()
    248.     {
    249.         foreach (var hexTile in _tileList)
    250.         {
    251.             var p = hexTile.Position;
    252.             p = p.normalized * radius;
    253.             hexTile.Position = p;
    254.         }
    255.     }
    256.  
    257.     //returns polygon representation
    258.     private List<Vector3> GetPolygon (HexTile Tile, float Offset = 0.0f)
    259.     {
    260.         //clear list
    261.         var poly = new List<Vector3>();
    262.  
    263.         foreach (var hexTri in Tile.HexTris)
    264.         {
    265.             var p = hexTri.GetCentre(_tileList).normalized;
    266.             p *= radius + Offset;
    267.             poly.Add(p);
    268.         }
    269.        
    270.         //sort the polygons so they are arranged in an orderly fashion
    271.         var edges = poly.Count;
    272.        
    273.         //angles won't be exact
    274.         //set an acceptable range to compensate for distortions
    275.         var maxAngle = 360.0f / edges + 5f;
    276.         var minAngle = 360.0f / edges - 5f;
    277.  
    278.         var sortedPoly = new List<Vector3> {poly[0]};
    279.         poly.RemoveAt(0);
    280.        
    281.         for (var i = 0; i < edges; i++)
    282.         {
    283.             var found = false;
    284.             var pop = new Vector3();
    285.             var smallestPoint = new Vector3();
    286.             var smallestAngle = 180.0f;
    287.            
    288.             //arrange points by angle
    289.             foreach (var p in poly)
    290.             {
    291.                 if (found) continue;
    292.                 //sometimes distortions will lead to angles not being found right away
    293.                 //lets store the smallest angle we find in case all angles don't fall within our set range
    294.                 //seems to work after filtering the angles closest to the acceptable range
    295.                 //TODO might be a better solution to this
    296.                 var angle = Vector3.Angle(Tile.Position - sortedPoly[i], Tile.Position - p);
    297.                 if (angle < smallestAngle)
    298.                 {
    299.                     smallestAngle = angle;
    300.                     smallestPoint = p;
    301.                 }
    302.                
    303.                 if (!(angle <= maxAngle) || !(angle >= minAngle)) continue;
    304.                
    305.                 sortedPoly.Add(p);
    306.                 found = true;
    307.                 pop = p;
    308.             }
    309.             if (!found && sortedPoly.Count < edges)
    310.             {
    311.                 pop = smallestPoint;
    312.                 sortedPoly.Add(pop);
    313.             }
    314.             poly.Remove(pop);
    315.         }
    316.  
    317.         sortedPoly.Reverse();
    318.         return sortedPoly;
    319.     }
    320.  
    321.     //creates a representation of each tile
    322.     private void GenerateHexGridMesh()
    323.     {
    324.         for (var i = 0; i < _tileList.Count; i++)
    325.             CreatePolygon(i);
    326.     }
    327.  
    328.     //creates an individual tile
    329.     //some of the tiles have their normals flipped
    330.     //TODO need to fix this
    331.     private void CreatePolygon(int Tile)
    332.     {
    333.  
    334.         var poly = GetPolygon(_tileList[Tile], 0.25f);
    335.         var edges = poly.Count;
    336.        
    337.         //find centre point:
    338.         var centre = new Vector3();
    339.         switch (edges)
    340.         {
    341.             //if 5 edges
    342.             //find the midpoint between point 2 and 3 (by index)
    343.             //find midpoint between point 0 and new point
    344.             //0<-.->midpoint
    345.             case 5:
    346.                 var midpoint = (poly[2] + poly[3]) / 2;
    347.                 centre = (poly[0] + midpoint) / 2;
    348.                 break;
    349.            
    350.             //if 6 edges
    351.             //find midpoint between opposing corners
    352.             //0<-.->3 1<-.->4 2<-.->5
    353.             case 6:
    354.                 centre = (poly[0] + poly[3]) / 2;
    355.                 break;
    356.         }
    357.        
    358.         var obj = new GameObject($"Polygon Tile {Tile}");
    359.  
    360.         var surfaceRenderer = obj.AddComponent<MeshRenderer>();
    361.         surfaceRenderer.material = tileMaterial;
    362.        
    363.         var terrainMesh = new Mesh();
    364.         var vertexCount = edges * 3;
    365.         var indices = new int[vertexCount];
    366.  
    367.         var vertices = new Vector3[vertexCount];
    368.         var normals = new Vector3[vertexCount];
    369.        
    370.         for (var i = 0; i < edges; i++)
    371.         {
    372.             var a = centre;
    373.             var b = poly[i];
    374.             var c = i != edges-1 ? poly[i + 1] : poly[0];
    375.  
    376.             var nA = a.normalized;
    377.             var nB = b.normalized;
    378.             var nC = c.normalized;
    379.  
    380.             indices[i * 3] = i * 3;
    381.             indices[i * 3 + 1] = i * 3 + 1;
    382.             indices[i * 3 + 2] = i * 3 + 2;
    383.  
    384.             vertices[i * 3] = a;
    385.             vertices[i * 3 + 1] = b;
    386.             vertices[i * 3 + 2] = c;
    387.  
    388.             normals[i * 3] = nA;
    389.             normals[i * 3 + 1] = nB;
    390.             normals[i * 3 + 2] = nC;
    391.         }
    392.  
    393.         terrainMesh.vertices = vertices;
    394.         terrainMesh.normals = normals;
    395.         terrainMesh.SetTriangles(indices, 0);
    396.  
    397.         var terrainFilter = obj.AddComponent<MeshFilter>();
    398.         terrainFilter.mesh = terrainMesh;
    399.     }
    400. }
    401.  
     

    Attached Files:

    Last edited: Feb 3, 2021
  2. Bunny83

    Bunny83

    Joined:
    Oct 18, 2010
    Posts:
    3,524
    So do I get you right, the missing tiles are not on purpose and you actually created a full sphere? If that's the case the code you use probably does not care about the winding order. This is a bit problematic ^^. Of course you could try to incoperate the right winding order in your triangle generation, however a simple fix would be to run a correction pass after you generated your triangles.

    As you may know the direction a triangle faces is determined by the winding order or the triangle. While in OpenGL the front face is usually determined by a counter clockwise winding, Unity (which uses a left handed coordinate system) defines the front face by a clockwise winding order.

    So it matters in which order the 3 indices of a triangle are stored in the triangle array. If you trace the vertices as seen from the camera in the order they are referenced by the triangle indices describe a clockwise winding, it means that triangle would be visible.

    The great thing about a sphere is that all tiles / faces are perpendicular to the normal vector that connects each vertex with the sphere center. So you can determine the winding order with a bit of vector math. So imagine you take a look at a single triangle with the corners A, B and C. Lets call the sphere center "O". To check the winding order of that triangle you just need to construct two edges of that triangle. For example AB and BC (the third edge would be CA). Now calculating the cross product between the two edges give you a vector that is perpendicular to the plane the triangle lies in. However the direction depends on the order of the two vectors. As mentioned Unity uses a left handed system, so the left-hand-rule applies. If you do
    var p = Vector3.Cross(AB, BC)
    the resulting vector would point outwards from the triangle if the winding order is correct.

    Now we can simply calculate the dot product between that vector and any of the vectors that go from the sphere center to any of the 3 vertices. Technically the correct vector would be the vector from the center of the sphere to the center of the triangle, but that's not really necessary as you just want to know if the triangle points in the right or wrong direction. So doing
    float d = Vector3.Dot(p, OA);
    gives us a number that will be positive if the triangle id oriented the right way and negative if it's oriented the wrong way. Once you identified a triangle that need to be flipped, all you have to do is swap two of the 3 indices of that triangle. Which pair you switch is irrelevant. So assuming a triangle has the indices 12, 3, 8 you can swap any two pairs. So valid solutions would be (3,12,8) or (12,8,3) or (8,3,12). So just swap the first two indices of that triangle and it will be oriented the right way.
     
    axxessdenied likes this.
  3. axxessdenied

    axxessdenied

    Joined:
    Nov 29, 2016
    Posts:
    33
    Yeah, I haven't figured out how to handle the winding order yet. That's definitely something I should implement. Your simple fix is along the lines of what I was thinking of trying in the mean time. Thanks for the detailed response :)
     
  4. axxessdenied

    axxessdenied

    Joined:
    Nov 29, 2016
    Posts:
    33
  5. Bunny83

    Bunny83

    Joined:
    Oct 18, 2010
    Posts:
    3,524
    I don't really think that this would be necessary. I guess you just have an error / the wrong order of the indices either in your subdivision routine or your original hex tri generation method. If the base structure is right and the subdivision is done correctly there should be no issues with the winding order.

    I've posted a subdivision implementation over here with some explanation in the comments below. It generally helps to draw an image of your structure so you don't get lost between your indices.
     
    Ardenian and axxessdenied like this.
  6. axxessdenied

    axxessdenied

    Joined:
    Nov 29, 2016
    Posts:
    33
    Thanks a lot for the help @Bunny83
    I've restarted the project using the ecs approach with dots and i've got a working mesh being created already and its running noticeably quicker too :)
     
  7. Touhma_Slater

    Touhma_Slater

    Joined:
    Nov 19, 2014
    Posts:
    20
    I would love to see the code for that if possible !