Search code examples
c++graphicsdirectxvoxel

Mesh generating algorithm for a voxel game


Im currently making a voxel game like Minecraft for fun with DirectX11. Game works with chunk system like any other voxel game, but my current algorithm for generating chunk mesh is not expandable. Block class has a few attributes like is block full and mesh type.

class Block
{
public:
    bool isFull = true;
    MeshType type = MeshType::FullBlock;
    Vector2i texture = { 9, 1 };
    Vector2i topTexture = { 9, 1 };
    const char* sound;

    Block(){}
    Block(bool isFull, MeshType type, Vector2i texture, Vector2i topTexture, const char* sound): isFull(isFull), type(type), texture(texture), topTexture(topTexture), sound(sound){}
    Block(bool isFull, MeshType type, Vector2i texture, const char* sound) : isFull(isFull), type(type), texture(texture), topTexture(texture), sound(sound) {}
    Block(bool isFull, MeshType type, Vector2i texture) : isFull(isFull), type(type), texture(texture), topTexture(texture) {}

};

Every block is then initialised in a vector

    blocks.reserve(64);

    Block air(false, MeshType::Empty, {0 ,0});
    blocks.emplace_back(air);

    Block grass(true, MeshType::FullBlock, { 3, 0 }, { 0, 0 }, "Audio/grass1.ogg");
    blocks.emplace_back(grass);

    Block stone(true, MeshType::FullBlock, { 1, 0 }, "Audio/stone1.ogg");
    blocks.emplace_back(stone);

    Block rose(false, MeshType::Cross, { 12 ,0 }, "Audio/grass1.ogg");
    blocks.emplace_back(rose);

    Block wheat(false, MeshType::Hash, { 8 ,3 });
    blocks.emplace_back(wheat);

    //and so on...

I have a function that accepts a vector of vertices and chunk data. That function loops through all the blocks with a few optimisations and emplaces data back into the vector that gets sent to the buffer.

for (int x = 0; x < ChunkWidth; x++)
    {
        for (int y = 0; y < ChunkHeight; y++)
        {
            for (int z = 0; z < ChunkWidth; z++)
            {
                if (IsDrawable[x][y][z] == 1)
                {
                    switch (blocks[chunk->BlockID[x + 16 * y + 16 * 256 * z]].type)
                    {
                    case MeshType::FullBlock:
                        BuildBlock(chunk, vertices, x, y, z);
                        break;
                    case MeshType::Cross:
                        FillCross(vertices, blocks[chunk->BlockID[x + 16 * y + 16 * 256 * z]], x + chunk->x * ChunkWidth, y, z + chunk->z * ChunkWidth);
                        break;
                    case MeshType::Hash:
                        FillHash(vertices, blocks[chunk->BlockID[x + 16 * y + 16 * 256 * z]], x + chunk->x * ChunkWidth, y, z + chunk->z * ChunkWidth);
                        break;
                    }
                }
            }
        }
    }

With every new mesh type the switch statement gets bigger and I think that is not the way to go. Im asking if there are better ways of doing this. I thank you in advance.


