Creating chunks mesh in a voxel engine is slow

2k Views Asked by At

I am working on a voxel engine in C++, and after implementing chunks, I realised that they are really expensive to generate. By this, I don't mean populating them with blocks, I mean generating a chunk mesh.

The game runs smoothly once chunks are generated, except for placing and removing voxels. Whenever a chunk changes, it's mesh is reconstructed. This is the expensive process. It takes about 0.36 seconds to do for one chunk, which causes a freeze for about 0.36 seconds when a chunk is edited. Furthermore, because of this 0.36-second spike for a single chunk, loading the world with more than a chunk radius or 3 or 4 takes several minutes. With 4 chunks, it takes 189 seconds, (4*2)^3*0.36 (512 chunks each at 0.36 seconds)

This is my mesh generation code. It iterates over every block in the chunk, and if it is not air, it adds cube vertices for it, else, ignores it. This will later become a more complex method with some stuff I have planned, which is bad if the method is already slow.

void WorldRenderer::constructChunkMesh(Chunk* chunk)
{
    if (!chunk->isInitialized() || chunk->getNumBlocks() <= 0)
        return; //If the chunk isn't initialized, or is empty, don't construct anything for it.

    ChunkMesh mesh;

    //iterate over every block within the chunk.
    //CHUNK_SIZE has a value of 16. Each chunk is 16x16x16 blocks.
    for (int x = 0; x < CHUNK_SIZE; x++)
    {
        for (int y = 0; y < CHUNK_SIZE; y++)
        {
            for (int z = 0; z < CHUNK_SIZE; z++)
            {
                if (chunk->getBlock(x, y, z) != Blocks::BLOCK_TYPE_AIR) //if the block is solid, add vertices, otherwise, don't render it.
                {
                    //the 8 vertices for a cube. mesh.addVertex(...) returns the index.
                    int i0 = mesh.addVertex(Vertex(glm::vec3(0.0F, 0.0F, 1.0F) + glm::vec3(x, y, z), glm::vec3(0.0F, 1.0F, 0.0F), glm::vec4(1.0F, 1.0F, 1.0F, 1.0F), glm::vec2(0.0F, 0.0F)));
                    int i1 = mesh.addVertex(Vertex(glm::vec3(1.0F, 0.0F, 1.0F) + glm::vec3(x, y, z), glm::vec3(0.0F, 1.0F, 0.0F), glm::vec4(1.0F, 1.0F, 1.0F, 1.0F), glm::vec2(0.0F, 0.0F)));
                    int i2 = mesh.addVertex(Vertex(glm::vec3(0.0F, 1.0F, 1.0F) + glm::vec3(x, y, z), glm::vec3(0.0F, 1.0F, 0.0F), glm::vec4(1.0F, 1.0F, 1.0F, 1.0F), glm::vec2(0.0F, 0.0F)));
                    int i3 = mesh.addVertex(Vertex(glm::vec3(1.0F, 1.0F, 1.0F) + glm::vec3(x, y, z), glm::vec3(0.0F, 1.0F, 0.0F), glm::vec4(1.0F, 1.0F, 1.0F, 1.0F), glm::vec2(0.0F, 0.0F)));
                    int i4 = mesh.addVertex(Vertex(glm::vec3(0.0F, 0.0F, 0.0F) + glm::vec3(x, y, z), glm::vec3(0.0F, 1.0F, 0.0F), glm::vec4(1.0F, 1.0F, 1.0F, 1.0F), glm::vec2(0.0F, 0.0F)));
                    int i5 = mesh.addVertex(Vertex(glm::vec3(1.0F, 0.0F, 0.0F) + glm::vec3(x, y, z), glm::vec3(0.0F, 1.0F, 0.0F), glm::vec4(1.0F, 1.0F, 1.0F, 1.0F), glm::vec2(0.0F, 0.0F)));
                    int i6 = mesh.addVertex(Vertex(glm::vec3(0.0F, 1.0F, 0.0F) + glm::vec3(x, y, z), glm::vec3(0.0F, 1.0F, 0.0F), glm::vec4(1.0F, 1.0F, 1.0F, 1.0F), glm::vec2(0.0F, 0.0F)));
                    int i7 = mesh.addVertex(Vertex(glm::vec3(1.0F, 1.0F, 0.0F) + glm::vec3(x, y, z), glm::vec3(0.0F, 1.0F, 0.0F), glm::vec4(1.0F, 1.0F, 1.0F, 1.0F), glm::vec2(0.0F, 0.0F)));

                    //The xyz coord in the iteration in world-relative coordinates, instead of chunk-relative
                    int wx = (chunk->getPos().x * CHUNK_SIZE) + x;
                    int wy = (chunk->getPos().y * CHUNK_SIZE) + y;
                    int wz = (chunk->getPos().z * CHUNK_SIZE) + z;

                    //top       y+
                    if (World::getBlock(wx, wy + 1, wz) <= 0)
                    {
                        //if a block does not exist in the y+ direction to this one, add the top face.
                        mesh.addFace(i2, i3, i7);
                        mesh.addFace(i2, i7, i6);
                    }
                    //bottom    y-
                    if (World::getBlock(wx, wy - 1, wz) <= 0)
                    {
                        //if a block does not exist in the y- direction to this one, add the top face.
                        mesh.addFace(i0, i4, i1);
                        mesh.addFace(i1, i4, i5);
                    }
                    //front     z-
                    if (World::getBlock(wx, wy, wz - 1) <= 0)
                    {
                        //if a block does not exist in the z- direction to this one, add the top face.
                        mesh.addFace(i6, i7, i4);
                        mesh.addFace(i7, i5, i4);
                    }
                    //back      z+
                    if (World::getBlock(wx, wy, wz + 1) <= 0)
                    {
                        //if a block does not exist in the z+ direction to this one, add the top face.
                        mesh.addFace(i0, i1, i2);
                        mesh.addFace(i1, i3, i2);
                    }
                    //right     x+
                    if (World::getBlock(wx + 1, wy, wz) <= 0)
                    {
                        //if a block does not exist in the x+ direction to this one, add the top face.
                        mesh.addFace(i1, i7, i3);
                        mesh.addFace(i1, i5, i7);
                    }
                    //left      x-
                    if (World::getBlock(wx - 1, wy, wz) <= 0)
                    {
                        //if a block does not exist in the x- direction to this one, add the top face.
                        mesh.addFace(i2, i6, i4);
                        mesh.addFace(i0, i2, i4);
                    }
                }
            }
        }
    }


    //The rest of this is OpenGL code, and doesn't add any significant
    //performance drop. I have measured this.
    GeometryData gd = MeshHandler::compileGeometry(mesh.vertices.data(), mesh.indices.data(), mesh.vertices.size(), mesh.indices.size());

    RenderableChunk rc;
    rc.pos = chunk->getPos();
    auto a = std::find(chunks.begin(), chunks.end(), rc);
    int index = a - chunks.begin();

    if (a != chunks.end())
    {
        rc = chunks[index];
    }
    else
    {
        GLuint VAO;
        GLuint* VBOs = new GLuint[2];

        //1527864 bytes maximum per chunk (1.5MB)

        glGenVertexArrays(1, &VAO);
        glBindVertexArray(VAO);

        glGenBuffers(2, VBOs);
        glBindBuffer(GL_ARRAY_BUFFER, VBOs[0]);
        glBufferData(GL_ARRAY_BUFFER, sizeof(Vertex) * 8 * MAX_BLOCKS, nullptr, GL_DYNAMIC_DRAW);

        glVertexAttribPointer(ATTRIB_VERTEX_ARRAY, 3, GL_FLOAT, GL_FALSE, sizeof(Vertex), BUFFER_OFFSET(offsetof(Vertex, position)));
        glEnableVertexAttribArray(ATTRIB_VERTEX_ARRAY);
        glVertexAttribPointer(ATTRIB_NORMAL_ARRAY, 3, GL_FLOAT, GL_FALSE, sizeof(Vertex), BUFFER_OFFSET(offsetof(Vertex, normal)));
        glEnableVertexAttribArray(ATTRIB_NORMAL_ARRAY);
        glVertexAttribPointer(ATTRIB_COLOUR_ARRAY, 4, GL_FLOAT, GL_FALSE, sizeof(Vertex), BUFFER_OFFSET(offsetof(Vertex, colour)));
        glEnableVertexAttribArray(ATTRIB_COLOUR_ARRAY);
        glVertexAttribPointer(ATTRIB_TEXTURE_ARRAY, 2, GL_FLOAT, GL_FALSE, sizeof(Vertex), BUFFER_OFFSET(offsetof(Vertex, texture)));
        glEnableVertexAttribArray(ATTRIB_TEXTURE_ARRAY);

        glBindBuffer(GL_ARRAY_BUFFER, 0);

        glBindBuffer(GL_ELEMENT_ARRAY_BUFFER, VBOs[1]);
        glBufferData(GL_ELEMENT_ARRAY_BUFFER, sizeof(GLushort) * 36 * MAX_BLOCKS, nullptr, GL_DYNAMIC_DRAW);
        glBindBuffer(GL_ELEMENT_ARRAY_BUFFER, 0);

        glBindVertexArray(0);

        rc.VAO = VAO;
        rc.VBOs = VBOs;
    }

    rc.numIndices = gd.numIndices;

    glBindVertexArray(rc.VAO);

    glBindBuffer(GL_ARRAY_BUFFER, rc.VBOs[0]);
    glBufferSubData(GL_ARRAY_BUFFER, 0, gd.vboSize(), gd.vertices);
    glBindBuffer(GL_ARRAY_BUFFER, 0);

    glBindBuffer(GL_ELEMENT_ARRAY_BUFFER, rc.VBOs[1]);
    glBufferSubData(GL_ELEMENT_ARRAY_BUFFER, 0, gd.iboSize(), gd.indices);
    glBindBuffer(GL_ELEMENT_ARRAY_BUFFER, 0);

    glBindVertexArray(0);

    if (index >= 0 && index < chunks.size())
    {
        chunks[index] = rc;
    }
    else
    {
        chunks.push_back(rc);
    }
}

