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\$
6
\$\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.

\$\endgroup\$
  • \$\begingroup\$ I'm not quit sure how this script works. I've tried to figure it out, but I still dont get it. And I don't understand how to incorporate it in my own script, since this uses things I haven't learned yet. Do you think you could explain it in some way ? You don't have to tho. \$\endgroup\$ – Samuel Fyckes Oct 3 at 11:47
  • \$\begingroup\$ What I've written above is my way of explaining it. If you ask me to explain it again, without more precision about what you need explained, I'm likely ro do it in a similar way, which wouldn't be helpful to you. So, try to narrow down: where did you get lost? What specific parts don't make sense to you? With that detail, I or another user can focus our explanations on the specific parts you need help with. If you need more room to explain what you need, try posting a new question, linking to this one for context. \$\endgroup\$ – DMGregory Oct 3 at 12:18
  • \$\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\$ – Samuel Fyckes Oct 4 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 at 11:44
  • \$\begingroup\$ Ohh okk. Now I get it. Thanks for your response. I'll study the script to undersatnd what does what and then integrate it to my code \$\endgroup\$ – Samuel Fyckes Oct 4 at 11:56

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.