0
\$\begingroup\$

I'm developping a procedural terrain generator in Unity 2020.3, using Sebastian Lague's tutorials and knowledge i've gathered through college projects. The heightmap looks just fine, and i had no issues in 2D, but upon generating the Meshes, i'm getting these weird stairs-like shapes: https://i.imgur.com/Mfg3fM5.png

Debugging that kind of issue has proven quite hard and i spent a few hours not understanding what's wrong already. Displaying the values of my heightmap does not show any issue, values are what i would expect from perlin noise.

I need help understanding what's wrong.

For reference, the noise generation function:

public static float[,] GenerateNoise(int width, int height, int octaves, float persistance, float lacunarity, float scale, Vector2 offset, float redistribution, int seed, 
                                        bool islandMode, float waterCoefficient, float warping1, float warping2
    ) {
        float[,] results = new float[width, height]; 
        float begin = Time.realtimeSinceStartup;

        //seed to have the possibility to recreate a noisemap
        System.Random randomGenerator = new System.Random(seed);
        //random offset
        Vector2[] octaveOffsets = new Vector2[octaves];
        for(int i = 0; i < octaves; i++)
        {
            float offsetX = randomGenerator.Next(-100000, 100000) + offset.x;
            float offsetY = randomGenerator.Next(-100000, 100000) + offset.y;
            octaveOffsets[i] = new Vector2(offsetX, offsetY);
        }

        for (float y = 0; y < width; y++) {
            for (float x = 0; x < height; x++) {
                // Initial values
                float amplitude = 1;
                float frequency = 1;
                float noiseValue = 0;

                // the FBM algorithm to generate layers
                for (int i = 0; i < octaves; i++) {
                    float xCoord = x / width * scale * frequency + octaveOffsets[i].x;
                    float yCoord = y / height * scale * frequency + octaveOffsets[i].y;

                    //warping reusing code from https://www.iquilezles.org/www/articles/warp/warp.htm
                    //add 1st level of warping
                    Vector2 q = new Vector2(Mathf.PerlinNoise(xCoord, yCoord), 
                                            Mathf.PerlinNoise(xCoord + 5.2f, yCoord + 1.3f));

                    //second level of warping
                    Vector2 r = new Vector2(Mathf.PerlinNoise(xCoord + warping1 * q.x + 1.7f, yCoord + warping1 * q.y + 9.2f), 
                                            Mathf.PerlinNoise(xCoord + warping1 * q.x + 5.2f, yCoord + warping1 * q.y + 1.3f));

                    noiseValue += Mathf.PerlinNoise(xCoord + warping2 * r.x, yCoord + warping2 * r.y) * amplitude;

                    frequency *= lacunarity;
                    amplitude *= persistance;
                }

                //redistribution to allow creation of flat areas if need be
                float finalValue = Mathf.Pow(noiseValue, redistribution);
                //create an island shape by lowering noise value the further you are from the middle of the map
                if (islandMode) 
                    finalValue = finalValue - Vector2.Distance(new Vector2(x, y), new Vector2(width / 2, height / 2)) / (width * waterCoefficient);
                results[(int) x, (int) y] = finalValue;
            }
        }

        Debug.Log("time to generate heightmap: " + (Time.realtimeSinceStartup - begin));
        return results;
    }

And the Mesh generation function:

public static Mesh GenerateMesh(float[,] noisemap, float minHeight, float maxHeight, int width, int height, float pointsPerUnit) {
        float begin = Time.realtimeSinceStartup;
        Mesh mesh = new Mesh();
        mesh.indexFormat = UnityEngine.Rendering.IndexFormat.UInt32;
        int nx = noisemap.GetLength(1);
        int ny = noisemap.GetLength(0);
       
        Vector3[] vertices = new Vector3[nx * ny];
        //calculate vertices
        for (int y = 0; y < ny; y++)
        {
            //only write the point if he is not at the end of a line (as it would not have a point to its right)
            for (int x = 0; x < nx; x++)
            {
                vertices[y * nx + x] = new Vector3((float) x / pointsPerUnit, noisemap[x, y] * maxHeight, (float) y / pointsPerUnit);
            }
        }

        Vector2[] uvs = new Vector2[vertices.Length];
        for (int i = 0; i < uvs.Length; i++)
        {
            uvs[i] = new Vector2(vertices[i].x / width, vertices[i].z / height);
        }

        List<int> triangles = new List<int>();
        //skips the first line as it has to points above 
        for (int y = 1; y < ny; y++)
        {
            //only write the point if he is not at the end of a line (as it would not have a point to its right)
            for (int x = 0; x < nx - 1; x++)
            {
                int current = y * nx + x; // the "number" of the current point in a "1D" fashion
                //write current, diagonal, above and current, right, diagonal
                int above = current - ny; //the point above the current
                int rDiagonal = above + 1; //the point in the right diagonal of the current
                int right = current + 1;//the point to the right of the current

                triangles.Add(current);
                triangles.Add(rDiagonal);
                triangles.Add(above);

                triangles.Add(current);
                triangles.Add(right);
                triangles.Add(rDiagonal);
            }
        }

        mesh.SetVertices(vertices);
        mesh.triangles = triangles.ToArray();
        mesh.SetUVs(0, uvs);
        mesh.RecalculateNormals();
        Debug.Log("time to generate mesh: " + (Time.realtimeSinceStartup - begin));
        return mesh;
    }
