PJacouF
PJacouF

Reputation: 19

Misalignment issues between different LOD chunks in procedurally generated infinite world in Unity

I am working on a personal project to improve myselft and learn more advanced stuff, but I am currently stuck and couln't find any solutions. Basically, I have 2 independent issues which a solution to one of them brings the other issue, which are:

Shifting issue demonstration

The right side of the "line" are higher detailed chunks and the left side are lwer detailed chunks.

Misalignment issue demonstration

This is also very visible in the play mode, regardless of how far the lower detailed chunks are (like empty spaces between chunks in a grid):

Misalignment issue demonstration in game

Below code is the script I add to each chunk when they spawn and it includes the mesh generation and LOD system:

using UnityEngine;

[RequireComponent(typeof(MeshFilter))]
[RequireComponent(typeof(MeshRenderer))]
[RequireComponent(typeof(MeshCollider))]
public class Chunk : MonoBehaviour
{
    private ChunkGenerator chunkGenerator;
    private MeshRenderer meshRenderer;
    private MeshFilter meshFilter;
    private MeshCollider meshCollider;

    private bool isActive = false;
    private TerrainSettings terrainSettings;

    private Mesh chunkMesh;
    private bool isMeshGenerated = false;

    private Mesh LODMesh;
    private bool isLODMeshGenerated = false;

    public Vector2Int ChunkCoord { get; private set; }

    // adds all the necessary components
    public void Initialize(Vector2Int chunkCoord, ChunkGenerator generator)
    {
        ChunkCoord = chunkCoord;
        chunkGenerator = generator;

        terrainSettings = generator.terrainSettings;

        if (!TryGetComponent<MeshFilter>(out meshFilter))
        {
            meshFilter = gameObject.AddComponent<MeshFilter>();
        }

        if (!TryGetComponent<MeshRenderer>(out meshRenderer))
        {
            meshRenderer = gameObject.AddComponent<MeshRenderer>();
        }

        if (!TryGetComponent<MeshCollider>(out meshCollider))
        {
            meshCollider = gameObject.AddComponent<MeshCollider>();
        }

        isMeshGenerated = false;
    }

    private void Update()
    {
        UpdateLODMeshes();
    }

    public void Activate()
    {
        if (!isActive)
        {
            gameObject.SetActive(true);

            UpdateLODMeshes();

            isActive = true;
        }
    }

    // very simple LOD logic
    private void UpdateLODMeshes()
    {
        Vector2Int playerChunk = chunkGenerator.GetPlayerChunk();
        int distanceX = Mathf.Abs(ChunkCoord.x - playerChunk.x);
        int distanceZ = Mathf.Abs(ChunkCoord.y - playerChunk.y);
        int lodDistance = chunkGenerator.LODDistance;

        if (distanceX <= lodDistance && distanceZ <= lodDistance)
        {
            if (!isMeshGenerated)
            {
                chunkMesh = GenerateMesh(1);
                isMeshGenerated = true;
                SetMaterial(terrainSettings.terrainMaterial);
            }
            SetMesh(chunkMesh);
            SetChunkScale(1);
        }
        else
        {
            if (!isLODMeshGenerated)
            {
                LODMesh = GenerateMesh(2);
                isLODMeshGenerated = true;
                SetMaterial(terrainSettings.terrainMaterial);
            }
            SetMesh(LODMesh);
            SetChunkScale(1.0225f);
        }
    }

    public void Deactivate()
    {
        if (isActive)
        {
            gameObject.SetActive(false);
            isActive = false;
        }
    }

