Efficient 3D noise

I’m working on a procedural planet generator. It’s going great, I’m using Sebastian Lague’s planet and I’ve implemented an LOD system for it by a guy named Simon.

However I’m at a crossroads, I need new noise. The noise.cs file Sebastian uses is too inefficient for my purposes (without any way to save some sort of output at least):

Where CalculatePointOnPlanet() is running every single vertex through noise.cs. Because I’m using an LOD system, this is run frequently which creates painful lag spikes. Now to be fair, this screenshot comes from a rather intensive test where I’m trying optimizations on more intense meshes than I’d actually implement for a game. This is also from the project running in the editor while the profiler is running. Nevertheless the amount of vertex math is too much, there is a considerable performance increase on a compiled project when I remove all this and just stick with a featureless sphere.

I’ve done some research around the internet on 3D noise for unity and there’s not a ton to go off, in every instance there’s someone offering different advice for a different use case or the thread is a decade old. I’ve found other people’s implementation of 3D noise and it looks like more of the same that I already have, vector math with little concern for frequent recalculations.

I’m wondering what the best option is for 3D noise that doesn’t involve a million iterations of vector math? I’ve seen cases where 2D noise was stored, whether generated on the fly in code or from an imported texture, is there a way to do this with 3D? I’ve looked but haven’t found anything.

Any other advice on how to make such a thing more performant would be appreciated, I’m not experienced.

1 Like

You are very lucky to have bottleneck with purely math code actually.
First of all Mono backend is VERY bad at math ops, since editor is running on mono(even you select il2cpp on player settings), do not make assumptions on about math heavy systems, instead profile builds
(except burst compiled methods these are okay on editor).

So switching mono to il2cpp will give huge benefit, but there is more powerful things thanks to unity, which is Burst compiler and and job system.

Bursting pure math code is very easy, just use new math library vectors instead of old ones (vector3 => float3) and add burst compile attribute top of method and class and pass and return by reference everything. (See burst documentation)

Parallel Job system could be bit tricky for beginners to multithreading, but I would definitely suggest that to learn about burst, Job system and advanced mesh api to anyone who want to create a procedural mesh system.

if I would list worse to better approach

  • math with mono compile
  • math with il2cpp compile
  • math with burst compile
  • math with burst compiled IJob
  • math with burst compiled parallel job (IJobFor)

Also you could try using floats instead of doubles.

2 Likes

Thanks a lot, I appreciate all the advice! Looks like I have some research to do.

I do still have a whole game to put on top of this! Incidentally my other main bottleneck is calculating normals for all the vertices, is that something your advice would help with as well?

you are very lucky again I’m considering myself as a normal recalculation guy :smile:
check this https://github.com/burak-efe/Ica_Normal_Tools
well some documentation need to be updated and and I would suggest you to check development branch

2 Likes

More research! I’ll look over this thanks. It’s not possible I won’t have questions, I’ll be back. It’s going to take a few days.

