3
\$\begingroup\$

I'm working on procedural planet generation project in Unity. To create a sphere, I use the following technique:

  1. Create six planes arranged as faces of a cube
  2. Normalize all vertices' positions to create a unit sphere
  3. Multiply each vertices' positions by the height value obtained from an IHeightsProvider (3D simplex noise in this case) + the planet's radius.

All operations necessary for creating a mesh (create vertex positions array, triangle indices array, normals array) are performed in a separate thread for performance reasons. Vertices, triangles, and normals, encapsulated in a MeshData object, are assigned to Unity's Mesh object in the main thread.

The problem is, of course, seams between faces of the cubesphere:

Seams between cubesphere faces enter image description here

These issues are due to the flawed normal computation algorithm, which only takes normals of triangles in the mesh into account, being not aware of the adjacent meshes. Ideally, I'd like to have normals computed regardless of adjacent meshes' presence (i.e no SetNeigbors), using the same heightsProvider used for obtaining the mesh's own vertices' heights.

I've been trying to figure it out myself, as well as extensively searching the web, for a few days so far, but the problem doesn't seem to become any clearer.

Here's my code for creating vertices, triangles and normals in a separate thread:

protected override void ThreadFunction (System.Object info)
    {
        int resolution = parameters.resolution;
        int numVertices = resolution * resolution;

        float minHeight = parameters.minHeight;
        float maxHeight = parameters.maxHeight;

        Quaternion faceRotation = parameters.rotation;

        // Create vertices
        // ---------------

        Vector3[] vertices = new Vector3[numVertices];

        int vertNum = 0;

        for (int x = 0; x < resolution; x++)
        {
            for (int y = 0; y < resolution; y++) 
            {
                Vector3 posOnCube = GetVertexPosOnCube (x, y);
                Vector3 posOnUnitSphere = CubeToSphere (posOnCube);

                Vector3 samplePosition = posOnUnitSphere;
                float height = GetHeight (samplePosition);

                vertices[vertNum++] = posOnUnitSphere * height;
            }
        }

        this.meshData.vertices = vertices;

        // Create triangle indices array
        // -----------------------------

        int[] triangles = new int [(resolution - 1) * (resolution - 1) * 6];

        int triangleIndex = 0;
        int vertexIndex = 0;

        for (int x = 0; x < resolution - 1; x++)
        {
            for (int y = 0; y < resolution - 1; y++) 
            {
                triangles[triangleIndex] = vertexIndex;
                triangles[triangleIndex + 3] = triangles[triangleIndex + 2] = vertexIndex + (resolution - 1) + 1;
                triangles[triangleIndex + 4] = triangles[triangleIndex + 1] = vertexIndex + 1;
                triangles[triangleIndex + 5] = vertexIndex + (resolution - 1) + 2;

                triangleIndex += 6;
                vertexIndex++;
            }

            vertexIndex++; 
        }

        this.meshData.triangles = triangles;

        // Calculate normals
        // -----------------

        Vector3[] normals = new Vector3 [numVertices];

        for (int i = 0; i < meshData.triangles.Length; i += 3)
        {
            int ind0 = meshData.triangles[i + 0];
            int ind1 = meshData.triangles[i + 1];
            int ind2 = meshData.triangles[i + 2];

            Vector3 pos0 = meshData.vertices [ind0];
            Vector3 pos1 = meshData.vertices [ind1];
            Vector3 pos2 = meshData.vertices [ind2];

            Vector3 to1 = pos1 - pos0;
            Vector3 to2 = pos2 - pos0;

            Vector3 normal = Vector3.Cross (to1, to2).normalized;

            normals [ind0] += normal;
            normals [ind1] += normal;
            normals [ind2] += normal;
        }

        for (int i = 0; i < numVertices; i += 1)
        {
            normals [i].Normalize ();
        }

        this.meshData.normals = normals;
    }
