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

[SOLVED] Grid of PrimitiveType.Plane objects has "seams"

Discussion in 'Scripting' started by uthon, Sep 4, 2020.

  1. uthon

    uthon

    Joined:
    Nov 23, 2015
    Posts:
    14
    Edit: Solved, for the updated sources see https://forum.unity.com/threads/sol...-plane-objects-has-seams.964355/#post-6279161

    I have created a tiled terrain map using Perlin noise that places PrimitiveType.Plane objects in a x,z grid, adjusting the Y value of each vertex in the planes for the height.

    I have found seams between the plane objects, and I don't know if it is geometry or lighting related.



    I've googled quite a bit on the topic and I can't seem to narrow down why these seams are appearing, and how to get rid of them.

    In Terrain::Stitch() I search for points on the planes that are "near", and if the points have different y values. The stitching never occurs, unless I omit the y test, which tells me that the geometry on the points is already the same so the stitching is irrelevant, which makes me wonder if it is some sort of lighting problem.

    My code is divided into two classes, TerrainManager and Terrain. The TerrainManager class is responsible for organizing individual Terrain objects into a grid, while the Terrain class is responsible for instantiating the PrimitiveType.Plane object, positioning it, and adusting the heights of the vertices.

    TerrainManager.cs:

    Code (CSharp):
    1. using System.Collections;
    2. using System.Collections.Generic;
    3. using UnityEngine;
    4.  
    5. public class TerrainManager : MonoBehaviour
    6. {
    7.     private const int X_TILE_COUNT = 10;
    8.     private const int Z_TILE_COUNT = 10;
    9.  
    10.     float TileSizeX = 10.0f;
    11.     float TileSizeZ = 10.0f;
    12.  
    13.     Camera MainCamera;
    14.  
    15.     Dictionary<string, Terrain> Tiles = new Dictionary<string, Terrain>();
    16.  
    17.     void Start()
    18.     {
    19.         MainCamera = Camera.main;
    20.         if(null == MainCamera)
    21.         {
    22.             Debug.Log("MainCamera is null (TerrainManager.Start)");
    23.         }
    24.         UpdateTerrain();
    25.    }
    26.  
    27.     void Update()
    28.     {
    29.     }
    30.  
    31.     void UpdateTerrain()
    32.     {
    33.         // Snap the camera position to the center of the tile grid.
    34.         float cameraX = (float)(Mathf.FloorToInt(MainCamera.transform.position.x) / (int)TileSizeX * (int)TileSizeX);
    35.         float cameraZ = (float)(Mathf.FloorToInt(MainCamera.transform.position.z) / (int)TileSizeZ * (int)TileSizeZ);
    36.  
    37.         // Get the starting position, framing a box around the snapped camera position.
    38.         float startX = cameraX - ((X_TILE_COUNT/2.0f) * TileSizeX);
    39.         float startY = 0.0f;
    40.         float startZ = cameraZ - ((Z_TILE_COUNT/2.0f) * TileSizeZ);
    41.  
    42.         // Generate new terrain
    43.         Terrain lastTerrainTile = null;
    44.         List<string> activeTiles = new List<string>();
    45.         for(int z = 0; z < Z_TILE_COUNT; z++)
    46.         {
    47.             lastTerrainTile = null;
    48.  
    49.             for(int x = 0; x < X_TILE_COUNT; x++)
    50.             {
    51.                 float positionX = startX + ((float)x * TileSizeX);
    52.                 float positionY = startY;
    53.                 float positionZ = startZ + ((float)z * TileSizeZ);
    54.  
    55.                 Terrain terrainTile = GetTerrainTile(positionX, positionY, positionZ);
    56.                 if(null == terrainTile)
    57.                 {
    58.                     terrainTile = CreateTerrainTile(positionX, positionY, positionZ);
    59.  
    60.                     if(lastTerrainTile != null)
    61.                     {
    62.                         lastTerrainTile.Stitch(terrainTile);
    63.                     }
    64.                 }
    65.                 else
    66.                 {
    67.                     string index = GetTileIndex(positionX, positionY, positionZ);
    68.                     activeTiles.Add(index);
    69.                 }
    70.  
    71.                 lastTerrainTile = terrainTile;
    72.             }
    73.         }
    74.  
    75.         // Clear old terrain.
    76.         List<string> deadTiles = new List<string>();
    77.         foreach(KeyValuePair<string, Terrain> entry in Tiles)
    78.         {
    79.             // Search for tiles that do not have an entry in the activeTiles dictionary.
    80.             if(-1 == activeTiles.IndexOf(entry.Key))
    81.             {
    82.                 deadTiles.Add(entry.Key);
    83.                 //entry.Value;
    84.             }
    85.         }
    86.  
    87.         foreach(string entry in deadTiles)
    88.         {
    89.             Tiles.Remove(entry);
    90.         }
    91.     }
    92.  
    93.     string GetTileIndex(float x, float y, float z)
    94.     {
    95.         string tileIndex = (Mathf.FloorToInt(x)).ToString() + "," + (Mathf.FloorToInt(y)).ToString() + "," + (Mathf.FloorToInt(z)).ToString();
    96.         return tileIndex;
    97.     }
    98.  
    99.     Terrain GetTerrainTile(float x, float y, float z)
    100.     {
    101.         Terrain terrain = null;
    102.  
    103.         string tileIndex = GetTileIndex(x, y, z);
    104.         if(Tiles.ContainsKey(tileIndex))
    105.         {
    106.             terrain = Tiles[tileIndex];
    107.         }
    108.  
    109.         return terrain;
    110.     }
    111.  
    112.     Terrain CreateTerrainTile(float x, float y, float z)
    113.     {
    114.         Terrain terrain = new Terrain();
    115.         terrain.Create(x, y, z);
    116.         Tiles.Add(GetTileIndex(x, y, z), terrain);
    117.         return terrain;
    118.     }
    119. }
    120.  
    Terrain.cs:
    Code (CSharp):
    1. using System.Collections;
    2. using System.Collections.Generic;
    3. using UnityEngine;
    4.  
    5. public class Terrain
    6. {
    7.     private GameObject mPlane;
    8.     public GameObject plane
    9.     {
    10.         get { return mPlane; }
    11.         set { mPlane = value; }
    12.     }
    13.  
    14.     private int mHeightScale = 5;
    15.  
    16.     private float mDetailScale = 5.0f;
    17.  
    18.     public Mesh mesh
    19.     {
    20.         get { return ((MeshFilter)mPlane.GetComponent("MeshFilter")).mesh; }
    21.     }
    22.  
    23.     public void Create(float x, float y, float z)
    24.     {
    25.         mPlane = GameObject.CreatePrimitive(PrimitiveType.Plane);
    26.         mPlane.transform.position = new Vector3(x, y, z);
    27.  
    28.         Vector3[] vertices = mesh.vertices;
    29.  
    30.         for(int v = 0; v < vertices.Length; v++)
    31.         {
    32.             float height = GetHeight(plane.transform.position.x + vertices[v].x, plane.transform.position.z + vertices[v].z);
    33.             vertices[v].y = height;
    34.         }
    35.  
    36.         Vector2[] uvs = new Vector2[vertices.Length];
    37.         for (int i = 0; i < uvs.Length; i++)
    38.         {
    39.             uvs[i] = new Vector2(vertices[i].x, vertices[i].z);
    40.         }
    41.         mesh.uv2 = uvs;
    42.      
    43.         mesh.vertices = vertices;
    44.         mesh.RecalculateBounds();
    45.         mesh.RecalculateNormals();
    46.  
    47.         // Renderer rend = mPlane.GetComponent<Renderer>();
    48.         // rend.material.mainTexture = Resources.Load("Plywood_Albedo") as Texture;
    49.         // rend.material.mainTexture.wrapMode = TextureWrapMode.Clamp;
    50.     }
    51.  
    52.     public void Stitch(Terrain nextTerrain)
    53.     {
    54.         Mesh currentMesh = ((MeshFilter)mPlane.GetComponent("MeshFilter")).mesh;
    55.         Vector3[] currentVertices = currentMesh.vertices;
    56.  
    57.         Mesh nextMesh = ((MeshFilter)nextTerrain.plane.GetComponent("MeshFilter")).mesh;
    58.         Vector3[] nextVertices = nextMesh.vertices;
    59.  
    60.         int stitchCount = 0;
    61.  
    62.         for(int currentVertexIndex = 0; currentVertexIndex < currentVertices.Length; currentVertexIndex++)
    63.         {
    64.             Vector3 currentVertex = currentVertices[currentVertexIndex];
    65.             currentVertex += mPlane.transform.position;
    66.  
    67.             for(int nextVertexIndex = 0; nextVertexIndex < nextVertices.Length; nextVertexIndex++)
    68.             {
    69.                 Vector3 nextVertex = nextVertices[nextVertexIndex];
    70.                 nextVertex += nextTerrain.plane.transform.position;
    71.  
    72.                 // If the two vertices are close, and the Y values differ, stitch them.
    73.                 if(IsNear(currentVertex, nextVertex) && currentVertex.y != nextVertex.y)
    74.                 {
    75.                     stitchCount++;
    76.                     currentVertices[currentVertexIndex].y = nextVertices[nextVertexIndex].y;
    77.                 }
    78.             }
    79.         }
    80.  
    81.         if(stitchCount > 0)
    82.         {
    83.             Debug.Log("Stitched terrains");
    84.             currentMesh.vertices = currentVertices;
    85.             currentMesh.RecalculateBounds();
    86.             currentMesh.RecalculateNormals();
    87.         }
    88.     }
    89.  
    90.     bool IsNear(Vector3 a, Vector3 b)
    91.     {
    92.         float tolerance = 0.1f;
    93.         Vector3 c = b - a;
    94.  
    95.         if(Mathf.Abs(c.x) <= tolerance && Mathf.Abs(c.y) <= tolerance && Mathf.Abs(c.z) <= tolerance)
    96.         {
    97.             return true;
    98.         }
    99.  
    100.         return false;
    101.     }
    102.  
    103.     public float GetHeight(float x, float z)
    104.     {
    105.         float height = Mathf.PerlinNoise(x/mDetailScale, z/mDetailScale) * mHeightScale;
    106.         return height;
    107.     }
    108. }
    109.  
    Any help is appreciated.
     
    Last edited: Sep 4, 2020
  2. neoshaman

    neoshaman

    Joined:
    Feb 11, 2011
    Posts:
    6,469
    - Verify if the normal of each point match,
    - probably should use the average of both normal on both points
    - alternative, generate the normal from the noise data directly
     
  3. Zer0Cool

    Zer0Cool

    Joined:
    Oct 24, 2014
    Posts:
    203
    Last edited: Sep 4, 2020
  4. uthon

    uthon

    Joined:
    Nov 23, 2015
    Posts:
    14
    @neoshaman Thank you that was it, it now works perfectly.



    Here are the updated classes:

    TerrainManager.cs:
    Code (CSharp):
    1. using System.Collections;
    2. using System.Collections.Generic;
    3. using UnityEngine;
    4.  
    5. public class TerrainManager : MonoBehaviour
    6. {
    7.     private const int X_TILE_COUNT = 10;
    8.     private const int Z_TILE_COUNT = 10;
    9.  
    10.     float TileSizeX = 10.0f;
    11.     float TileSizeZ = 10.0f;
    12.  
    13.     Camera MainCamera;
    14.  
    15.     Dictionary<string, Terrain> Tiles = new Dictionary<string, Terrain>();
    16.  
    17.     void Start()
    18.     {
    19.         MainCamera = Camera.main;
    20.         if(null == MainCamera)
    21.         {
    22.             Debug.Log("MainCamera is null (TerrainManager.Start)");
    23.         }
    24.         UpdateTerrain();
    25.    }
    26.  
    27.     void Update()
    28.     {
    29.     }
    30.  
    31.     void UpdateTerrain()
    32.     {
    33.         // Snap the camera position to the center of the tile grid.
    34.         float cameraX = (float)(Mathf.FloorToInt(MainCamera.transform.position.x) / (int)TileSizeX * (int)TileSizeX);
    35.         float cameraY = 0.0f;
    36.         float cameraZ = (float)(Mathf.FloorToInt(MainCamera.transform.position.z) / (int)TileSizeZ * (int)TileSizeZ);
    37.  
    38.         // Offset the camera position to the top,left of the terrain location. This centers the
    39.         // terrain generation around the camera position.
    40.         cameraX -= (X_TILE_COUNT/2.0f) * TileSizeX;
    41.         cameraZ -= (Z_TILE_COUNT/2.0f) * TileSizeZ;
    42.  
    43.         // Generate new terrain
    44.         List<string> activeTiles = new List<string>();
    45.         for(int z = 0; z < Z_TILE_COUNT; z++)
    46.         {
    47.             for(int x = 0; x < X_TILE_COUNT; x++)
    48.             {
    49.                 float positionX = cameraX + ((float)x * TileSizeX);
    50.                 float positionY = cameraY;
    51.                 float positionZ = cameraZ + ((float)z * TileSizeZ);
    52.  
    53.                 Terrain terrainTile = GetTerrainTile(positionX, positionY, positionZ);
    54.                 if(null == terrainTile)
    55.                 {
    56.                     terrainTile = CreateTerrainTile(positionX, positionY, positionZ);
    57.                 }
    58.  
    59.                 string index = GetTileIndex(positionX, positionY, positionZ);
    60.                 activeTiles.Add(index);
    61.             }
    62.         }
    63.  
    64.         // Clear old terrain.
    65.         List<string> deadTiles = new List<string>();
    66.         foreach(KeyValuePair<string, Terrain> entry in Tiles)
    67.         {
    68.             // Search for tiles that do not have an entry in the activeTiles dictionary.
    69.             if(-1 == activeTiles.IndexOf(entry.Key))
    70.             {
    71.                 deadTiles.Add(entry.Key);
    72.                 //entry.Value;
    73.             }
    74.         }
    75.  
    76.         foreach(string entry in deadTiles)
    77.         {
    78.             Tiles.Remove(entry);
    79.         }
    80.  
    81.         // Stitch together tiles.
    82.         for(int z = 0; z < Z_TILE_COUNT; z++)
    83.         {
    84.             for(int x = 0; x < X_TILE_COUNT; x++)
    85.             {
    86.                 // Quad indices:
    87.                 // 2 3
    88.                 // 0 1
    89.                 //
    90.                 // Quad 0 = current terrain position.
    91.                 Terrain[] terrainQuad = new Terrain[4];
    92.  
    93.                 // Current (lower left quad, element 0)
    94.                 float positionX = cameraX + ((float)x * TileSizeX);
    95.                 float positionY = cameraY;
    96.                 float positionZ = cameraZ + ((float)z * TileSizeZ);
    97.                 terrainQuad[0] = GetTerrainTile(positionX, positionY, positionZ);
    98.              
    99.                 // Right (lower right quad, element 1)
    100.                 positionX = cameraX + ((float)(x + 1) * TileSizeX);
    101.                 positionY = cameraY;
    102.                 positionZ = cameraZ + ((float)z * TileSizeZ);
    103.                 terrainQuad[1] = GetTerrainTile(positionX, positionY, positionZ);
    104.  
    105.                 // Top (upper left quad, element 2)
    106.                 positionX = cameraX  + ((float)x * TileSizeX);
    107.                 positionY = cameraY;
    108.                 positionZ = cameraZ + ((float)(z + 1) * TileSizeZ);
    109.                 terrainQuad[2] = GetTerrainTile(positionX, positionY, positionZ);
    110.  
    111.                 // Top Right (upper right quad, element 3)
    112.                 positionX = cameraX + ((float)(x + 1) * TileSizeX);
    113.                 positionY = cameraY;
    114.                 positionZ = cameraZ + ((float)(z + 1) * TileSizeZ);
    115.                 terrainQuad[3] = GetTerrainTile(positionX, positionY, positionZ);
    116.  
    117.                 for(int terrainQuadIndex = 1; terrainQuadIndex < 3; terrainQuadIndex++)
    118.                 {
    119.                     if(terrainQuad[terrainQuadIndex] != null)
    120.                     {
    121.                         terrainQuad[0].Stitch(terrainQuad[terrainQuadIndex]);
    122.                     }
    123.                 }
    124.             }
    125.         }
    126.     }
    127.  
    128.     string GetTileIndex(float x, float y, float z)
    129.     {
    130.         string tileIndex = (Mathf.FloorToInt(x)).ToString() + "," + (Mathf.FloorToInt(y)).ToString() + "," + (Mathf.FloorToInt(z)).ToString();
    131.         return tileIndex;
    132.     }
    133.  
    134.     Terrain GetTerrainTile(float x, float y, float z)
    135.     {
    136.         Terrain terrain = null;
    137.  
    138.         string tileIndex = GetTileIndex(x, y, z);
    139.         if(Tiles.ContainsKey(tileIndex))
    140.         {
    141.             terrain = Tiles[tileIndex];
    142.         }
    143.  
    144.         return terrain;
    145.     }
    146.  
    147.     Terrain CreateTerrainTile(float x, float y, float z)
    148.     {
    149.         Terrain terrain = new Terrain();
    150.         terrain.Create(x, y, z);
    151.         Tiles.Add(GetTileIndex(x, y, z), terrain);
    152.         return terrain;
    153.     }
    154. }
    155.  
    Terrain.cs:
    Code (CSharp):
    1. using System.Collections;
    2. using System.Collections.Generic;
    3. using UnityEngine;
    4.  
    5. public class Terrain
    6. {
    7.     private GameObject mPlane;
    8.     public GameObject plane
    9.     {
    10.         get { return mPlane; }
    11.         set { mPlane = value; }
    12.     }
    13.  
    14.     private int mHeightScale = 5;
    15.  
    16.     private float mDetailScale = 5.0f;
    17.  
    18.     public Mesh mesh
    19.     {
    20.         get { return ((MeshFilter)mPlane.GetComponent("MeshFilter")).mesh; }
    21.     }
    22.  
    23.     public void Create(float x, float y, float z)
    24.     {
    25.         mPlane = GameObject.CreatePrimitive(PrimitiveType.Plane);
    26.         mPlane.transform.position = new Vector3(x, y, z);
    27.  
    28.         Vector3[] vertices = mesh.vertices;
    29.  
    30.         for(int v = 0; v < vertices.Length; v++)
    31.         {
    32.             float height = GetHeight(plane.transform.position.x + vertices[v].x, plane.transform.position.z + vertices[v].z);
    33.             vertices[v].y = height;
    34.         }
    35.  
    36.         Vector2[] uvs = new Vector2[vertices.Length];
    37.         for (int i = 0; i < uvs.Length; i++)
    38.         {
    39.             uvs[i] = new Vector2(vertices[i].x, vertices[i].z);
    40.         }
    41.         mesh.uv2 = uvs;
    42.      
    43.         mesh.vertices = vertices;
    44.         mesh.RecalculateBounds();
    45.         mesh.RecalculateNormals();
    46.     }
    47.  
    48.     public void Stitch(Terrain nextTerrain)
    49.     {
    50.         Mesh currentMesh = ((MeshFilter)mPlane.GetComponent("MeshFilter")).mesh;
    51.         Vector3[] currentVertices = currentMesh.vertices;
    52.         Vector3[] currentNormals = currentMesh.normals;
    53.  
    54.         Mesh nextMesh = ((MeshFilter)nextTerrain.plane.GetComponent("MeshFilter")).mesh;
    55.         Vector3[] nextVertices = nextMesh.vertices;
    56.         Vector3[] nextNormals = nextMesh.normals;
    57.  
    58.         int stitchCount = 0;
    59.  
    60.         for(int currentVertexIndex = 0; currentVertexIndex < currentVertices.Length; currentVertexIndex++)
    61.         {
    62.             Vector3 currentVertex = currentVertices[currentVertexIndex];
    63.             currentVertex += mPlane.transform.position;
    64.  
    65.             for(int nextVertexIndex = 0; nextVertexIndex < nextVertices.Length; nextVertexIndex++)
    66.             {
    67.                 Vector3 nextVertex = nextVertices[nextVertexIndex];
    68.                 nextVertex += nextTerrain.plane.transform.position;
    69.  
    70.                 // If the two vertices are close, average the normals.
    71.                 if(IsNear(currentVertex, nextVertex))
    72.                 {
    73.                     Vector3 average = currentNormals[currentVertexIndex] + nextNormals[nextVertexIndex];
    74.                     average /= 2.0f;
    75.  
    76.                     currentNormals[currentVertexIndex] = average;
    77.                     nextNormals[nextVertexIndex] = average;
    78.  
    79.                     stitchCount++;
    80.                 }
    81.             }
    82.         }
    83.  
    84.         if(stitchCount > 0)
    85.         {
    86.             currentMesh.vertices = currentVertices;
    87.             currentMesh.normals = currentNormals;
    88.             currentMesh.RecalculateBounds();
    89.          
    90.             nextMesh.vertices = nextVertices;
    91.             nextMesh.normals = nextNormals;
    92.             nextMesh.RecalculateBounds();
    93.         }
    94.     }
    95.  
    96.     bool IsNear(Vector3 a, Vector3 b)
    97.     {
    98.         float tolerance = 0.1f;
    99.         Vector3 c = b - a;
    100.  
    101.         if(Mathf.Abs(c.x) <= tolerance && Mathf.Abs(c.y) <= tolerance && Mathf.Abs(c.z) <= tolerance)
    102.         {
    103.             return true;
    104.         }
    105.  
    106.         return false;
    107.     }
    108.  
    109.     public float GetHeight(float x, float z)
    110.     {
    111.         float height = Mathf.PerlinNoise(x/mDetailScale, z/mDetailScale) * mHeightScale;
    112.         return height;
    113.     }
    114. }
    115.