And the struct ChunkMesh used, where I believe the problem is:

struct ChunkMesh
{
    std::vector<Vertex> vertices;
    std::vector<GLushort> indices;

    int addVertex(Vertex v)
    {
        //add a vertex to the mesh, and return its index in the list.
        vertices.push_back(v);
        return vertices.size() - 1;
    }

    void addFace(int v0, int v1, int v2)
    {
        //construct a face with 3 vertices.
        indices.push_back(v0);
        indices.push_back(v1);
        indices.push_back(v2);
    }
};

I believe the problem is in the ChunkMesh struct, with the push_backs used. The std::vector is very slow for hundreds of push_backs, but I cannot find an alternative. What can I replace the vector with?

Am I going about rendering the chunks entirely wrong? How can I optimise this function?

Any help would be much appreciated.

Thanks.

Edit: I have tried reserving the vectors, which, to my confusion had no effect on the performance. It remains at 0.36 seconds.

I added a constructor to ChunkMesh to take in the number of blocks, like so:

ChunkMesh(int numBlocks)
{
    vertices.reserve(numBlocks * 8); //8 vertices per cube
    indices.reserve(numBlocks * 36); //36 indices per cube
}
1

There are 1 best solutions below

2
On

My recommendation would be to evaluate if you need the vertices which are not at the surface of the chunk.

If not, you do not need to add them to your ChunkMesh, which reduces the number of the vertices and push_back calls noticeable.