    // mesh generation
    public Mesh GenerateMesh(int resolution)
    {
        Mesh mesh = new();
        int chunkSize = chunkGenerator.chunkSize;

        // offset calculation
        Vector2Int offset = new(ChunkCoord.x * (chunkSize - resolution), ChunkCoord.y * (chunkSize - resolution));

        // noise map generation
        float[,] noiseMap = NoiseGenerator.GenerateNoiseMap(
            terrainSettings.noiseScale, terrainSettings.frequency, terrainSettings.octaves, terrainSettings.persistence, terrainSettings.lacunarity,
            terrainSettings.groundLevel, terrainSettings.groundFlatness, terrainSettings.mountainLevel, terrainSettings.mountainFlatness,
            terrainSettings.mountainHeightMultiplier, terrainSettings.generateIslands, terrainSettings.islandFrequency,
            terrainSettings.islandRadiusRandomizer, terrainSettings.islandRadius, terrainSettings.islandHeightMultiplier, chunkSize, chunkSize, offset
        );

        // generating mesh data
        GenerateMeshData(chunkSize, resolution, out Vector3[] vertices, out Vector2[] uvs, out int[] triangles, noiseMap);

        mesh.vertices = vertices;
        mesh.uv = uvs;
        mesh.triangles = triangles;

        mesh.RecalculateBounds();
        mesh.RecalculateNormals();
        meshCollider.sharedMesh = mesh;

        return mesh;
    }

    // mesh data generation
    public void GenerateMeshData(int chunkSize, int resolution, out Vector3[] vertices, out Vector2[] uvs, out int[] triangles, float[,] noiseMap)
    {
        int reducedSize = chunkSize / resolution;
        int numVertices = reducedSize * reducedSize;
        int numTriangles = (reducedSize - 1) * (reducedSize - 1) * 6;

        vertices = new Vector3[numVertices];
        uvs = new Vector2[numVertices];
        triangles = new int[numTriangles];

        int vertexIndex = 0;
        int triangleIndex = 0;

        // vertex generation
        for (int y = 0; y < reducedSize; y++)
        {
            for (int x = 0; x < reducedSize; x++)
            {
                int xPos = x * resolution;
                int zPos = y * resolution;

                float yPos = noiseMap[xPos, zPos] * terrainSettings.heightScale;

                vertices[vertexIndex] = new Vector3(xPos, yPos, zPos);
                uvs[vertexIndex] = new Vector2(x / (float)(reducedSize - 1), y / (float)(reducedSize - 1));

                // triangle generation
                if (x < reducedSize - 1 && y < reducedSize - 1)
                {
                    // Define the indices of the vertices for the triangles
                    int topLeft = vertexIndex;
                    int topRight = vertexIndex + 1;
                    int bottomLeft = vertexIndex + reducedSize;
                    int bottomRight = vertexIndex + reducedSize + 1;

                    triangles[triangleIndex] = topLeft;
                    triangles[triangleIndex + 1] = bottomLeft;
                    triangles[triangleIndex + 2] = topRight;
                    triangles[triangleIndex + 3] = topRight;
                    triangles[triangleIndex + 4] = bottomLeft;
                    triangles[triangleIndex + 5] = bottomRight;

                    triangleIndex += 6;
                }

                vertexIndex++;
            }
        }
    }

    private void SetMesh(Mesh mesh)
    {
        meshFilter.mesh = mesh;
    }

    private void SetMaterial(Material material)
    {
        if (meshRenderer != null)
        {
            meshRenderer.material = material;
        }
    }

    private void SetChunkScale(float scale)
    {
        transform.localScale = new(scale, 1, scale);
    }
}

The issues happen when I play around with the offset calculation.

Vector2Int offset = new(ChunkCoord.x * (chunkSize - resolution), ChunkCoord.y * (chunkSize - resolution));
Vector2Int offset = new(ChunkCoord.x * (chunkSize - 1), ChunkCoord.y * (chunkSize - 1));

Also, the below code is the noise generation script:

using UnityEngine;

public static class NoiseGenerator
{
    private const float Half = 0.5f;
    private const float Zero = 0f;
    private const float One = 1f;