\$\endgroup\$
3
  • \$\begingroup\$ Two quick (somewhat related) questions. One: instead of transforming six planes into a sphere, why aren't you just doing that directly with a cube? Then meshes are directly connected, you save exponential number of vertices and your life becomes easier. Two: do the seams also appear before applying the noise? If no, it would be nice to see how your applying it. \$\endgroup\$
    – MAnd
    Nov 30, 2015 at 16:41
  • \$\begingroup\$ @maAnd 1. Because I plan to make each face of the cube an independent quadtree which subdivides as the camera comes closer. 2. Yes they do. They're less noticeable but that's probably because of the smoothness of an ideal sphere's surface as far as I undestand. \$\endgroup\$ Nov 30, 2015 at 17:34
  • \$\begingroup\$ One quick & dirty fix is to apply a cleanup pass after calculating your normals: walk the edges of your planes and average the normals of coincident vertices (2-way average for most, 3-way at the corners). This ensures they're consistent without much added complexity, though it only works if you have adjacent faces loaded (which, conveniently, is the only time the seam is visible anyway). I can expand this into an answer if you find it to be of use. \$\endgroup\$
    – DMGregory
    Dec 1, 2015 at 5:08

1 Answer 1

1
\$\begingroup\$

The problem is your normals on the edges of your tiles, you can't calculate them without having the next tile in the range.

I got round this problem by generating a border row around my tile (or in my case voxel chunk) so that when i come to build meshes from that data I can correctly determine the real normals.

you might be able to solve this by simply doing this in the first loop pair ...

for (int x = -1; x < resolution + 1; x++)
        {
            for (int y = -1; y < resolution + 1; y++) 
            {

More input based on comments below:

so the theory goes something like this ... i want a mesh that sits in the range 0 to 10 on each axis with all the right normal info, so here's how I would do it ...

Vector3[] positions = new [12 * 12 * 12];

// generate the full range
for(x -1 to 11) 
  for(y -1 to 11) 
      positions[ComputeIndex(x,y,z)] = ComputePos(x.y.z);

    // iterate over the range to generate mesh info
List<vert> meshInfo = new List<vert>;
    int currentIndex = 0;
    for(x 0 to 10) 
      for(y 0 to 10) 
        {
          var vert = new vert { 
              pos = positions[ComputeIndex(x,y,z)],
              index = currentIndex,
              normal = ComputeNormal(x,y,z),
              uv = ComputeUv(x,y,z)
          }
          currentIndex++;
        }

// assuming a unity mesh is what we want
var mesh = new Mesh {
   vertices = meshInfo.Select(i => i.pos).ToArray(),
   normals = meshInfo.Select(i => i.normal).ToArray(),
   uv = meshInfo.Select(i => i.uv).ToArray()
};
mesh.triangles(meshInfo.Select(i => i.index).ToArray());
\$\endgroup\$
7
  • \$\begingroup\$ I considered this as an option, but couldn't figure out how to make it distinguish "outer" vertices from the "inner" ones so that I don't just end up with a grid two vertices wider. Could you please provide more implementation details? \$\endgroup\$ Nov 30, 2015 at 17:26
  • \$\begingroup\$ The change I proposed in my answer need only be applied to the first loop pair. That will generate you vertex data for your mesh +1 around the border. In the second and third set of loops do not make this change and your meshing will continue to use only the set you are interested in. \$\endgroup\$
    – War
    Nov 30, 2015 at 22:11
  • \$\begingroup\$ if you can't figure that out, surely the edge verts are verts where the values for either x or y are < 0 or == resolution \$\endgroup\$
    – War
    Nov 30, 2015 at 22:13
  • \$\begingroup\$ That part is clear, thanks. The confusing part is computing normals for the inner vertices: since that is done by traversing the triangle array, I'd have to create another triangle array which includes the outer vertices. And for that I'd have to make another vertex index for the bigger grid in the first ("vertex") loop, and then, in the "normal" loop, map it onto the inner grid's vertex index. \$\endgroup\$ Dec 1, 2015 at 5:50
  • 1
    \$\begingroup\$ No, first generate 1 set of verts, the range of which is say -1 to 11 on each axis then generate a mesh for the range 0 to 10 on each axis. Your indices will need to be generated in the second part ... hang on I will tweak my answer. \$\endgroup\$
    – War
    Dec 4, 2015 at 10:17

You must log in to answer this question.

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