Aligning Vertices of neighboring irregular shaped faces as well as neighboring Voxels

Hi lovely Unity Community!

I am currently trying to recreate a Dungeon Keeper clone in Unity.
I already got a script creating the blocks which later on will be mineable.
The way I create them is instantiating a GameObject, which gets the block.cs script attached to it.
This script then runs a function MakeBlock(Vector3 position) that runs 5x MakeFace(string face) functions for each side of the block and its top. After each call of the MakeFace() function, the new face mesh is being merged with the former one until I end up with the finished block. In the process, the function will save corner, edge and surface vertices in different arrays, so I have access to them later on.

The problem:
My blocks are slightly randomized in shape to create a irregular look. Currently I don’t allow edge and corner vertices to be irregular so not to create visual seams between the faces and also between neighboring blocks.
In fact I want to have all the vertices randomized but cannot wrap my mind around a technique to align edge and corner vertices of neighboring blocks.
I even failed at giving a blocks individual faces the same corner and edge vertices, since all vertices are saved in the vertices array, which additionally the corner and edge vertices are being stored as copy into their respective arrays. The mesh is created from only the vertices array, so each face has its own corner and edge vertices, despite them being locatet exactly where their neighboring faces corner and edge vertices are. (Problem of double vertices instead of using the same)
I also didn’t address the problem of blocked (not visible) surfaces still being rendered, but that’s a problem for another day.

What would be your approach to include the same edge or corner vertices for different faces and their polygon triangles when creating them with the following script?
And is there a way to combine vertices occupying the same space when merging meshes?

Attached you will see the important part of the MakeFace() function and a screenshot.

private void MakeFace(string face)
  {
    Mesh currentMesh = new Mesh();
    currentMesh.name = "Top Mesh";

    int v = 0;
    int t = 0;
    int e = edgeCount;
    int c = cornerCount;

    float vertexOffsetX;
    float vertexOffsetY;
    float vertexOffsetZ;
 
    // Top Face
    if (face == "top") {
      vertexOffsetX = cellSizeTopX * 0.5f;
      vertexOffsetZ = cellSizeTopZ * 0.5f;

      vertices = new Vector3[(topGridWidthNum + 1) * (topGridWidthNum + 1)];
      triangles = new int[topGridWidthNum * topGridWidthNum * 6];

      Vector3 gridOffset = new Vector3(-(blockSize*0.5f - cellSizeTopX*0.5f), blockHeight, -(blockSize*0.5f - cellSizeTopZ*0.5f));

      for (int x = 0; x <= topGridWidthNum; x++)
      {
        for (int z = 0; z <= topGridWidthNum; z++)
        {
          Vector3 cellOffset = new Vector3(x * cellSizeTopX, 0, z * cellSizeTopZ);
          Vector3 testVector = new Vector3(-vertexOffsetX, 0, -vertexOffsetZ) + cellOffset + gridOffset;

          if ((x == 0 && z == 0) ||
              (x == 0 && z == topGridWidthNum) ||
              (x == topGridWidthNum && z == 0) ||
              (x == topGridWidthNum && z == topGridWidthNum))
          {
            if (!IsInArray(testVector, cornerVerts))
            {
              cornerVerts[c] = testVector;
              c++;
            }
            vertices[v] = testVector;
          }
          else if ((x == 0 && z != 0 && z != topGridWidthNum) ||
                   (x == topGridWidthNum && z != 0 && z != topGridWidthNum) ||
                   (z == 0 && x != 0 && x != topGridWidthNum) ||
                   (z == topGridWidthNum && x != 0 && x != topGridWidthNum))
          {
            if (!IsInArray(testVector, edgeVerts))
            {
              edgeVerts[e] = testVector;
              e++;
            }
            vertices[v] = testVector;
          }
          else
          {
            vertices[v] = new Vector3(-vertexOffsetX, Random.Range(-range, range), -vertexOffsetZ) + cellOffset + gridOffset;
          }
          v++;
        }
      }

      v = 0;
      for (int x = 0; x < topGridWidthNum; x++)
      {
        for (int z = 0; z < topGridWidthNum; z++)
        {
          triangles[t]     = v;
          triangles[t + 1] = v + 1;
          triangles[t + 2] = v + (topGridWidthNum + 1);
          triangles[t + 3] = v + (topGridWidthNum + 1);
          triangles[t + 4] = v + 1;
          triangles[t + 5] = v + (topGridWidthNum + 1) + 1;
          v++;
          t += 6;
        }
        v++;
      }
    }

    if (face == "north") {
...
...
...
...
...
   }
   edgeCount = e;
   cornerCount = c;
   UpdateMesh(currentMesh);
}

I skipped a big part of the script above since it just repeats the code to create another mesh. The basics of how it works can be derived from the code snippet above.

