Search code examples
pythonopenglpygametexturespyopengl

Texture arrays in OpenGL


I am working on a project and I need to use texture arrays to apply textures. I have asked many questions about this, none of which I got an answer I was completely satisfied with (Get access to later versions of GLSL , OpenGL: Access Array Texture in GLSL , and OpenGL: How would I implement texture arrays?) so I'm asking a more broad question to hopefully get a response. Anyways, How would I texture an object in OpenGL (PyOpenGL more specifically, but it's fine if you put your answer in C++). I already have a way to load the texture arrays, just not a way to apply it. This is the desired result:

Example

Image from opengl-tutorial

and this is what I currently have for loading array textures:

def load_texture_array(path,width,height):
    teximg = pygame.image.load(path)
    texels = teximg.get_buffer().raw
    texture = GLuint(0)

    layerCount = 6
    mipLevelCount = 1

    glGenTextures(1, texture)
    glBindTexture(GL_TEXTURE_2D_ARRAY, texture)
    glTexStorage3D(GL_TEXTURE_2D_ARRAY, mipLevelCount, GL_RGBA8, width, height, layerCount)
    glTexSubImage3D(GL_TEXTURE_2D_ARRAY, 0, 0, 0, 0, width, height, layerCount, GL_RGBA, GL_UNSIGNED_BYTE, texels)

    glTexParameteri(GL_TEXTURE_2D_ARRAY, GL_TEXTURE_MIN_FILTER, GL_LINEAR)
    glTexParameteri(GL_TEXTURE_2D_ARRAY, GL_TEXTURE_MAG_FILTER, GL_LINEAR)
    glTexParameteri(GL_TEXTURE_2D_ARRAY, GL_TEXTURE_WRAP_S, GL_CLAMP_TO_EDGE)
    glTexParameteri(GL_TEXTURE_2D_ARRAY, GL_TEXTURE_WRAP_T, GL_CLAMP_TO_EDGE)

TLDR: How would I apply textures to objects in OpenGL using texture arrays?

I will happily provide any other information if necessary.