```
\$\endgroup\$
1
  • 3
    \$\begingroup\$ My approach to debugging this would be to create a very tiny mesh (the smallest one where the artifact appears). That should be an amount of data which you can look at manually and compare the actual data to the expected data. I would also try to replace the noise function with a simple linear gradient and see if the artifacts still occur. That way I could exclude that it's a problem with the noise pattern. \$\endgroup\$
    – Philipp
    Apr 19, 2021 at 12:57

1 Answer 1

1
\$\begingroup\$

It looks like your offset range is relatively large in relation to the float number format. Floats have about 23 bits for actual number data, the rest being for the exponent and sign. At extreme offset values around 100000, you'll only have about 4-5 bits worth of precision left for the fractional part between integer values. This can very easily cause stepping issues like this, and it also makes sense that it is occurring in only one direction -- only one of your randomized offsets happened to be a big number.

I do like that you have an offset step, though. This is instrumental in reducing origin-biased patterns where the same parts of the noise add together at different frequencies, especially when you can't use different seeds, and a lot of people don't account for it.

To mitigate the problem, you could reduce the offset range by a few orders of magnitude. This should correct it in your implementation as it is, but it isn't the solution I would go with. It would be better to replace the noise function entirely, for two reasons:

  1. Mathf.PerlinNoise isn't a very good noise function. Being the unmitigated Perlin algorithm of its namesake, it creates significant feature direction bias along 45 and 90 degree angles relative to the coordinate space. This means you'll only get mountains and coastlines that follow these directions -- something that's ideally supposed be unbiased. It's also 2D-only, meaning it isn't possible to do a domain-rotation trick to produce good 2D results from 3D. Take it with a grain of salt when tutorials recommend you to use this function, because this compromise is entirely unnecessary in most applications. It's better to import a library that support a good Simplex-type noise implementation, or a 3D Perlin which you can domain-rotate if you ensure to apply the rotation. It's Unity's fault for including this function in isolation - thereby influencing people to use it, and tutorial writers' faults for not understanding and conveying the issue, but it's also easy to solve for yourself and import a library.

  2. You can support much larger coordinates and bigger maps, without worrying about issues like this, if you pick a noise library that supports doubles instead of floats.

  3. (EDIT) Most libraries also support seeding noise the "right" way, rather than randomizing offsets in an unseedable function.

Perlin, 3D, domain rotation comparison 2D Simplex-type

I would take FastNoiseLite (drop the FastNoiseLite.cs file into your project) and do one of the following.

  • For 2D Simplex-type noise, setNoiseType(FastNoiseLite.NoiseType.OpenSimplex2) (default), and use GetNoise(x, y)
  • For domain-rotated 3D Perlin, do setRotationType3D(FastNoiseLite.RotationType3D.ImproveXYPlanes), setNoiseType(FastNoiseLite.NoiseType.Perlin), and only use GetNoise(x, y, 0) not GetNoise(x, y). You can also:
    • Do setRotationType3D(FastNoiseLite.RotationType3D.ImproveXZPlanes) and GetNoise(x, 0, z)
    • Use this to improve Value and ValueCubic which have similar problems as Perlin when unrotated or used in 2D
    • Use this on the 3D Simplex-type noise to further improve their results.
    • Actually use the third coordinate, such as for 3D noise in a mostly-2D world. Use your vertical coordinate in place of the previously-unused one, for best results.

This library can also do the fractal summation for you, by setting the fractal type to FBm, so you can completely offload that stage. You can still do it yourself though, by leaving this set to None.

Important usage details:

  • It handles frequency scaling internally, with frequency set to 0.01 by default. You can either embrace this, or call SetFrequency(1) to continue doing it how you were.
  • Its range is -1 to 1 not 0 to 1. So if you are going to use the Mathf.Pow function to curve the value range, or just need the values to match what you've come to expect, then you can rescale to 0 to 1 range by doing value = value * 0.5f + 0.5f.

To use doubles rather than floats, there is a configuration option at the top of the FastNoiseLite.cs file.

Final note: I see your island falloff uses vector distance. It is better to use a squared distance based falloff curve, in order to avoid a sharp point in the middle. Replace your finalValue = finalValue - Vector2.Distance(new Vector2(x, y), new Vector2(width / 2, height / 2)) / (width * waterCoefficient); line with finalValue = finalValue - (new Vector2(x, y) - new Vector2(width / 2, height / 2)).sqrMagnitude * (1.0f / (width * width * waterCoefficient)); It's also more efficient because it avoids the square root call.

\$\endgroup\$
1
  • \$\begingroup\$ Thanks a lot for your reaaally complete answer! Reducing the offset value by 100 times fixed everything indeed. I will have a lot of things to work on on top of what i already had in mind. \$\endgroup\$
    – Bellaedris
    Apr 19, 2021 at 21:02

You must log in to answer this question.

Not the answer you're looking for? Browse other questions tagged .