1
\$\begingroup\$

I am trying to do something like Minecraft game in Unity. Currently I am trying to generate a chunk of blocks, given dimensions. I can successfully create a chunk of N x 0 x 0 sized chunk, but due to lack of logic, I have no idea on how to make triangles and vertices match for other blocks.

Basically, what I mean is, I can generate something like this:

enter image description here

But can't do this:

enter image description here

My Block vertices and Triangles:

Vector3[] cVerts = {
        new Vector3 (0, 0, 0), //0
        new Vector3 (1, 0, 0), //1
        new Vector3 (1, 1, 0), //2
        new Vector3 (0, 1, 0), //3
        new Vector3 (0, 1, 1), //4
        new Vector3 (1, 1, 1), //5
        new Vector3 (1, 0, 1), //6
        new Vector3 (0, 0, 1), //7
    };
int[] cTris = {
        0, 2, 1, //face front
        0, 3, 2,
        2, 3, 4, //face top
        2, 4, 5,
        1, 2, 5, //face right
        1, 5, 6,
        0, 7, 4, //face left
        0, 4, 3,
        5, 4, 7, //face back
        5, 7, 6,
        0, 6, 7, //face bottom
        0, 1, 6
    };

Chunk creation method:

     void CreateChunk(){
        ...
        for (int x = 0; x < 100; x++)
        {
           for(int y = 0; y < 10; y++){
               for (int z = 0; z < 1; z++)
               {
                  AddBlockAtPosition(new VectorInt(x, y, z), "Grass");
               }
           }
        }

        ChunkMesh.vertices = CurrentChunkVerts.ToArray();
        ChunkMesh.triangles = CurrentChunkTris.ToArray();

        //ChunkMesh.uv = uvs.ToArray();
        ChunkMesh.RecalculateNormals();
   }

AddBlockAtPosition() method:

    void AddBlockAtPosition(VectorInt pos, string name)
    {
       int x = pos.x, y = pos.y, z = pos.z;

       //create separate cube data
       CubeMesh cube = new CubeMesh(vertices.ToArray(), triangles.ToArray(), uvs.ToArray());
       //I use MoveCube() to change each Block's vertex and triangle numbers, so they will "match" in a Chunk mesh, when they become part of it.
       cube.MoveCube(new Vector3(x, y, z), (x + y + z) * 8); 
       //cube.MoveUV(new Vector2(x + y + z, x + y + z));

       CurrentChunkVerts.AddRange(cube.Vertices());
       CurrentChunkTris.AddRange(cube.Triangles());
       //CurrentChunkUV.AddRange(cube.UVs());
    }

And most importantly, that's where my problems are, MoveCube();

Class Cube{
    private Vector3[] vertices;
    private int[] triangles;
    ...
    public void MoveCube(Vector3 direction, int n)
    {
        for(int i=0; i<vertices.Length; i++)
        {
            vertices[i] += direction;
        }
        for (int i = 0; i < triangles.Length; i++)
        {
            triangles[i] = triangles[i] + n;
        }
    }
}

I know that I am not the first to re-create minecraft, I have found this tutorial https://www.blockstory.net/node/56

But for me as a beginner, some "simple" parts like these I have problems with are skipped. And other tutorials just generalize the idea, which isn't enough for me.

\$\endgroup\$

1 Answer 1

7
\$\begingroup\$

Remember, the whole point of using a chunk mesh is to try to render just the outside skin, and hide all the internal detail inside the chunk where we can't see it. If we generate a whole cube (all 12 triangles) at every occupied position, then we might as well spawn it as a separate mesh and at least hope GPU instancing saves us some work - otherwise we're both losing instancing AND rendering stuff we don't need, the worst of both worlds.

So, to minimize redundant geometry, we can do something a bit like this...

