Search code examples
pythonopengl3dshaderpyopengl

Draw Multiple Objects With PyOpenGL Shaders


I just started using shaders with PyOpenGL, and I managed to draw one 3d object, but I'm not able to draw a second one. I tried making another init() function and doing all the vao and vbo stuff twice, and then between drawing objects I switched to using the second shader program, but I can't draw a second object.

from OpenGL.GL import *
from OpenGL.GL.shaders import compileProgram, compileShader
import numpy as np
from math import sin, cos, tan, radians

# Vertex Shader
vertex_shader = """
#version 330
in vec3 position;
in vec3 color;
out vec3 newColor;
uniform mat4 model;
uniform mat4 view;
uniform mat4 projection;

void main()
{
    gl_Position = projection * view * model * vec4(position, 1.0);
    newColor = color;
}
"""

# Fragment Shader
fragment_shader = """
#version 330
in vec3 newColor;
out vec4 outColor;

void main()
{
    outColor = vec4(newColor, 1.0);
}
"""

# Cube vertices and colors
vertices = np.array([
    # Vertices        Colors
    [-.1, -.1, -.1], [1, 0, 0],
    [.1, -.1, -.1], [0, 1, 0],
    [.1, .1, -.1], [0, 0, 1],
    [-.1, .1, -.1], [1, 1, 0],
    [-.1, -.1, .1], [0, 1, 1],
    [.1, -.1, .1], [1, 1, 1],
    [.1, .1, .1], [0.5, 0.5, 0.5],
    [-.1, .1, .1], [0.5, 0.5, 0.5],
], dtype=np.float32)

indices = np.array([
    0, 1, 2, 2, 3, 0,  # Front face
    1, 5, 6, 6, 2, 1,  # Right face
    5, 4, 7, 7, 6, 5,  # Back face
    4, 0, 3, 3, 7, 4,  # Left face
    3, 2, 6, 6, 7, 3,  # Top face
    4, 5, 1, 1, 0, 4,  # Bottom face
], dtype=np.uint32)


# Cube vertices and colors
vertices2 = np.array([
    # Vertices        Colors
    [.2, -.1, -.1], [1, 0, 0],
    [.4, -.1, -.1], [0, 1, 0],
    [.4, .1, -.1], [0, 0, 1],
    [.2, .1, -.1], [1, 1, 0],
    [.2, -.1, .1], [0, 1, 1],
    [.4, -.1, .1], [1, 1, 1],
    [.4, .1, .1], [0.5, 0.5, 0.5],
    [.2, .1, .1], [0.5, 0.5, 0.5],
], dtype=np.float32)

indices2 = np.array([
    0, 1, 2, 2, 3, 0,  # Front face
    1, 5, 6, 6, 2, 1,  # Right face
    5, 4, 7, 7, 6, 5,  # Back face
    4, 0, 3, 3, 7, 4,  # Left face
    3, 2, 6, 6, 7, 3,  # Top face
    4, 5, 1, 1, 0, 4,  # Bottom face
], dtype=np.uint32)


def init1():
    # Compile shaders and create shader program
    shader_program = compileProgram(compileShader(vertex_shader, GL_VERTEX_SHADER),
                                    compileShader(fragment_shader, GL_FRAGMENT_SHADER))

    # Create Vertex Array Object (VAO)
    vao = glGenVertexArrays(1)
    glBindVertexArray(vao)

    # Create Vertex Buffer Object (VBO) and Element Buffer Object (EBO)
    vbo = glGenBuffers(1)
    glBindBuffer(GL_ARRAY_BUFFER, vbo)
    glBufferData(GL_ARRAY_BUFFER, vertices.nbytes, vertices, GL_STATIC_DRAW)

    ebo = glGenBuffers(1)
    glBindBuffer(GL_ELEMENT_ARRAY_BUFFER, ebo)
    glBufferData(GL_ELEMENT_ARRAY_BUFFER, indices.nbytes, indices, GL_STATIC_DRAW)

    # Set attribute pointers
    stride = 6 * vertices.itemsize
    position_offset = ctypes.c_void_p(0)
    color_offset = ctypes.c_void_p(3 * vertices.itemsize)
    glVertexAttribPointer(0, 3, GL_FLOAT, GL_FALSE, stride, position_offset)
    glVertexAttribPointer(1, 3, GL_FLOAT, GL_FALSE, stride, color_offset)
    glEnableVertexAttribArray(0)
    glEnableVertexAttribArray(1)

    return shader_program