    public static float[,] GenerateNoiseMap(TerrainSettings settings, int mapWidth, int mapHeight, Vector2 offset)
    {
        float[,] noiseMap = new float[mapWidth, mapHeight];
        float invNoiseScale = One / settings.noiseScale;

        float halfWidth = mapWidth * Half;
        float halfHeight = mapHeight * Half;

        for (int y = 0; y < mapHeight; y++)
        {
            for (int x = 0; x < mapWidth; x++)
            {
                float sampleX = (x - halfWidth + offset.x) * invNoiseScale;
                float sampleY = (y - halfHeight + offset.y) * invNoiseScale;

                float amplitude = One;
                float frequency = One;
                float noiseHeight = Zero;

                for (int i = 0; i < settings.octaves; i++)
                {
                    float perlinValue = Mathf.PerlinNoise(sampleX * frequency, sampleY * frequency);
                    noiseHeight += perlinValue * amplitude;

                    amplitude *= settings.persistence;
                    frequency *= settings.lacunarity;
                }

                float groundFlatness = Mathf.Clamp01((settings.groundLevel - noiseHeight) * settings.groundFlatness);
                float groundSmoothTransition = SmoothStep(Zero, One, groundFlatness);

                noiseHeight = Mathf.Lerp(noiseHeight, settings.groundLevel, groundSmoothTransition);

                float mountainFlatness = Mathf.Clamp01((noiseHeight - settings.mountainLevel) * settings.mountainFlatness);
                float mountainSmoothTransition = SmoothStep(Zero, One, mountainFlatness);

                noiseHeight = Mathf.Lerp(noiseHeight, settings.mountainLevel, mountainSmoothTransition);

                if (noiseHeight > settings.mountainLevel)
                {
                    float mountainMultiplier = settings.mountainHeightMultiplier * (noiseHeight - settings.mountainLevel);
                    noiseHeight += mountainMultiplier;
                }

                if (settings.generateIslands)
                {
                    float islandNoise = Mathf.PerlinNoise(sampleX * settings.islandFrequency, sampleY * settings.islandFrequency);
                    float islandMultiplier = Mathf.Clamp01(islandNoise - settings.islandRadiusRandomizer) * settings.islandRadius;

                    noiseHeight += islandMultiplier * settings.islandHeightMultiplier;
                }

                noiseMap[x, y] = noiseHeight;
            }
        }

        return noiseMap;
    }

    private static float SmoothStep(float edge0, float edge1, float x)
    {
        x = Mathf.Clamp01((x - edge0) / (edge1 - edge0));
        return x * x * (3 - 2 * x);
    }
}

I know my codes are a bit messy and not very optimized but I'm really stuck here and I can't continue to work on this before I solve this issue. I know the "solutions" are not really solutions, since they bring another issue. I played around with the offset calculation and tried too many things to the equation but I know this can't be solved by guessing and hoping to find the correct eqation. I know there is a crucial mistake but I can't find it and I need help. Thanks in advance for all the help.

Upvotes: 0

Views: 61

Answers (1)

PJacouF
PJacouF

Reputation: 19

Shortly after I posted my question, I've found a solution. I was certain that the issue was either in the offset calculation or in the noise generation. Thankfully, after I came across with this another related post here, I became smart enough to look for something else.

Before I even implemented the LOD system, there was an "empty space between chunks" issue in my chunk generator, so I prematurely solved it with following chunk position calculation (by subtracting 1 from chunksize):

float posX = x * (chunkSize - 1);
float posZ = z * (chunkSize - 1);

Since I was only working with 1 level of detail, it made sense to "solve" it this way. The solution is to remove the "- 1" from the calculation and replace this line in "Chunk" script:

...
Mesh mesh = new();
int chunkSize = chunkGenerator.chunkSize;

Vector2Int offset = new(ChunkCoord.x * (chunkSize - 1), ChunkCoord.y * (chunkSize - 1));
...

With this:

...
Mesh mesh = new();
int chunkSize = chunkGenerator.chunkSize + resolution;

Vector2Int offset = new(ChunkCoord.x * (chunkSize - resolution), ChunkCoord.y * (chunkSize - resolution));
...

Subtracting the "resolution" with "chunkSize" in offset calculation is the correct way, but we also add "resolution" to chunkSize itself to prevent the all issues listed in the question.

Upvotes: 0

Related Questions