c++opengltexture-mappingopengl-3voxel

Using Texture Atlas as Texture Array in OpenGL


I'm now building a Voxel game. In the beginning, I use a texture atlas that stores all voxel textures and it works fine. After that, I decided to use Greedy Meshing in my game, thus texture atlas is not useful anymore. I read some articles which said that should use Texture Array instead. Then I tried to read and use the texture array technique for texturing. However, the result I got was all black in my game. So what am I missing?

This is my texture atlas (600 x 600)

Here is my Texture2DArray, I use this class to read and save a texture array

Texture2DArray::Texture2DArray() : Internal_Format(GL_RGBA8), Image_Format(GL_RGBA), Wrap_S(GL_REPEAT), Wrap_T(GL_REPEAT), Wrap_R(GL_REPEAT), Filter_Min(GL_NEAREST), Filter_Max(GL_NEAREST), Width(0), Height(0)
{
   glGenTextures(1, &this->ID);
}

void Texture2DArray::Generate(GLuint width, GLuint height, unsigned char* data)
{
   this->Width = width;
   this->Height = height;

   glBindTexture(GL_TEXTURE_2D_ARRAY, this->ID);

   // I cannot decide what the texture array layer (depth) should be (I put here is 1 for layer number)
   //Can anyone explain to me how to decide the texture layer here? 
   glTexImage3D(GL_TEXTURE_2D_ARRAY, 1, this->Internal_Format, this->Width, this->Height, 0, 1 , this->Image_Format, GL_UNSIGNED_BYTE, data);

   glTexParameteri(GL_TEXTURE_2D_ARRAY, GL_TEXTURE_WRAP_S, this->Wrap_S);
   glTexParameteri(GL_TEXTURE_2D_ARRAY, GL_TEXTURE_WRAP_T, this->Wrap_T);
   glTexParameteri(GL_TEXTURE_2D_ARRAY, GL_TEXTURE_WRAP_R, this->Wrap_R);

   glTexParameteri(GL_TEXTURE_2D_ARRAY, GL_TEXTURE_MIN_FILTER, this->Filter_Min);
   glTexParameteri(GL_TEXTURE_2D_ARRAY, GL_TEXTURE_MIN_FILTER, this->Filter_Max);
   
   //unbind this texture for another creating texture
   glBindTexture(GL_TEXTURE_2D_ARRAY, 0);
}

void Texture2DArray::Bind() const
{
   glBindTexture(GL_TEXTURE_2D_ARRAY, this->ID);
}

Here is my Fragment Shader

#version 330 core

uniform sampler2DArray ourTexture;

in vec2 texCoord;

out vec4 FragColor;

void main(){
   // 1 (the layer number) just for testing
   FragColor = texture(ourTexture,vec3(texCoord, 1));
}

Here is my Vertex Shader

#version 330 core

layout (location = 0) in vec3 inPos;
layout (location = 1) in vec2 inTexCoord;

out vec2 texCoord;

uniform mat4 model;
uniform mat4 view;
uniform mat4 projection;

void main(){
    gl_Position =  projection * view * vec4(inPos,1.0f);
    texCoord = inTexCoord;
}  

This my rendering result

EDIT 1:

I figured out that texture atlas doesn't work with texture array because it is a grid so OpenGl cannot decide where it should begin. So I create a vertical texture (18 x 72) and try again but it still all black everywhere.

I have checked binding the texture before using it.


Solution

  • When the 3 dimensional texture image is specified, then the depth has to be the number of images which have to be stored in the array (e.g. imageCount). The width and the height parameter represent the width and height of 1 tile (e.g. tileW, tileH). The layer should be 0 and the border parameter has to be 0. See glTexImage3D. glTexImage3D creates the data store for the texture image. The memory which is required for the textures is reserved (GPU). It is possible to pass a pointer to the image data, but it is not necessary.

    If all the tiles are stored in a vertical atlas, then the image data can be set directly:

    glTexImage3D(GL_TEXTURE_2D_ARRAY, 0, this->Internal_Format, 
                 tileW, tileH, imageCount, 0,
                 this->Image_Format, GL_UNSIGNED_BYTE, data);
    

    If the tiles are in the 16x16 atlas, then the tiles have to by extracted from the texture atlas and to set each subimage in the texture array. (data[i] is the imaged data of one tile). Create the texture image:

    glTexImage3D(GL_TEXTURE_2D_ARRAY, 0, this->Internal_Format, 
                 tileW, tileH, imageCount, 0,
                 this->Image_Format, GL_UNSIGNED_BYTE, nullptr);
    

    After that use glTexSubImage3D to put the texture data to the data store of the texture object. glTexSubImage3D uses the existing data store and copies data. e.g.:

    for (int i = 0; i < imageCount; ++i)
    {
        glTexSubImage3D(GL_TEXTURE_2D_ARRAY, 0,
            0, 0, i,
            tileW, tileH, 1,
            this->Image_Format, GL_UNSIGNED_BYTE, data[i]);
    }
    

    Note, you've to extract the tiles from the texture atlas and to set each subimage in the texture array. (data[i] is the imaged data of one tile)


    An algorithm to extract the tiles and specify the texture image may look as follows

    #include <algorithm>    // std::copy
    #include <vector>       // std::vector
    
    unsigned char* data = ...; // 16x16 texture atlas image data
    int tileW = ...;           // number of pixels in a row of 1 tile
    int tileH = ...;           // number of pixels in a column of 1 tile
    int channels = 4;          // 4 for RGBA
    
    int tilesX = 16;
    int tilesY = 16;
    int imageCount = tilesX * tilesY;
    
    glTexImage3D(GL_TEXTURE_2D_ARRAY, 0, this->Internal_Format, 
                 tileW, tileH, imageCount, 0,
                 this->Image_Format, GL_UNSIGNED_BYTE, nullptr);
    
    std::vector<unsigned char> tile(tileW * tileH * channels);
    int tileSizeX = tileW * channels;
    int rowLen    = tilesX * tileSizeX;
    
    for (int iy = 0; iy < tilesY; ++ iy)
    {
        for (int ix = 0; ix < tilesX; ++ ix)
        {
            unsigned char *ptr = data + iy*rowLen + ix*tileSizeX;
            for (int row = 0; row < tileH; ++ row)
                std::copy(ptr + row*rowLen, ptr + row*rowLen + tileSizeX,
                          tile.begin() + row*tileSizeX);
    
    
            int i = iy * tilesX + ix;
            glTexSubImage3D(GL_TEXTURE_2D_ARRAY, 0,
                0, 0, i,
                tileW, tileH, 1,
                this->Image_Format, GL_UNSIGNED_BYTE, tile.data());
        }
    }