(Here I'm going to assume your chunks can be made of multiple materials, whose textures are stored as a texture array so we can use UVs over an arbitrary range and they'll tile correctly for us. This way when four faces of the same material meet, we only need 1 vertex, rather than 4 with different UV coordinates. (We can also do this in the shader math if texture arrays aren't available.)

First we define the characteristics we want to be the same at any shared vertex:

struct VertexSignature {
    int3 position,
    CardinalDirection normal,
    BlockMaterial material,
}

Now we can use this to de-duplicate vertices, referring back to the same index anytime our code asks for a vertex with the same characteristics:

Dictionary<VertexSignature, int> _vertexLookup = new Dictionary...
int _vertexCount = 0;

int GetVertexIndex(VertexSignature signature) {
    int index;
    if(!_vertexLookup.TryGetValue(signature, out index) {
        index = _vertexCount++;
        _vertexLookup.Add(signature, index);
    }
    return index;
}

Next we can iterate over all the spots in our chunk, to see which blocks need to produce faces (I'm going to be a bit loose & pseudocode-y for brevity):

foreach(int3 position in chunk.Bounds) {
    // I assume ultimately you'll have some type of data structure that records
    // the current block at each position, whether the product of a terrain generator
    // or player mining/building, etc.
    var block = World.GetBlockAt(position);

    // If this block is empty air, we don't need to draw any faces for it.
    if(block.type == BlockType.Empty)
        continue;

    // Otherwise, we check if this block is exposed on any face.
    foreach(CardinalDirection direction in Neighbourhood) {
        var neighbour = World.GetBlockAt(position + direction);

        // If the block is occluded on this side, we don't need to draw
        // the faces in between the two blocks.
        if(neighbour.type != BlockType.Empty)
            continue;
        
        // Otherwise, we've found an exposed face. Add it to the list.
        AddFace(position, direction, block.type);
    }
}

To add each face, we just look up the corresponding position offsets we want for each vertex for a face on that side of the cube, request an index for each resulting vertex, and add those indices to our growing list.

List<int> _indices;

void AddFace(int3 position, CardinalDirection direction, BlockMaterial material) {
    // Have a table or map that lets you quickly look up the 4 vertex offsets
    // from the cube's bottom-left-back corner for a given face direction.
    int3[] vertexPositions = GetVertexPositions(direction);
    
    VertexSignature signature;
    signature.normal = direction;
    signature.material = material;

    signature.position = position + vertexPositions[0];
    int a = GetVertexIndex(signature);

    signature.position = position + vertexPositions[1];
    int b = GetVertexIndex(signature);

    signature.position = position + vertexPositions[2];
    int c = GetVertexIndex(signature);

    signature.position = position + vertexPositions[3];
    int d = GetVertexIndex(signature);

    _indices.Add(a);
    _indices.Add(b);
    _indices.Add(c);

    _indices.Add(a);
    _indices.Add(c);
    _indices.Add(d);
}

Lastly, once we've visited every block and added all the indices we need, we can bake down our vertex signatures into the corresponding arrays for positions, normals, and UVs...

vertices = new Vector3[_vertexCount];
normals = new Vector3[_vertexCount];
uvs = new Vector3[_vertexCount];

foreach(var pair in _vertexLookup) {
    int index = pair.value;
    vertices[index] = pair.key.position;
    normals[index] = (Vector3)pair.key.normal;

    Vector3 uv = ProjectPositionToUV(pair.key.position, pair.key.normal);
    uv.z = pair.key.material.index;

    uvs[index] = uv;
}

From here you've got all the ingredients you need to build a mesh of arbitrary shape & size, with shared vertices and no internal faces. Note that since we've only been using basic types & math above, all of this can be done on a separate thread, leaving only the final assembly of these buffers into the Mesh instance and uploading to the GPU for the main thread to handle.

The simple method shown here still produces some redundant faces & vertices: a long rectangular plane of the same material could just be two faces instead of length * width * 2, but that's a further optimization we can tackle separately. This Q&A thread and linked article goes into more detail about merging coplanar quads this way.


Here's one way you could implement the texture mapping function:

Vector2 ProjectPositionToUV(int3 position, CardinalDirection normal) {
    switch(normal) {
        case CardinalDirection.Up:
        case CardinalDirection.Down:
             return new Vector2(position.x, position.z);
        case CardinalDirection.East:
        case CardinalDirection.West:
             return new Vector2(position.z, position.y);
        case CardinalDirection.North:
        case CardinalDirection.South:
             return new Vector2(position.x, position.y);
    }
    // Fallback for an invalid input direction.
    return Vector2.zero;
}
\$\endgroup\$
6
  • \$\begingroup\$ I don't understand what the script do and how it does it. Is this the script supposed to hide inside faces ? If not what does it do ? If yes, how do you make it happen in general wit that script ? Does it delete inside triangles ? does it somehow merge vertices ? Does it just hide them ? \$\endgroup\$ Oct 4, 2019 at 11:40
  • \$\begingroup\$ This script builds a mesh consisting of only outside faces. It does it by scanning through the voxel data for the chunk, checking for places where each voxel is exposed to air. Where it finds such a spot, it creates a new pair of triangles to draw there. If two solid voxels sit back-to-back, it generates no triangles between them. It's also set up to share vertices, so where four voxels in a plane meet at a central point, you get one shared vertex instead of four. \$\endgroup\$
    – DMGregory
    Oct 4, 2019 at 11:44
  • \$\begingroup\$ @DMGregory I know its a bit late, but only now I've actually had time to start implementing this. Everything's fine so far, I got vertex joining optimization done now, but I am stuck on the ProjectPositionToUV function. Does pair.key.position even matter here? I think I've ideas on how to make this work by adding additional pair.Key.localOffset variable into VertexSignature struct, but if I am doing this wrong I would like to know \$\endgroup\$
    – Nick
    Mar 4, 2020 at 18:03
  • 1
    \$\begingroup\$ I've included an example. You can of course choose to project your texture coordinates any other way that suits your needs. \$\endgroup\$
    – DMGregory
    Mar 4, 2020 at 18:17
  • \$\begingroup\$ @DMGregory Sorry, I still can't understand how this should work. Since vertices are now shared between 2 voxels, and for each of these voxels, each vertex is a different "local offset"(eg: Bottom right corner is bottom left corner for the next voxel), how is texture going to be projected correctly? Also I've trouble understanding how global position of vertex in your code(aka position.xyz) directly represents offset on the 2D texture atlas? (If atlas is 256x256 each 16x16 representing 1 block) \$\endgroup\$
    – Nick
    Mar 5, 2020 at 14:15

You must log in to answer this question.

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