def init2():
    # Compile shaders and create shader program
    shader_program2 = compileProgram(compileShader(vertex_shader, GL_VERTEX_SHADER),
                                    compileShader(fragment_shader, GL_FRAGMENT_SHADER))

    # Create Vertex Array Object (VAO)
    vao = glGenVertexArrays(1)
    glBindVertexArray(vao)

    # Create Vertex Buffer Object (VBO) and Element Buffer Object (EBO)
    vbo = glGenBuffers(1)
    glBindBuffer(GL_ARRAY_BUFFER, vbo)
    glBufferData(GL_ARRAY_BUFFER, vertices2.nbytes, vertices2, GL_STATIC_DRAW)

    ebo = glGenBuffers(1)
    glBindBuffer(GL_ELEMENT_ARRAY_BUFFER, ebo)
    glBufferData(GL_ELEMENT_ARRAY_BUFFER, indices2.nbytes, indices2, GL_STATIC_DRAW)

    # Set attribute pointers
    stride = 6 * vertices.itemsize
    position_offset = ctypes.c_void_p(0)
    color_offset = ctypes.c_void_p(3 * vertices2.itemsize)
    glVertexAttribPointer(0, 3, GL_FLOAT, GL_FALSE, stride, position_offset)
    glVertexAttribPointer(1, 3, GL_FLOAT, GL_FALSE, stride, color_offset)
    glEnableVertexAttribArray(0)
    glEnableVertexAttribArray(1)

    return shader_program2

def main():
    # Initialize Pygame
    pygame.init()
    width, height = 800, 600
    pygame.display.set_mode((width, height), pygame.OPENGL | pygame.DOUBLEBUF)
    pygame.display.set_caption("Shader Testing")
    glViewport(0, 0, width, height)

    width, height = 800, 600
    shader_program = init1()
    shader_program2 = init2()

    running = True
    while running:
        for event in pygame.event.get():
            if event.type == pygame.QUIT:
                running = False

        glClearColor(0.0, 0.0, 0.0, 0.0)
        glClear(GL_COLOR_BUFFER_BIT | GL_DEPTH_BUFFER_BIT)

        # Define the view and projection matrices (unchanged)
        view = [1.0, 0.0, 0.0, 0.0,
                0.0, 1.0, 0.0, 0.0,
                0.0, 0.0, 1.0, 0.0,
                0.0, 0.0, -3.0, 1.0]

        aspect_ratio = width / height
        near = 0.1
        far = 100.0
        fov = 45.0

        top = near * tan(radians(fov) / 2.0)
        right = top * aspect_ratio

        projection = ([near / right, 0.0, 0.0, 0.0],
                      [0.0, near / top, 0.0, 0.0],
                      [0.0, 0.0, -(far + near) / (far - near), -1.0],
                      [0.0, 0.0, -(2 * far * near) / (far - near), 0.0])

        # Define the model matrix (rotate the cube)
        angle = pygame.time.get_ticks() / 1000.0  # Rotate the cube over time
        model = ([cos(angle), -sin(angle), 0, 0],
                 [sin(angle), cos(angle), 0, 0],
                 [0, 0, 1, 0],
                 [0, 0, 0, 1])

        # Use the shader program
        glUseProgram(shader_program)

        # Set the model, view, and projection matrices in the shader
        model_loc = glGetUniformLocation(shader_program, "model")
        view_loc = glGetUniformLocation(shader_program, "view")
        projection_loc = glGetUniformLocation(shader_program, "projection")
        glUniformMatrix4fv(model_loc, 1, GL_FALSE, model)
        glUniformMatrix4fv(view_loc, 1, GL_FALSE, view)
        glUniformMatrix4fv(projection_loc, 1, GL_FALSE, projection)

        # Draw the cube
        glDrawElements(GL_TRIANGLES, len(indices), GL_UNSIGNED_INT, None)

        # Use the shader program
        glUseProgram(shader_program2)

        # Set the model, view, and projection matrices in the shader
        model_loc = glGetUniformLocation(shader_program, "model")
        view_loc = glGetUniformLocation(shader_program, "view")
        projection_loc = glGetUniformLocation(shader_program, "projection")
        glUniformMatrix4fv(model_loc, 1, GL_FALSE, model)
        glUniformMatrix4fv(view_loc, 1, GL_FALSE, view)
        glUniformMatrix4fv(projection_loc, 1, GL_FALSE, projection)

        # Draw the cube
        glDrawElements(GL_TRIANGLES, len(indices2), GL_UNSIGNED_INT, None)

        pygame.display.flip()

    pygame.quit()