Solution

  • I think making different derived classes with a common parent class Block is the way to go here. You add a virtual method in it whose behaviour is overridden in the derived classes. Then you place them in a polymorphic vector of std::shared_ptr<Block> and call them. If you are afraid that for some reason this might be too slow you might replace the virtual functions with the Curiously Recurring Template Pattern (CRTP) to achieve static polymorphism. So something like:

    Implementatin of the base class Block: Can stay roughly the same bout you add a virtual method draw(...) which is the common interface for all derived classes:

    class Block {
      public:
        bool isFull = true;
        Vector2i texture = { 9, 1 };
        Vector2i topTexture = { 9, 1 };
        const char* sound;
    
        Block() {
          return;
        }
        Block(bool isFull, Vector2i const& texture, Vector2i const& topTexture, const char* sound)
        : isFull(isFull), texture(texture), topTexture(topTexture), sound(sound) {
          return;
        }
        Block(bool isFull, Vector2i const& texture, const char* sound)
        : isFull(isFull), texture(texture), topTexture(texture), sound(sound) {
          return;
        }
        Block(bool const& isFull, Vector2i const& texture)
        : isFull(isFull), texture(texture), topTexture(texture) {
          return;
        }
    
        // Virtual method that every derived class should override
        // Could contain default behaviour but here I declared it as pure virtual method (therefore the = 0)
        // Didn't know the data types for chunk and vertices so I used Chunk and Vertices respectively
        virtual void draw(Chunk const& chunk, Vertices const& vertices, int x, int y, int z, int chunkWidth) = 0;
    };
    

    The different types of blocks are introduced as derived classes that inherit the constructor (or you might implement a new one as well) and override the behaviour of the draw(...) class. If you are not planning to inherit from this derived class then you can mark it as final or if you won't be overriding draw in a derived class you can mark only draw as final

    class Empty: public Block {
      public:
        using Block::Block; // Use the constructor of the base class
        
        // Overwrite behaviour of the base class here
        void draw(Chunk const& chunk, Vertices const& vertices, int x, int y, int z, int chunkWidth) override {
          return;
        }
     };
    
     class FullBlock: public Block {
      public:
        using Block::Block;
    
        void draw(Chunk const& chunk, Vertices const& vertices, int x, int y, int z, int chunkWidth) override {
          // Move contents of BuildBlock here
          BuildBlock(chunk, vertices, x, y, z);
          return;
        }
     };
    
     class Cross final: public Block {
      public:
        using Block::Block;
    
        void draw(Chunk const& chunk, Vertices const& vertices, int x, int y, int z, int chunkWidth) override {
          // Move contents of FillCross here! No need to pass blocks[i] or rewrite FillCross to take something else than a Block, e.g. a std::shared_ptr<Block>
          FillCross(vertices, *this, x + chunk->x * chunkWidth, y, z + chunk->z * chunkWidth);
          return;
        }
     };
    
     class Hash final: public Block {
      public:
        using Block::Block;
    
        void draw(Chunk const& chunk, Vertices const& vertices, int x, int y, int z, int chunkWidth) override {
          // Same here
          FillHash(vertices, *this, x + chunk->x * chunkWidth, y, z + chunk->z * chunkWidth);
          return;
        }
     };
    

    Then you add all the blocks as std::shared_ptr or better std::unique_ptr if the resources are not shared! (a wrapper for a plain pointer from #include <memory>)

    // Consider using std::unique_ptr if you are not using the individual objects outside of the std::vector
    std::vector<std::shared_ptr<Block>> blocks = {};
    blocks.reserve(64);
    
    auto air = std::make_shared<Empty>(false, {0 ,0});
    blocks.emplace_back(air);
    
    auto grass = std::make_shared<FullBlock>(true, { 3, 0 }, { 0, 0 }, "Audio/grass1.ogg");
    blocks.emplace_back(grass);
    
    auto stone = std::make_shared<FullBlock>(true, { 1, 0 }, "Audio/stone1.ogg");
    blocks.emplace_back(stone);
    
    auto rose = std::make_shared<Cross>(false, { 12 ,0 }, "Audio/grass1.ogg");
    blocks.emplace_back(rose);
    
    auto wheat = std::make_shared<Hash>(false, { 8 ,3 });
    blocks.emplace_back(wheat);
    

    You can call then the implementation of the different derived classes as follows:

    for (int x = 0; x < chunkWidth; x++) {
      for (int y = 0; y < chunkHeight; y++) {
        for (int z = 0; z < chunkWidth; z++) {
          if (IsDrawable[x][y][z] == 1) {
            blocks[chunk->BlockID[x + 16 * y + 16 * 256 * z]]->draw(chunk, vertices, x, y, z, chunkWidth);
          }
        }
      }
    }
    

    Here I put together a simplified working example to play around with in an online compiler.