0
\$\begingroup\$

I'm creating a procedurally generated planet object in Unity. I start with a cube which is created from 6 meshes (6 sides) and normalize the vertices to get a standard sphere. The problem is that there are visible seams between the meshes. I know if I set the vertex normals to the direction from the center of the sphere to the current vertex then seams will gone but my next step is to add noise to the meshes to generate terrain on the sphere.

What I already tried to remove the seams:

  • Generating an extra strip around the mesh enter image description here
  • Generating an extra strip around the mesh which is located on the adjacent sides enter image description here

The second solution is almost perfect but seams are still visible a little bit. enter image description here

When I create the mesh object:

  • call the built-in RecalculateNormals on the mesh (including the borders)
  • remove the border vertices, indices and normals
  • reassign the vertices, indices and normals to the mesh object (excluding the borders)

I use libnoisedotnet to create simplex Perlin noise on the surface:

private MeshData CalculateMeshData()
{
    MeshData meshData = new MeshData();
    meshData.ChunkSize = CHUNK_SIZE;
    meshData.BorderSize = BORDER_SIZE;

    int borderedResolution = meshData.BorderedResolution;
    int vertexIndex = -1;
    float faceWidth = _radius * 2 / CHUNK_SIZE;
    float currentYPosition = -_radius - faceWidth * BORDER_SIZE;

    for (int y = 0; y < borderedResolution; y++)
    {
        float currentXPosition = -_radius - faceWidth * BORDER_SIZE;
        for (int x = 0; x < borderedResolution; x++)
        {
            bool isMeshVertex = !IsBorderVertex(x, y, borderedResolution);
            Vector3 axisAOffset = _axisA * currentXPosition;
            Vector3 axisbOffset = _axisB * currentYPosition;
            Vector3 pointOnUnitCube = _center + axisAOffset + axisbOffset;
            Vector3 vertex = pointOnUnitCube.normalized;
            vertex = vertex * GetElevation(vertex);
            
            vertexIndex++;
            meshData.AddVertex(vertex, isMeshVertex);

            // Prevents to create more two triangles next to the edge of the mesh.
            if (x != borderedResolution - 1 && y != borderedResolution - 1)
            {
                bool isMeshTriangle = IsMeshTriangle(x, y, borderedResolution);
                meshData.AddTriangle(vertexIndex,
                                     vertexIndex + borderedResolution + 1,
                                     vertexIndex + borderedResolution,
                                     isMeshTriangle);

                meshData.AddTriangle(vertexIndex,
                                 vertexIndex + 1,
                                 vertexIndex + borderedResolution + 1,
                                 isMeshTriangle);
            }

            currentXPosition += faceWidth;
        }

        currentYPosition += faceWidth;
    }

    return meshData;
}

private float GetElevation(Vector3 position)
{
    return 1 + _noise.Evaluate(position) / 10f;
}

Here is the noise library what I tried to use: https://github.com/SebLague/Procedural-Planets/blob/master/Procedural%20Planet%20E03/Assets/Noise.cs

Is it possible to remove the seams fully?

\$\endgroup\$
3
  • \$\begingroup\$ Have you considered to not create 6 separate meshes but instead put all the vertices from the 6 sides into one mesh? \$\endgroup\$ – Philipp Dec 21 '20 at 15:15
  • \$\begingroup\$ I assumed creating LOD system with quadtree is easier if I have separated meshes for each side. \$\endgroup\$ – Panthesilea Dec 21 '20 at 15:24
  • \$\begingroup\$ I am not sure what kind of LOD system you intend to build, but I suspect that it would lead to even more problems with seams if you keep looking at the six sides individually. \$\endgroup\$ – Philipp Dec 22 '20 at 1:56
1
\$\begingroup\$

Let's modify your simplex noise function to also compute the gradient of the noise field at the sample position. This lets us share the work between the two computations, so it's cheaper than adding a second EvaluateGradient() method or sampling the noise field multiple times to get finite differences.

First we'll modify the signature to give us our gradient in an out variable:

public float Evaluate(Vector3 point, out Vector3 gradient)

Then we'll initialize our gradient to a zero vector:

gradient = Vector3.zero;

Toward the bottom of the function, you'll find four sections that look like this, computing the value contribution from each corner of the simplex:

if (t0 > 0)
{
    t0 *= t0;
    int gi0 = _random[ii + _random[jj + _random[kk]]]%12;
    n0 = t0*t0*Dot(Grad3[gi0], x0, y0, z0);
}

