Search Unity

  1. Unity 2019.2 is now released.
    Dismiss Notice

Terrain Chunk borders don't match

Discussion in 'Scripting' started by fhilprec, Nov 6, 2019.

  1. fhilprec

    fhilprec

    Joined:
    Dec 21, 2018
    Posts:
    11
    I'm trying to generate Terrain using the marching cubes algorithm and some 3D noise. For optimization purposes i'm trying to split the world into chunks. My problem now is that the edges create little holes between the chunks.
    Here is my code

    Code (CSharp):
    1. using System.Collections;
    2. using System.Collections.Generic;
    3. using UnityEngine;
    4.  
    5. public class NoiseTerrain : MonoBehaviour
    6. {
    7.     [Range(1, 30)]
    8.     public int chunkresolution = 11;
    9.     public int chunksize = 1;
    10.     Noise noise = new Noise();
    11.     [Range(0, 1)]
    12.     public float threshhold = 0.6f;
    13.     public Vector3 offset = new Vector3(0, 0, 5);
    14.     TriAngulationTable tri = new TriAngulationTable();
    15.     [Range(0.01f, 10f)]
    16.     public float frequenzy = 2f;
    17.     public float amplitude = 1f;
    18.     int[,,] noisevalues;
    19.     public Material mat;
    20.     List<Vector3> verticeslist = new List<Vector3>();               //only for debugging make class list after finising
    21.     List<int> triangleslist = new List<int>();              //same here
    22.     public float accuracy;
    23.  
    24.     int x = 0;
    25.     int y = 0;
    26.     int z = 0;
    27.  
    28.  
    29.  
    30.     private float density(Vector3 point1)
    31.     {
    32.         return (noise.Evaluate(point1 / (chunkresolution - 1) * frequenzy + gameObject.transform.position/chunksize) + 1 )/2;
    33.     }
    34.  
    35.    
    36.  
    37.     private void triangulate(int[,,] noisevalues)
    38.     {
    39.  
    40.         string binary = "";
    41.  
    42.         binary += noisevalues[x + 1, y, z + 1].ToString();
    43.         binary += noisevalues[x, y, z + 1].ToString(); ;
    44.         binary += noisevalues[x, y, z].ToString();
    45.         binary += noisevalues[x + 1, y, z].ToString();
    46.         binary += noisevalues[x + 1, y + 1, z + 1].ToString();
    47.         binary += noisevalues[x, y + 1, z + 1].ToString();
    48.         binary += noisevalues[x, y + 1, z].ToString();
    49.         binary += noisevalues[x + 1, y + 1, z].ToString();
    50.  
    51.  
    52.         int verticescount = 0;
    53.  
    54.         for (int i = 0; i < 16; i++)          
    55.         {
    56.  
    57.             int index = tri.triTable[System.Convert.ToInt32(binary, 2), i];
    58.             if (index != -1)
    59.             {
    60.  
    61.                 Vector3 point1 = (tri.edgetable[index, 0] + new Vector3(x, y, z));
    62.                 Vector3 point2 = (tri.edgetable[index, 1] + new Vector3(x,y,z));
    63.  
    64.                 float v1 = density(point1);
    65.                 float v2 = density(point2);
    66.                 float t = (threshhold-v1) / (v2-v1);
    67.                 Vector3 newp;        
    68.                 newp = point1 + t * (point2-point1);
    69.                 //newp = (point1 + point2) / 2;
    70.  
    71.                 verticeslist.Add(newp / (chunkresolution-1));
    72.                 verticescount++;
    73.  
    74.                /* GameObject sphere = GameObject.CreatePrimitive(PrimitiveType.Sphere);
    75.                 sphere.transform.position = newp / (chunkresolution - 1) + gameObject.transform.position;
    76.                 sphere.transform.localScale = new Vector3(0.01f, 0.01f, 0.01f);*/
    77.  
    78.  
    79.             }
    80.  
    81.         }
    82.  
    83.  
    84.         for (int i = 0; i < verticescount; i++)
    85.         {
    86.             triangleslist.Add(verticeslist.Count - verticescount + i);
    87.         }
    88.  
    89.  
    90.  
    91.      
    92.     }
    93.  
    94.  
    95.     private void Update()
    96.     {
    97.         int[,,] noisevalues = new int[chunkresolution, chunkresolution, chunkresolution];
    98.         triangleslist.Clear();
    99.         verticeslist.Clear();
    100.         if (gameObject.GetComponent<MeshRenderer>() == null) { gameObject.AddComponent<MeshRenderer>(); }
    101.         if (gameObject.GetComponent<MeshFilter>() == null) { gameObject.AddComponent<MeshFilter>(); }
    102.    
    103.         //filling the 3d array with the noisevalues
    104.         for (int x = 0; x < chunkresolution ; x++)             //by adding the minus the borders are rendered
    105.         {
    106.  
    107.             for (int y = 0; y < chunkresolution; y++)
    108.             {
    109.  
    110.                 for (int z = 0; z < chunkresolution; z++)
    111.                 {
    112.                     float noisevalue = density(new Vector3(x, y, z));
    113.                    
    114.                     if ( noisevalue > threshhold)
    115.                     {
    116.  
    117.  
    118.                         noisevalues[x, y, z] = 1;
    119.  
    120.                        /* GameObject sphere = GameObject.CreatePrimitive(PrimitiveType.Sphere);
    121.                         sphere.transform.position = new Vector3(x, y, z) / (chunkresolution - 1) + gameObject.transform.position;
    122.                         sphere.transform.localScale = new Vector3(0.01f, 0.01f, 0.01f);*/
    123.                    
    124.            
    125.                    
    126.                        
    127.                     }
    128.                     else
    129.                     {
    130.                         noisevalues[x, y, z] = 0;
    131.                     }
    132.  
    133.  
    134.                 }
    135.             }
    136.  
    137.  
    138.         }
    139.  
    140.  
    141.  
    142.  
    143.         for (z = 0; z < chunkresolution - 1; z++)
    144.         {
    145.             for (y = 0; y < chunkresolution - 1; y++)
    146.             {
    147.                 for (x = 0; x < chunkresolution - 1; x++)
    148.                 {
    149.  
    150.                     triangulate(noisevalues);
    151.                 }
    152.             }
    153.         }
    154.  
    155.  
    156.         Mesh mesh = new Mesh();
    157.         mesh.Clear();
    158.         //if mesh gets to
    159.         mesh.indexFormat = UnityEngine.Rendering.IndexFormat.UInt32;
    160.         Vector3[] verticesarray = new Vector3[verticeslist.Count];
    161.         int[] trianglesarray = new int[triangleslist.Count];
    162.  
    163.  
    164.         verticesarray = verticeslist.ToArray();
    165.         trianglesarray = triangleslist.ToArray();
    166.  
    167.  
    168.         mesh.vertices = verticesarray;
    169.         mesh.triangles = trianglesarray;
    170.         mesh.RecalculateNormals();
    171.  
    172.         gameObject.GetComponent<MeshFilter>().mesh = mesh;
    173.         gameObject.transform.localScale = new Vector3(chunksize, chunksize, chunksize);
    174.         gameObject.GetComponent<MeshRenderer>().material = mat;
    175.  
    176.  
    177.  
    178.  
    179.     }
    180.  
    181.  
    182.  
    183. }
    184.  
    185.  
    186.  
     
  2. fhilprec

    fhilprec

    Joined:
    Dec 21, 2018
    Posts:
    11
    Chunks.PNG
    That's what i am talking about
     
  3. Joe-Censored

    Joe-Censored

    Joined:
    Mar 26, 2013
    Posts:
    6,735
  4. fhilprec

    fhilprec

    Joined:
    Dec 21, 2018
    Posts:
    11
  5. Yoreki

    Yoreki

    Joined:
    Apr 10, 2019
    Posts:
    572
    I had a similar problem with my implementation. If you are like me, you basically forgot to create a mesh for the block between the chunks. From a 2D perspective, you are creating a block for coordinates 0 and1, then 1 and 2, then 2 and 3 and so on. If your chunk size is 16, you end at 15 and 16. The next chunk start with 16 and 17 and so on. This means, you never create a mesh for the cube from 16 to 17, which effectively lies between chunks. You probably need to account for that.

    Also, if you are going for utmost performance, take a look at dual contouring instead. It's basically marching cubes on dope. However, it adds quite a bit of complexity, so only do this if you for some reason want/need all the performance benefits you can get. It's by no means necessary for real-time terrain generation. Using DOTS i was able to generate a 32x32x64 chunk (plus basic texturing and normals interpolation) in just under 7 milliseconds with marching cubes, most of it being memory allocation i still did on the mainthread, so i'm pretty sure i could bring this down to 4ms or less if i come back to it.
     
    Last edited: Nov 7, 2019
  6. fhilprec

    fhilprec

    Joined:
    Dec 21, 2018
    Posts:
    11
    Firstival thank you for your response. When i get this to work i will try to maximize performance but it has to work at first :D. I might be completely wrong because i did not fully understand your response, meshes do connect occasionally. If I just draw a flat surface they match perfectly. Only if i change the points position using a noise function the chunks dont add up correctly. Maybe you can see this in the picture. I made sure that all my meshes have exactly the size 1. So i dont know where i should fit another small mesh between two chunks.
     
  7. Yoreki

    Yoreki

    Joined:
    Apr 10, 2019
    Posts:
    572
    Hm yeah, i guess the gap looks too small for what i was describing. I was not sure if that was just a perspective thing.
    You said when you draw a flat surface then it works, but when you introduce noise it creates these small gaps? Is everything else the same? The chunking process, the way you turn your scalar field into the mesh? Because if it is, then there should not be a difference in the behavior. It's highly likely that there are some minor differences.
    Other than that, this looks like a floating point problem, but i highly doubt it is. Unless you are spawning your chunks at random locations already and use a way too huge scale maybe? Eventually floats lose precision, which may result in gaps like these. But then again, since you store your chunks at integer positions (i assume), floating point problems would looks differently and not only occur between chunks.

    To explain my previous post again:
    In marching cubes you 'march an imaginary cube through a field of scalar values'. The first cube at the (0,0,0) chunk would contain the scalar values for the coordinates: (0,0,0), (0,0,1), (1,0,0), (1,0,1), (0,1,0), (0,1,1), (1,1,0), (1,1,1).
    From these coordinates you get the noise value, and then decide on the mesh that belongs there based on some threshold, deciding which of these coordinates are in or outside of the shape based on their value.
    What i meant, and probably described a bit confusingly, is that the first cube is the one between the coordinates 0 and 1 (just looking at one axis basically). With that i meant exactly what i described above, but writing all 8 coordinates would be a pain in the butt hehe. Then the next cube is between coordinates 1 and 2 and so on. Naively, you'd do that until number <= chunkSize, meaning the last cube you look at is between 15 and 16. The next chunk would then start at 16, so its first cube would be between 16 and 17, then 17 and 18 and so on. This means we never look at the mesh that belongs between 16 and 17.
    Unless it's something with the perspective, your gap looks to small to be caused by this, but that was a mistake i make in my implementation when i experimented with marching cubes.
     
  8. fhilprec

    fhilprec

    Joined:
    Dec 21, 2018
    Posts:
    11
    Thank you for your help. Although i haven't been able to fix it, I really appreciate your effort.

    Yes, i think it is the same. In my code there is a part where i assign a 1 to a 3d array if the according noisevalue is above a certain threshhold. When I want to create a flat surface, the only thing i am changing is, that i dont call the noise function but I simply set all coordinates with a y-value of for example 6 to 1.

    The scale of a chunk is exactly 1. I think i have tried different scales but it did not change anything.

    Would you recommend to scrap my code and go for dual contouring instead?
     
  9. fhilprec

    fhilprec

    Joined:
    Dec 21, 2018
    Posts:
    11
    I just found out that, if I dont add a variable frequenzy the problem doesnt occur. But now i cant change the frequenzy anymore unfortunately.
     
  10. fhilprec

    fhilprec

    Joined:
    Dec 21, 2018
    Posts:
    11
    I finally figured it out.

    Code (CSharp):
    1. private float density(Vector3 point1)
    2.     {
    3.         return (noise.Evaluate(point1 / (chunkresolution - 1) * frequenzy + gameObject.transform.position/chunksize) + 1 )/2;
    4.     }
    In my density function i was adding the gameobjects position without applying the frequenzy to it. That was the whole mistake :D. Now i can focus on your tipps.
     
  11. fhilprec

    fhilprec

    Joined:
    Dec 21, 2018
    Posts:
    11
    But i still have performance issues and i am not sure how to change that. I have read about compute shaders and DOTS but i do not know how to implement them.
     
  12. Yoreki

    Yoreki

    Joined:
    Apr 10, 2019
    Posts:
    572
    Basically, you just dont want the workload on the mainthread. That's more or less it. You can decide whether you want it on the GPU (compute shaders) or on the CPU (best choice being DOTS but not required). Compute shaders are probably a bit faster over all, but i prefer DOTS since i can then use the graphics card for visuals. This depends on the game tho. A game that's not visually intensive may want to use compute shaders for the terrain generation, especially if it's already a CPU intensive game.

    About implementing them.. i dont think there is an easy answer. Compute shaders are, well, shaders, thus it's kind of a topic on its own, not to mention a different programming language. While DOTS is still C#, it's data oriented programming, which is pretty different from the object oriented workflow you got used to, so it's more or less also a new "language" to learn. On top of that DOTS is extremely new, so most videos you find on it are outdated, the documentation is... at least somewhat existant, partly i guess. It's a pain to work with, but extremely powerful. You dont need to use all of it tho. Using Jobs (basically safe and easy multithreading) and Burst (free performance boost with jobs) is enough for a huge speedup. Iirc, for me it was about a 70x (that's factor 70, not 70%) speedup overall. Not to mention it being off the main thread. However, implementing it was neither straight forward, nor easy. I learned a lot on the way tho :)