if __name__ == "__main__":
    main()```

Solution

  • The vertex array object and the shader program are not tied. You must bind the vertex array object before drawing the mesh. OpenGL is a state engine. The currently bound vertex array object is a global state and the draw call (glDraw*) uses the specification stored in the currently bound vertex array object to draw the mesh. Also note that it is absolutely pointless to compile and link the same shader twice. You can use the same shader program to draw multiple meshes. All you have to do is set the individual uniforms before the drawing call. Normally, you have an individual model matrix for each mesh:

    def init1():
        # [...]
    
        return vao
    
    def init2():
        # [...]
    
        return vao
    
    def main():
        # [...]
    
        shader_program = compileProgram(compileShader(vertex_shader, GL_VERTEX_SHADER),
                                        compileShader(fragment_shader, GL_FRAGMENT_SHADER))
        model_loc = glGetUniformLocation(shader_program, "model")
        view_loc = glGetUniformLocation(shader_program, "view")
        projection_loc = glGetUniformLocation(shader_program, "projection")
        vao = init1()
        vao2 = init2()
    
        running = True
        while running:
            # [...]
    
            glUseProgram(shader_program)
            glUniformMatrix4fv(view_loc, 1, GL_FALSE, view)
            glUniformMatrix4fv(projection_loc, 1, GL_FALSE, projection)
    
            glUniformMatrix4fv(model_loc, 1, GL_FALSE, model)
            glBindVertexArray(vao)
            glDrawElements(GL_TRIANGLES, len(indices), GL_UNSIGNED_INT, None)
    
            glUniformMatrix4fv(model_loc, 1, GL_FALSE, model2)
            glBindVertexArray(vao2)
            glDrawElements(GL_TRIANGLES, len(indices2), GL_UNSIGNED_INT, None)
    
            # [...]
    

    In your case, you don't really need 2 different meshes, just different model matrices. See the example based on your code:

    import pygame
    from OpenGL.GL import *
    from OpenGL.GL.shaders import compileProgram, compileShader
    import numpy as np
    from math import sin, cos, tan, radians
    
    # Vertex Shader
    vertex_shader = """
    #version 330
    in vec3 position;
    in vec3 color;
    out vec3 newColor;
    uniform mat4 model;
    uniform mat4 view;
    uniform mat4 projection;
    
    void main()
    {
        gl_Position = projection * view * model * vec4(position, 1.0);
        newColor = color;
    }
    """
    
    # Fragment Shader
    fragment_shader = """
    #version 330
    in vec3 newColor;
    out vec4 outColor;
    
    void main()
    {
        outColor = vec4(newColor, 1.0);
    }
    """
    
    # Cube vertices and colors
    vertices = np.array([
        # Vertices        Colors
        [-.1, -.1, -.1], [1, 0, 0],
        [.1, -.1, -.1], [0, 1, 0],
        [.1, .1, -.1], [0, 0, 1],
        [-.1, .1, -.1], [1, 1, 0],
        [-.1, -.1, .1], [0, 1, 1],
        [.1, -.1, .1], [1, 1, 1],
        [.1, .1, .1], [0.5, 0.5, 0.5],
        [-.1, .1, .1], [0.5, 0.5, 0.5],
    ], dtype=np.float32)
    
    indices = np.array([
        0, 1, 2, 2, 3, 0,  # Front face
        1, 5, 6, 6, 2, 1,  # Right face
        5, 4, 7, 7, 6, 5,  # Back face
        4, 0, 3, 3, 7, 4,  # Left face
        3, 2, 6, 6, 7, 3,  # Top face
        4, 5, 1, 1, 0, 4,  # Bottom face
    ], dtype=np.uint32)
    
    
    def init():
        # Create Vertex Array Object (VAO)
        vao = glGenVertexArrays(1)
        glBindVertexArray(vao)
    
        # Create Vertex Buffer Object (VBO) and Element Buffer Object (EBO)
        vbo = glGenBuffers(1)
        glBindBuffer(GL_ARRAY_BUFFER, vbo)
        glBufferData(GL_ARRAY_BUFFER, vertices.nbytes, vertices, GL_STATIC_DRAW)
    
        ebo = glGenBuffers(1)
        glBindBuffer(GL_ELEMENT_ARRAY_BUFFER, ebo)
        glBufferData(GL_ELEMENT_ARRAY_BUFFER, indices.nbytes, indices, GL_STATIC_DRAW)
    
        # Set attribute pointers
        stride = 6 * vertices.itemsize
        position_offset = ctypes.c_void_p(0)
        color_offset = ctypes.c_void_p(3 * vertices.itemsize)
        glVertexAttribPointer(0, 3, GL_FLOAT, GL_FALSE, stride, position_offset)
        glVertexAttribPointer(1, 3, GL_FLOAT, GL_FALSE, stride, color_offset)
        glEnableVertexAttribArray(0)
        glEnableVertexAttribArray(1)
    
        return vao
    
    def main():
        # Initialize Pygame
        pygame.init()
        width, height = 800, 600
        pygame.display.set_mode((width, height), pygame.OPENGL | pygame.DOUBLEBUF)
        pygame.display.set_caption("Shader Testing")
        glViewport(0, 0, width, height)
    
        width, height = 800, 600
    
        # Compile shaders and create shader program
        shader_program = compileProgram(compileShader(vertex_shader, GL_VERTEX_SHADER),
                                        compileShader(fragment_shader, GL_FRAGMENT_SHADER))
        model_loc = glGetUniformLocation(shader_program, "model")
        view_loc = glGetUniformLocation(shader_program, "view")
        projection_loc = glGetUniformLocation(shader_program, "projection")
        vao = init()
    
        running = True
        while running:
            for event in pygame.event.get():
                if event.type == pygame.QUIT:
                    running = False
    
            glClearColor(0.0, 0.0, 0.0, 0.0)
            glClear(GL_COLOR_BUFFER_BIT | GL_DEPTH_BUFFER_BIT)
            glEnable(GL_DEPTH_TEST)
    
            # Define the view and projection matrices (unchanged)
            view = [1.0, 0.0, 0.0, 0.0,
                    0.0, 1.0, 0.0, 0.0,
                    0.0, 0.0, 1.0, 0.0,
                    0.0, 0.0, -3.0, 1.0]
    
            aspect_ratio = width / height
            near = 0.1
            far = 100.0
            fov = 45.0
    
            top = near * tan(radians(fov) / 2.0)
            right = top * aspect_ratio
    
            projection = ([near / right, 0.0, 0.0, 0.0],
                          [0.0, near / top, 0.0, 0.0],
                          [0.0, 0.0, -(far + near) / (far - near), -1.0],
                          [0.0, 0.0, -(2 * far * near) / (far - near), 0.0])
    
            # Define the model matrix (rotate the cube)
            angle = pygame.time.get_ticks() / 1000.0  # Rotate the cube over time
            model = ([cos(angle), -sin(angle), 0, 0],
                     [sin(angle), cos(angle), 0, 0],
                     [0, 0, 1, 0],
                     [0, 0, 0, 1])
            model2 = ([cos(angle), -sin(angle), 0, 0],
                     [sin(angle), cos(angle), 0, 0],
                     [0, 0, 1, 0],
                     [0.5, 0, 0, 1])
            model3 = ([cos(angle), -sin(angle), 0, 0],
                     [sin(angle), cos(angle), 0, 0],
                     [0, 0, 1, 0],
                     [-0.5, 0, 0, 1])
    
            # Use the shader program
            glUseProgram(shader_program)
    
            # Set the model, view, and projection matrices in the shader 
            glUniformMatrix4fv(view_loc, 1, GL_FALSE, view)
            glUniformMatrix4fv(projection_loc, 1, GL_FALSE, projection)
    
            glUniformMatrix4fv(model_loc, 1, GL_FALSE, model)
            glBindVertexArray(vao)
            glDrawElements(GL_TRIANGLES, len(indices), GL_UNSIGNED_INT, None)
    
            glUniformMatrix4fv(model_loc, 1, GL_FALSE, model2)
            glBindVertexArray(vao)
            glDrawElements(GL_TRIANGLES, len(indices), GL_UNSIGNED_INT, None)
    
            glUniformMatrix4fv(model_loc, 1, GL_FALSE, model3)
            glBindVertexArray(vao)
            glDrawElements(GL_TRIANGLES, len(indices), GL_UNSIGNED_INT, None)
    
            pygame.display.flip()
    
        pygame.quit()
    
    
    if __name__ == "__main__":
        main()