Solution

  • If you want to use a 2D Array Texture for a cube, each of the 6 textures for the 6 side must be the same size. You can lookup the texture by 3 dimensional texture coordinates. The 3rd component of the texture coordinate is the index of the 2d texture in the 2d texture array.
    Hence the texture coordinates for the 6 sides are

    0:  [(0, 0, 0), (1, 0, 0), (1, 1, 0), (0, 1, 0)]
    1:  [(0, 0, 1), (1, 0, 1), (1, 1, 1), (0, 1, 1)]
    2:  [(0, 0, 2), (1, 0, 2), (1, 1, 2), (0, 1, 2)]
    3:  [(0, 0, 3), (1, 0, 3), (1, 1, 3), (0, 1, 3)]
    4:  [(0, 0, 4), (1, 0, 4), (1, 1, 4), (0, 1, 4)]
    5:  [(0, 0, 5), (1, 0, 5), (1, 1, 5), (0, 1, 5)]
    

    Get the 3 dimensional texture coordinate attribute in the vertex shader and pass it to the fragment shader:

    in a_uv;
    out v_uv;
    
    // [...]
    
    void main()
    {
        v_uv = a_uv;
    
        // [...]
    }
    

    Use the 3 dimensional texture coordinate to look up the sampler2DArray in the fragment shader:

    out v_uv;
    uniform sampler2DArray u_texture;
    
    // [...]
    
    void main()
    {
        vec4 texture(u_texture, v_uv.xyz);
    
        // [...]
    }
    

    Create a GL_TEXTURE_2D_ARRAY and use glTexSubImage3D to load 6 2-dimensional images to the 6 planes of the 2D Array Texture. In the following image_planes is a list with the 6 2-dimensional image planes:

    tex_obj = glGenTextures(1)
    glBindTexture(GL_TEXTURE_2D_ARRAY, self.tex_obj)
    glTexImage3D(GL_TEXTURE_2D_ARRAY, 0, GL_RGBA, sizeX, sizeY, 6, 0, GL_RGBA, GL_UNSIGNED_BYTE, None)
    for i in range(6):
        glTexSubImage3D(GL_TEXTURE_2D_ARRAY, 0, 0, 0, i, sizeX, sizeY, 1, GL_RGBA, GL_UNSIGNED_BYTE, image_planes[i])
    glTexParameterf(GL_TEXTURE_2D_ARRAY, GL_TEXTURE_MAG_FILTER, GL_LINEAR)
    glTexParameterf(GL_TEXTURE_2D_ARRAY, GL_TEXTURE_MIN_FILTER, GL_LINEAR)
    

    See also PyGame and OpenGL 4.


    Minimal example:

    import os, math, ctypes
    import glm
    from OpenGL.GL import *
    from OpenGL.GL.shaders import *
    from OpenGL.arrays import *
    import pygame
    
    pygame.init()
    
    image_path = r"images"
    image_names = ["banana64.png", "apple64.png", "fish64.png", "rocket64.png", "ice64.png", "boomerang64.png"]
    
    image_planes = [
        (GLubyte * 4)(255, 0, 0, 255), (GLubyte * 4)(0, 255, 0, 255), (GLubyte * 4)(0, 0, 255, 255),
        (GLubyte * 4)(255, 255, 0, 255), (GLubyte * 4)(0, 255, 255, 255), (GLubyte * 4)(255, 0, 255, 255)]
    image_size = (1, 1)
    
    for i, filename in enumerate(image_names):
        try:
            image = pygame.image.load(os.path.join(image_path, filename))
            image_size = image.get_size()
            image_planes[i] = pygame.image.tostring(image, 'RGBA')
        except:
            pass
    
    class MyWindow:
    
        __glsl_vert = """
            #version 130
    
            in vec3 a_pos;
            in vec3 a_nv;
            in vec3 a_uv;
    
            out vec3 v_pos;
            out vec3 v_nv;
            out vec3 v_uv;
    
            uniform mat4 u_proj;
            uniform mat4 u_view;
            uniform mat4 u_model;
    
            void main()
            {
                mat4 model_view = u_view * u_model;
                mat3 normal     = mat3(model_view);
    
                vec4 view_pos   = model_view * vec4(a_pos.xyz, 1.0);
    
                v_pos       = view_pos.xyz;
                v_nv        = normal * a_nv;  
                v_uv        = a_uv;  
                gl_Position = u_proj * view_pos;
            }
        """
    
        __glsl_frag = """
            #version 130
    
            out vec4 frag_color;
            in  vec3 v_pos;
            in  vec3 v_nv;
            in  vec3 v_uv;
    
            uniform sampler2DArray u_texture;
    
            void main()
            {
                vec3  N     = normalize(v_nv);
                vec3  V     = -normalize(v_pos);
                float ka    = 0.1;
                float kd    = max(0.0, dot(N, V)) * 0.9;
                vec4  color = texture(u_texture, v_uv.xyz);
                frag_color  = vec4(color.rgb * (ka + kd), color.a);
            }
        """
    
        def __init__(self, w, h):
            self.__caption = 'OpenGL Window'
            self.__vp_size = [w, h]
    
            pygame.display.gl_set_attribute(pygame.GL_DEPTH_SIZE, 24)  
            self.__screen = pygame.display.set_mode(self.__vp_size, pygame.DOUBLEBUF| pygame.OPENGL)
            
            self.__program = compileProgram( 
                compileShader( self.__glsl_vert, GL_VERTEX_SHADER ),
                compileShader( self.__glsl_frag, GL_FRAGMENT_SHADER ),
            )
            self.___attrib = { a : glGetAttribLocation (self.__program, a) for a in ['a_pos', 'a_nv', 'a_uv'] }
            print(self.___attrib)
            self.___uniform = { u : glGetUniformLocation (self.__program, u) for u in ['u_model', 'u_view', 'u_proj'] }
            print(self.___uniform)
    
            v = [[-1,-1,1], [1,-1,1], [1,1,1], [-1,1,1], [-1,-1,-1], [1,-1,-1], [1,1,-1], [-1,1,-1]]
            n = [[0,0,1], [1,0,0], [0,0,-1], [-1,0,0], [0,1,0], [0,-1,0]]
            e = [[0,1,2,3], [1,5,6,2], [5,4,7,6], [4,0,3,7], [3,2,6,7], [1,0,4,5]]
            t = [[0, 0], [1, 0], [1, 1], [0, 1]]
            index_array = [si*4+[0, 1, 2, 0, 2, 3][vi] for si in range(6) for vi in range(6)]
            attr_array = []
            for si in range(len(e)):
                for i, vi in enumerate(e[si]):
                    attr_array += [*v[vi], *n[si], *t[i], si]
    
            self.__no_vert = len(attr_array) // 10
            self.__no_indices = len(index_array)
            vertex_attributes = (ctypes.c_float * len(attr_array))(*attr_array)
            indices = (ctypes.c_uint32 * self.__no_indices)(*index_array)
    
            self.__vao = glGenVertexArrays(1)
            self.__vbo, self.__ibo = glGenBuffers(2)
    
            glBindVertexArray(self.__vao)
            glBindBuffer(GL_ELEMENT_ARRAY_BUFFER, self.__ibo)
            glBufferData(GL_ELEMENT_ARRAY_BUFFER, indices, GL_STATIC_DRAW)
            glBindBuffer(GL_ARRAY_BUFFER, self.__vbo)
            glBufferData(GL_ARRAY_BUFFER, vertex_attributes, GL_STATIC_DRAW)
    
            float_size = ctypes.sizeof(ctypes.c_float)   
            glVertexAttribPointer(self.___attrib['a_pos'], 3, GL_FLOAT, False, 9*float_size, None)
            glVertexAttribPointer(self.___attrib['a_nv'], 3, GL_FLOAT, False, 9*float_size, ctypes.c_void_p(3*float_size))
            glVertexAttribPointer(self.___attrib['a_uv'], 3, GL_FLOAT, False, 9*float_size, ctypes.c_void_p(6*float_size))
            glEnableVertexAttribArray(self.___attrib['a_pos'])
            glEnableVertexAttribArray(self.___attrib['a_nv'])
            glEnableVertexAttribArray(self.___attrib['a_uv'])
    
            glEnable(GL_DEPTH_TEST)
            glUseProgram(self.__program)
    
            glActiveTexture(GL_TEXTURE0)
            sizeX, sizeY = image_size
            self.tex_obj = glGenTextures(1)
            glBindTexture(GL_TEXTURE_2D_ARRAY, self.tex_obj)
            glTexImage3D(GL_TEXTURE_2D_ARRAY, 0, GL_RGBA, sizeX, sizeY, 6, 0, GL_RGBA, GL_UNSIGNED_BYTE, None)
            for i in range(6):
                glTexSubImage3D(GL_TEXTURE_2D_ARRAY, 0, 0, 0, i, sizeX, sizeY, 1, GL_RGBA, GL_UNSIGNED_BYTE, image_planes[i])
            glTexParameterf(GL_TEXTURE_2D_ARRAY, GL_TEXTURE_MAG_FILTER, GL_LINEAR)
            glTexParameterf(GL_TEXTURE_2D_ARRAY, GL_TEXTURE_MIN_FILTER, GL_LINEAR)
    
        def run(self):
            self.__starttime = 0
            self.__starttime = self.elapsed_ms()
            
            run = True
            while run:
                for event in pygame.event.get():
                    if event.type == pygame.QUIT:
                        run = False
                self.__mainloop()
                pygame.display.flip()
    
            pygame.quit()
    
        def elapsed_ms(self):
          return pygame.time.get_ticks() - self.__starttime
    
        def __mainloop(self):
    
            proj, view, model  = glm.mat4(1), glm.mat4(1), glm.mat4(1)
            aspect = self.__vp_size[0]/self.__vp_size[1]
            proj = glm.perspective(glm.radians(90.0), aspect, 0.1, 10.0)
            view = glm.lookAt(glm.vec3(0,-3,0), glm.vec3(0, 0, 0), glm.vec3(0,0,1))
            angle1 = self.elapsed_ms() * math.pi * 2 / 5000.0
            angle2 = self.elapsed_ms() * math.pi * 2 / 7333.0
            model = glm.rotate(model, angle1, glm.vec3(1, 0, 0))
            model = glm.rotate(model, angle2, glm.vec3(0, 1, 0))
    
            glUniformMatrix4fv(self.___uniform['u_proj'], 1, GL_FALSE, glm.value_ptr(proj) )
            glUniformMatrix4fv(self.___uniform['u_view'], 1, GL_FALSE, glm.value_ptr(view) )
            glUniformMatrix4fv(self.___uniform['u_model'], 1, GL_FALSE, glm.value_ptr(model) )
    
            glClearColor(0.2, 0.3, 0.3, 1.0)
            glClear(GL_COLOR_BUFFER_BIT | GL_DEPTH_BUFFER_BIT)
    
            glDrawElements(GL_TRIANGLES, self.__no_indices, GL_UNSIGNED_INT, None)
    
    window = MyWindow(800, 600)
    window.run()