And this is the UpdateMesh(currentMesh) function which fuses the meshes together:

  void UpdateMesh(Mesh currentMesh)
  {
    currentMesh.vertices = vertices;
    currentMesh.triangles = triangles;
    currentMesh.RecalculateNormals();

    Mesh tempMesh = new Mesh();
    tempMesh = CombineMeshes(new List<Mesh> { this.gameObject.GetComponent<MeshFilter>().mesh, currentMesh });
    this.gameObject.GetComponent<MeshFilter>().mesh = tempMesh;
    this.gameObject.GetComponent<MeshFilter>().mesh.name = "FinalMesh";
  }

And the CombineMeshes(List meshes):

private Mesh CombineMeshes(List<Mesh> meshes)
  {
    var combine = new CombineInstance[meshes.Count];
    Matrix4x4 myTransform = transform.worldToLocalMatrix;
    for (int i = 0; i < meshes.Count; i++)
    {
      combine[i].mesh = meshes[i];
      combine[i].transform = transform.localToWorldMatrix * myTransform;
    }
    var mesh = new Mesh();
    mesh.CombineMeshes(combine);
    this.GetComponent<MeshFilter>().mesh = mesh;
    return mesh;
  }

Screenshot of how the blocks look:

Thank you very much for any help you can give me and your time looking into this! <3

Here’s a good article about turning a straight line edge between two faces into a “noisy” edge. Maybe you can glean some insight from it:

https://www.redblobgames.com/maps/noisy-edges/

1 Like

Thanks, I will have a look into it. Right now I settled with creating the initial cube just with 8 vertices and subdividing the surfaces later on, once the voxel map is established and works.

Okay I finally have done it!
The code might not be the prettiest, but it worked out perfectly.

I still have only 1 quad per block side but all I need to do now is subdivide the quad and randomize the vertices which should be fairly simple.

For everyone who faces (no pun intended) the same problem, here is how I have done it:

  1. First I create all blocks uniformly in a grid.
  2. Then I randomize their 8 vertices (Since I want it on the x/z plane, I don’t touch y when randomizing)
  3. I check for the neighbouring block.
  4. I grab the 4 vertices facing to the my current reference block.
  5. Then I align all 4 vertices of the neighbouring block to the ones of my reference block and
  6. Redraw the altered block.

Here is my Vertices-Align-Function. Keep in mind that (in my case) all vertices are in Local Space and have to be transformed to Global Space for alignment before then being saved back into the blocks vertices array transformed into the blocks Local Space.

Aligning Function: (Inside the MapGenerator Script)

void AlignBlockVertices(MapData data)
  {
    for (int z = 0; z < data.Depth; z++)
    {
      for (int x = 0; x < data.Width; x++)
      {
        if (data.GetCell(x, z) == 0)
        {
          continue;
        }
        Block thisBlock = GameObject.Find("Block_"+ x + z).GetComponent<Block>();
        Debug.Log("thisBlock: " + thisBlock.gameObject.name);
        for (int i = 0; i < 4; i++)
        {
          if (data.GetNeighbor(x, z, (Direction)i) != 0)
          {
            Block neighborBlock = GameObject.Find(data.GetNeighborName(x, z, (Direction)i)).GetComponent<Block>();
            Debug.Log("neighborBlock: " + neighborBlock.gameObject.name);
            int k = 0;
            switch(i)
            {
              case 0:
                k = 2;
                break;
              case 1:
                k = 3;
                break;
              case 2:
                k = 0;
                break;
              case 3:
                k = 1;
                break;
            }
            neighborBlock.AdjustVertex(
              BlockMeshData.faceTriangles[k][0],
              thisBlock.WorldPosition(thisBlock.blockVertices[BlockMeshData.faceTriangles[i][1]])
            );
            neighborBlock.AdjustVertex(
              BlockMeshData.faceTriangles[k][1],
              thisBlock.WorldPosition(thisBlock.blockVertices[BlockMeshData.faceTriangles[i][0]])
            );
            neighborBlock.AdjustVertex(
              BlockMeshData.faceTriangles[k][2],
              thisBlock.WorldPosition(thisBlock.blockVertices[BlockMeshData.faceTriangles[i][3]])
            );
            neighborBlock.AdjustVertex(
              BlockMeshData.faceTriangles[k][3],
              thisBlock.WorldPosition(thisBlock.blockVertices[BlockMeshData.faceTriangles[i][2]])
            );
            neighborBlock.RedrawMeshFace();   
          }
        }
      }
    }
  }

Adjustment of the vertex (inside the Block script)

public void AdjustVertex(int oldVertexInt, Vector3 newVertexPosition)
  {
    blockVertices[oldVertexInt] = LocalPosition(newVertexPosition);
  }

And the two local/global space conversion functions: (Also in the Block Script)

public Vector3 WorldPosition(Vector3 vertex)
  {
    return transform.TransformPoint(vertex);
  }

  public Vector3 LocalPosition(Vector3 vertex)
  {
    return transform.InverseTransformPoint(vertex);
  }

1 Like