Since it may serve future me well, I’ll paste where I’m currently getting my normals from. I did not write this code. SurfaceNormalFromIndices() at the bottom is the performance monster.

        normals = new Vector3[vertices.Length];

        int triangleCount = triangles.Length / 3;

        int vertexIndexA;
        int vertexIndexB;
        int vertexIndexC;

        Vector3 triangleNormal;

        int[] edgefansIndices = Presets.quadTemplateEdgeIndices[quadIndex];

        for (int i = 0; i < triangleCount; i++)
        {
            int normalTriangleIndex = i * 3;
            vertexIndexA = triangles[normalTriangleIndex];
            vertexIndexB = triangles[normalTriangleIndex + 1];
            vertexIndexC = triangles[normalTriangleIndex + 2];

            triangleNormal = SurfaceNormalFromIndices(vertexIndexA, vertexIndexB, vertexIndexC);


            // Don't calculate the normals on the edge edgefans here. They are only calculated using the border vertices.
            if (edgefansIndices[vertexIndexA] == 0)
            {
                normals[vertexIndexA] += triangleNormal;
            }
            if (edgefansIndices[vertexIndexB] == 0)
            {
                normals[vertexIndexB] += triangleNormal;
            }
            if (edgefansIndices[vertexIndexC] == 0)
            {
                normals[vertexIndexC] += triangleNormal;
            }
        }

        int borderTriangleCount = borderTriangles.Length / 3;

        for (int i = 0; i < borderTriangleCount; i++)
        {
            int normalTriangleIndex = i * 3;
            vertexIndexA = borderTriangles[normalTriangleIndex];
            vertexIndexB = borderTriangles[normalTriangleIndex + 1];
            vertexIndexC = borderTriangles[normalTriangleIndex + 2];

            triangleNormal = SurfaceNormalFromIndices(vertexIndexA, vertexIndexB, vertexIndexC);

            // Apply the normal if the vertex is on the visible edge of the quad
            if (vertexIndexA >= 0 && (vertexIndexA % (Presets.quadRes + 1) == 0 ||
                vertexIndexA % (Presets.quadRes + 1) == Presets.quadRes ||
                (vertexIndexA >= 0 && vertexIndexA <= Presets.quadRes) ||
                (vertexIndexA >= (Presets.quadRes + 1) * Presets.quadRes && vertexIndexA < (Presets.quadRes + 1) * (Presets.quadRes + 1))))
            {
                normals[vertexIndexA] += triangleNormal;
            }
            if (vertexIndexB >= 0 && (vertexIndexB % (Presets.quadRes + 1) == 0 ||
                vertexIndexB % (Presets.quadRes + 1) == Presets.quadRes ||
                (vertexIndexB >= 0 && vertexIndexB <= Presets.quadRes) ||
                (vertexIndexB >= (Presets.quadRes + 1) * Presets.quadRes && vertexIndexB < (Presets.quadRes + 1) * (Presets.quadRes + 1))))
            {
                normals[vertexIndexB] += triangleNormal;
            }
            if (vertexIndexC >= 0 && (vertexIndexC % (Presets.quadRes + 1) == 0 ||
                vertexIndexC % (Presets.quadRes + 1) == Presets.quadRes ||
                (vertexIndexC >= 0 && vertexIndexC <= Presets.quadRes) ||
                (vertexIndexC >= (Presets.quadRes + 1) * Presets.quadRes && vertexIndexC < (Presets.quadRes + 1) * (Presets.quadRes + 1))))
            {
                normals[vertexIndexC] += triangleNormal;
            }
        }

        // Normalize the result to combine the aproximations into one
        for (int i = 0; i < normals.Length; i++)
        {
            normals[i].Normalize();
        }

        return (vertices, GetTrianglesWithOffset(triangleOffset), GetBorderTrianglesWithOffset(borderTriangleOffset, triangleOffset), borderVertices, normals);
    }
private Vector3 SurfaceNormalFromIndices(int indexA, int indexB, int indexC)
    {
        Vector3 pointA = (indexA < 0) ? borderVertices[-indexA - 1] : vertices[indexA];
        Vector3 pointB = (indexB < 0) ? borderVertices[-indexB - 1] : vertices[indexB];
        Vector3 pointC = (indexC < 0) ? borderVertices[-indexC - 1] : vertices[indexC];

        // Get an aproximation of the vertex normal using two other vertices that share the same triangle
        Vector3 sideAB = pointB - pointA;
        Vector3 sideAC = pointC - pointA;
        Vector3 side = Vector3.Cross(sideAB, sideAC);
        return side.normalized;
    }

5000 steps forward, 5000 steps back:

I did my best to transfer my math code to a job which I think “worked”. I had to butcher the code to do it, but theoretically it’s doing everything it would be doing, just incorrectly. But without errors! I’m now spending all my time waiting for the jobs to run. I believe with the way it’s set up now, it’s running several thousand instances of the job (as I calculate the vertices for each individual quad of each LOD). I’m now thinking about where to go next.

Edit - managed something far more egregious. Why is the EditorLoop taking 60,000ms?

Here is the timeline for this. I don’t know what I’m looking at honestly.

You mean you schedule thousand of jobs separately for same calculation? Why is that exactly?
Also your GC increased a lot, make sure you using Advanced mesh api’s getdata methods with native container.
Last picture shows nothing multithreaded, look at worker threads these all empty :C If you can share the new code (or better git repo) maybe we can help.