We're going to modify those to this:

if (t0 > 0) {
    // We need a t-cubed later, so we'll store this into its own variable.
    double tSquared = t0 * t0;
    
    // Same as before - note that this code differs slightly between the 4 corners.
    int gi0 = _random[ii + _random[jj + _random[kk]]]%12;

    // Naming our local corner gradient and dot product for re-use.
    var g = Grad3[gi0];
    double dot = Dot(g, x0, y0, z0);

    // Same calculation as before, just with our new variable names.
    n0 = tSquared * tSquared * dot;

    // Compute the gradient of t (derivative with respect to x, y, z).
    Vector3 tPrime = -2f * new Vector3((float)x0, (float)y0, (float)z0);

    // Accumulate the gradient contribution.
    gradient += (float)(4 * tSquared * t0 * dot) * tPrime 
              + (float)(tSquared * tSquared) * new Vector3(g[0], g[1], g[2]);
}

Repeat that substitution for each of the four, making sure to change t0, x0 etc. into the corresponding t1, x1. Or refactor this into a function you can call four times with different arguments - just be careful of the differences in the calculation of the gradient index.

Now in our planet generator, we'll modify your elevation function to also compute the normal vector for the point:

float GetElevationAndNormal(Vector3 direction, Noise out Vector3 normal) {
    // Sample the noise value and gradient at our position.
    float sample = _noise.Evaluate(direction * noiseFrequency, out Vector3 gradient);

    // Zero out the component of the gradient perpendicular to the sphere.
    var surfaceGradient = gradient - direction * Vector3.Dot(gradient, direction);

    // Compute an updated normal - leaning away from the direction of increase.
    normal = (direction - 25f * noiseAmplitude * noiseFrequency * surfaceGradient).normalized;

    // Convert our noise sample to an elevation.
    return 1.0f + noiseAmplitude * sample;
}

Note that because the only input to this function is the direction on the sphere, when we return to that same direction on another mesh, we'll get the same output (ie. consistent normals on both sides of the seam). So there's no need for the extra skirt geometry to try to force RecaclulateNormals to average-out the way you want.

I'll confess that 25f magic number in there is a bit of a fudge factor. My calculated normals were initially too soft, but I haven't had my coffee yet so I'm struggling a bit to figure out why. It might be because I'm approximating the surface as being locally flat, and the curvature of the sphere has more impact than that. In any case, for now, scaling by 25 seems to bring this in line with the sharpness of shading you get from RecalculateNormals but without the seams. I'll update this if I can find a better mathematical justification for getting the right normal tilt.

If you want to elaborate on your noise sampling - say layering octaves of noise for an FBM/Turbulence effect - you can use the fact that the differential operator is linear. So the gradient of a sum of scaled noise fields is just the sum of their scaled gradients. So as you sum up the elevation from each octave, also accumulate the gradients separately, then convert to a normal as shown above.

\$\endgroup\$
12
  • \$\begingroup\$ Thanks, it works fine. Yes I'm going to use multiple layers and according your answer it seems pretty easy. \$\endgroup\$ – Panthesilea Dec 26 '20 at 18:36
  • \$\begingroup\$ How should I change the 25f * noiseAmplitude * noiseFrequency part when multiple noise layers are added? Here is the detailed question: link \$\endgroup\$ – Panthesilea Jan 7 at 13:25
  • \$\begingroup\$ Remember the frequency and amplitude are attributes of a single octave. That multiplication belongs inside your loop, so you can use different frequencies and amplitudes for the gradient from each octave of noise. \$\endgroup\$ – DMGregory Jan 7 at 13:27
  • \$\begingroup\$ Do you mean put this inside the layer loop: s += 25f * noiseFrequency * noiseAmplitude? And use the sum to calculate the normal: normal = (direction - s * surfaceGradient).normalized? I tried this but not perfect I divided the s with the layerCount * 2.5f and almost perfect but not identical with the Unity way). But maybe I misunderstood this. \$\endgroup\$ – Panthesilea Jan 7 at 13:57
  • 1
    \$\begingroup\$ The gradient is the derivative of the noise function. If you modify the noise function, you need to calculate a new derivative. You can post a new question if you need help computing the derivative for a particular function. \$\endgroup\$ – DMGregory Mar 8 at 21:36

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service, privacy policy and cookie policy

Not the answer you're looking for? Browse other questions tagged or ask your own question.