pythonopenglpyopengl

Managing multiple OpenGL VBO and VAO instances in Python


I need some help with the code found below, where I try to load an OBJ using Impasse (a fork of PyAssimp) that may contain multiple meshes. In order to do that and help myself with debugging I am storing all the data in a dictionary structure, which allows me to grab the vertex, color and face data in an easy way.

Both code snippets are taken from Mario Rosasco's tutorial series on PyOpenGL with the first one being heavily modified to support PyGame, ImGUI (I have removed all the windows since it's not useful for my question), loading from an OBJ file and supporting multiple meshes. The initial code uses a single VAO (with the respective VBO that mixes vertex and color data) and IBO and it's working. Upon request I can provide it.

Once I started adding multiple buffer and array objects, I got myself in a pickle and now I cannot render anything but the black background color of my scene.

The data structure I am using is quite simple - a dictionary with 4 keys:

The first three have the same element structure, namely:

{
  'buff_id' : <BUFFER OBJECT REFERENCE>,
  'data' : {
    'values' : <BUFFER OBJECT DATA (flattened)>,
    'count' : <NUMBER OF ELEMENTS IN BUFFER OBJECT DATA>,
    'stride' : <DIMENSION OF A SINGLE DATUM IN A BUFFER OBJECT DATA>
   }
}

I assume that a multi-mesh OBJ (I use Blender for the creation of my files) has a separate vertex etc. data per mesh and that every mesh is defined as a g component. In addition I use triangulation when exporting (so the stride is currently not really needed) so all my faces are primitives.

I am almost 100% sure that I have missed a binding/unbinding somewhere but I cannot find the spot. Or perhaps there is a different and more fundamental issue with my code.

import sys
from pathlib import Path
import shutil

import numpy as np

from OpenGL.GLU import *
from OpenGL.GL import *

import pygame

import glm
from utils import *
from math import tan, cos, sin, sqrt
from ctypes import c_void_p

from imgui.integrations.pygame import PygameRenderer
import imgui

import impasse
from impasse.constants import ProcessingPreset, MaterialPropertyKey
from impasse.structs import Scene, Camera, Node
from impasse.helper import get_bounding_box

scene_box = None

def loadObjs(f_path: Path):
    if not f_path:
        raise TypeError('f_path not of type Path')
    if not f_path.exists():
        raise ValueError('f_path not a valid filesystem path')
    if not f_path.is_file():
        raise ValueError('f_path not a file')
    if not f_path.name.endswith('.obj'):
        raise ValueError('f_path not an OBJ file')
    
    mtl_pref = 'usemtl '
    mltlib_found = False
    mtl_names = []
    obj_parent_dir = f_path.parent.absolute()
    obj_raw = None
    with open(f_path, 'r') as f_obj:
        obj_raw = f_obj.readlines()
        for line in obj_raw:
            if line == '#':
                continue
            elif line.startswith('mtllib'):
                mltlib_found = True
            elif mtl_pref in line:
                mtl_name = line.replace(mtl_pref, '')
                print('Found material "{}" in OBJ file'.format(mtl_name))
                mtl_names.append(mtl_name)

    args = {}
    args['processing'] = postprocess = ProcessingPreset.TargetRealtime_Fast
    scene = impasse.load(str(f_path), **args).copy_mutable()
    scene_box = (bb_min, bb_max) = get_bounding_box(scene)

    print(len(scene.meshes))

    return scene

def createBuffers(scene: impasse.core.Scene):
    vbos = []
    cbos = []
    ibos = []
    vaos = []

    for mesh in scene.meshes:
        print('Processing mesh "{}"'.format(mesh.name))
        color_rnd = np.array([*np.random.uniform(0.0, 1.0, 3), 1.0], dtype='float32')

        vbo = glGenBuffers(1)
        vertices = np.array(mesh.vertices)
        vbos.append({
            'buff_id' : vbo,
            'data' : {
                'values' : vertices.flatten(),
                'count' : len(vertices),
                'stride' : len(vertices[0])
            }
        })
        glBindBuffer(GL_ARRAY_BUFFER, vbos[-1]['buff_id'])
        glBufferData(
            # PyOpenGL allows for the omission of the size parameter
            GL_ARRAY_BUFFER,
            vertices,
            GL_STATIC_DRAW
        )
        glBindBuffer(GL_ARRAY_BUFFER, 0)

        cbo = glGenBuffers(1)
        print('Random color: {}'.format(color_rnd))
        colors = np.full((len(vertices), 4), color_rnd)
        cbos.append({
            'buff_id' : cbo,
            'data' : {
                'values' : colors.flatten(),
                'count' : len(colors),
                'stride' : len(colors[0])
            }
        })
        glBindBuffer(GL_ARRAY_BUFFER, cbos[-1]['buff_id'])
        glBufferData(
            GL_ARRAY_BUFFER,
            colors,
            GL_STATIC_DRAW
        )
        glBindBuffer(GL_ARRAY_BUFFER, 0)
        
        ibo = glGenBuffers(1)
        indices = np.array(mesh.faces)
        ibos.append({
            'buff_id' : ibo,
            'data' : {
                'values' : indices,
                'count' : len(indices),
                'stride' : len(indices[0])
            }
        })
        glBindBuffer(GL_ELEMENT_ARRAY_BUFFER, ibos[-1]['buff_id'])
        glBufferData(
            GL_ELEMENT_ARRAY_BUFFER,
            indices,
            GL_STATIC_DRAW
        )
        glBindBuffer(GL_ELEMENT_ARRAY_BUFFER, 0)

    # Generate the VAOs. Technically, for same VBO data, a single VAO would suffice
    for mesh_idx in range(len(ibos)):
        vao = glGenVertexArrays(1)
        vaos.append(vao)

        glBindVertexArray(vaos[-1])
        
        vertex_dim = vbos[mesh_idx]['data']['stride']
        print('Mesh vertex dim: {}'.format(vertex_dim))
        glBindBuffer(GL_ARRAY_BUFFER, vbos[mesh_idx]['buff_id'])
        glEnableVertexAttribArray(0)
        glVertexAttribPointer(0, vertex_dim, GL_FLOAT, GL_FALSE, 0, None)

        color_dim = cbos[mesh_idx]['data']['stride']
        print('Mesh color dim: {}'.format(color_dim))
        glBindBuffer(GL_ARRAY_BUFFER, cbos[mesh_idx]['buff_id'])
        glEnableVertexAttribArray(1)
        glVertexAttribPointer(1, color_dim, GL_FLOAT, GL_FALSE, 0, None)
        
        glBindBuffer(GL_ELEMENT_ARRAY_BUFFER, ibos[mesh_idx]['buff_id'])
        
        glBindVertexArray(0)

    return {
        'vbos' : vbos,
        'cbos' : cbos,
        'ibos' : ibos,
        'vaos' : vaos
        }

wireframe_enabled = False
transform_selected = 0

# Helper function to calculate the frustum scale. 
# Accepts a field of view (in degrees) and returns the scale factor
def calcFrustumScale(fFovDeg):
    degToRad = 3.14159 * 2.0 / 360.0
    fFovRad = fFovDeg * degToRad
    return 1.0 / tan(fFovRad / 2.0)

# Global variable to represent the compiled shader program, written in GLSL
program = None

# Global variables to store the location of the shader's uniform variables
modelToCameraMatrixUnif = None
cameraToClipMatrixUnif = None

# Global display variables
cameraToClipMatrix = np.zeros((4,4), dtype='float32')
fFrustumScale = calcFrustumScale(45.0)

# Set up the list of shaders, and call functions to compile them
def initializeProgram():
    shaderList = []
    
    shaderList.append(loadShader(GL_VERTEX_SHADER, "PosColorLocalTransform.vert"))
    shaderList.append(loadShader(GL_FRAGMENT_SHADER, "ColorPassthrough.frag"))
    
    global program 
    program = createProgram(shaderList)
    
    for shader in shaderList:
        glDeleteShader(shader)
    
    global modelToCameraMatrixUnif, cameraToClipMatrixUnif
    modelToCameraMatrixUnif = glGetUniformLocation(program, "modelToCameraMatrix")
    cameraToClipMatrixUnif = glGetUniformLocation(program, "cameraToClipMatrix")
    
    fzNear = 1.0
    fzFar = 61.0
    
    global cameraToClipMatrix
    # Note that this and the transformation matrix below are both
    # ROW-MAJOR ordered. Thus, it is necessary to pass a transpose
    # of the matrix to the glUniform assignment function.
    cameraToClipMatrix[0][0] = fFrustumScale
    cameraToClipMatrix[1][1] = fFrustumScale
    cameraToClipMatrix[2][2] = (fzFar + fzNear) / (fzNear - fzFar)
    cameraToClipMatrix[2][3] = -1.0
    cameraToClipMatrix[3][2] = (2 * fzFar * fzNear) / (fzNear - fzFar)
    
    glUseProgram(program)
    glUniformMatrix4fv(cameraToClipMatrixUnif, 1, GL_FALSE, cameraToClipMatrix.transpose())
    glUseProgram(0)


def initializeBuffers():
    global vbo, cbo, ibo

    vbo = glGenBuffers(1)
    glBindBuffer(GL_ARRAY_BUFFER, vbo)
    glBufferData(
        # PyOpenGL allows for the omission of the size parameter
        GL_ARRAY_BUFFER,
        obj['vertices'],
        GL_STATIC_DRAW
    )
    glBindBuffer(GL_ARRAY_BUFFER, 0)

    cbo = glGenBuffers(1)
    glBindBuffer(GL_ARRAY_BUFFER, cbo)
    glBufferData(
        GL_ARRAY_BUFFER,
        obj['colors'],
        GL_STATIC_DRAW
    )
    glBindBuffer(GL_ARRAY_BUFFER, 0)
    
    ibo = glGenBuffers(1)
    glBindBuffer(GL_ELEMENT_ARRAY_BUFFER, ibo)
    glBufferData(
        GL_ELEMENT_ARRAY_BUFFER,
        obj['faces'],
        GL_STATIC_DRAW
    )
    glBindBuffer(GL_ELEMENT_ARRAY_BUFFER, 0)
    

# Helper functions to return various types of transformation arrays
def calcLerpFactor(fElapsedTime, fLoopDuration):
    fValue = (fElapsedTime % fLoopDuration) / fLoopDuration
    if fValue > 0.5:
        fValue = 1.0 - fValue
    return fValue * 2.0
    

def computeAngleRad(fElapsedTime, fLoopDuration):
    fScale = 3.14159 * 2.0 / fLoopDuration
    fCurrTimeThroughLoop = fElapsedTime % fLoopDuration
    return fCurrTimeThroughLoop * fScale


def rotateY(fElapsedTime, **mouse):
    fAngRad = computeAngleRad(fElapsedTime, 2.0)
    fCos = cos(fAngRad)
    fSin = sin(fAngRad)
    
    newTransform = np.identity(4, dtype='float32')
    newTransform[0][0] = fCos
    newTransform[2][0] = fSin
    newTransform[0][2] = -fSin
    newTransform[2][2] = fCos
    # offset 
    newTransform[0][3] = 0.0 #-5.0
    newTransform[1][3] = 0.0 #5.0
    newTransform[2][3] = mouse['wheel']
    return newTransform

# A list of the helper offset functions.
# Note that this does not require a structure def in python.
# Each function is written to return the complete transform matrix.
g_instanceList = [
    rotateY,
]


# Initialize the OpenGL environment
def init(w, h):

    initializeProgram()

    glEnable(GL_CULL_FACE)
    glCullFace(GL_BACK)
    glFrontFace(GL_CW)
    
    glEnable(GL_DEPTH_TEST)
    glDepthMask(GL_TRUE)
    glDepthFunc(GL_LEQUAL)
    glDepthRange(0.0, 1.0)

    glMatrixMode(GL_PROJECTION)
    print('Scene bounding box:', scene_box)
    gluPerspective(45, (w/h), 0.1, 500)
    #glTranslatef(0, 0, -100)

   
def render(time, imgui_impl, mouse, mem, buffers):
    global wireframe_enabled, transform_selected

    #print(mouse['wheel'])
    
    glClearColor(0.0, 0.0, 0.0, 0.0)
    glClearDepth(1.0)
    glClear(GL_COLOR_BUFFER_BIT | GL_DEPTH_BUFFER_BIT)
    
    if wireframe_enabled:
        glPolygonMode(GL_FRONT_AND_BACK, GL_LINE)
    else:
        glPolygonMode(GL_FRONT_AND_BACK, GL_FILL)

    glUseProgram(program)
    
    fElapsedTime = pygame.time.get_ticks() / 1000.0
    transformMatrix = g_instanceList[transform_selected](fElapsedTime, **mouse)

    glUniformMatrix4fv(modelToCameraMatrixUnif, 1, GL_FALSE, transformMatrix.transpose())
    #glDrawElements(GL_TRIANGLES, len(obj['faces']), GL_UNSIGNED_SHORT, None)
    for vao_idx, vao in enumerate(buffers['vaos']):
        print('Rendering VAO {}'.format(vao_idx))
        print('SHAPE VBO', buffers['vbos'][vao_idx]['data']['values'].shape)
        print('SHAPE CBO', buffers['cbos'][vao_idx]['data']['values'].shape)
        print('SHAPE IBO', buffers['ibos'][vao_idx]['data']['values'].shape)

        print('''Address:
    VBO:\t{}
    CBO:\t{}
    IBO:\t{}
    VAO:\t{}
'''.format(id(buffers['vbos'][vao_idx]), id(buffers['cbos'][vao_idx]), id(buffers['ibos'][vao_idx]), id(vao)))

        glBindVertexArray(vao)
        index_dim = buffers['ibos'][vao_idx]['data']['stride']
        index_count = buffers['ibos'][vao_idx]['data']['count']
        glDrawElements(GL_TRIANGLES, index_count, GL_UNSIGNED_SHORT, None)
        glBindVertexArray(0)

    glUseProgram(0)

    imgui.new_frame()
    # Draw windows
    imgui.end_frame()
    imgui.render()
    imgui_impl.render(imgui.get_draw_data())

            
# Called whenever the window's size changes (including once when the program starts)
def reshape(w, h):
    global cameraToClipMatrix
    cameraToClipMatrix[0][0] = fFrustumScale * (h / float(w))
    cameraToClipMatrix[1][1] = fFrustumScale

    glUseProgram(program)
    glUniformMatrix4fv(cameraToClipMatrixUnif, 1, GL_FALSE, cameraToClipMatrix.transpose())
    glUseProgram(0)
    
    glViewport(0, 0, w, h)
    
def main():
    width = 800
    height = 800

    display = (width, height)

    pygame.init()
    pygame.display.set_caption('OpenGL VAO with pygame')
    pygame.display.set_mode(display, pygame.DOUBLEBUF | pygame.OPENGL | pygame.RESIZABLE)
    pygame.display.gl_set_attribute(pygame.GL_CONTEXT_MAJOR_VERSION, 4)
    pygame.display.gl_set_attribute(pygame.GL_CONTEXT_MINOR_VERSION, 1)
    pygame.display.gl_set_attribute(pygame.GL_CONTEXT_PROFILE_MASK, pygame.GL_CONTEXT_PROFILE_CORE)

    imgui.create_context()
    impl = PygameRenderer()
    io = imgui.get_io()
    #io.set_WantCaptureMouse(True)
    io.display_size = width, height

    #scene = loadObjs(Path('assets/sample0.obj'))
    scene = loadObjs(Path('assets/cubes.obj'))
    #scene = loadObjs(Path('assets/shapes.obj'))
    #scene = loadObjs(Path('assets/wooden_watch_tower/wooden watch tower2.obj'))
    buffers = createBuffers(scene)

    init(width, height)

    wheel_factor = 0.3
    mouse = {
        'pressed' : False,
        'motion' : {
            'curr' : np.array([0, 0]),
            'prev' : np.array([0, 0])
        },
        'pos' : {
            'curr' : np.array([0, 0]),
            'prev' : np.array([0, 0])
        },
        'wheel' : -10
    }
    
    clock = pygame.time.Clock()
    while True:
        for event in pygame.event.get():
            if event.type == pygame.QUIT or (event.type ==  pygame.KEYDOWN and event.key == pygame.K_ESCAPE):
                pygame.quit()
                quit()
            impl.process_event(event)

            if event.type == pygame.MOUSEMOTION:
                if mouse['pressed']:
                    #glRotatef(event.rel[1], 1, 0, 0)
                    #glRotatef(event.rel[0], 0, 1, 0)
                    mouse['motion']['curr'] = [event.rel[1], event.rel[0]]
                    mouse['pos']['curr'] = event.pos
            if event.type == pygame.MOUSEWHEEL:
                mouse['wheel'] += event.y * wheel_factor

        for event in pygame.mouse.get_pressed():
            mouse['pressed'] = pygame.mouse.get_pressed()[0] == 1

        render(time=clock, imgui_impl=impl, mouse=mouse, mem=None, buffers=buffers)

        mouse['motion']['prev'] = mouse['motion']['curr']
        mouse['pos']['prev'] = mouse['pos']['curr']

        pygame.display.flip()
        pygame.time.wait(10)

if __name__ == '__main__':
    main()

with utils module being defined as

from OpenGL.GLU import *
from OpenGL.GL import *
import os
import sys

# Function that creates and compiles shaders according to the given type (a GL enum value) and 
# shader program (a file containing a GLSL program).
def loadShader(shaderType, shaderFile):
    # check if file exists, get full path name
    strFilename = findFileOrThrow(shaderFile)
    shaderData = None
    with open(strFilename, 'r') as f:
        shaderData = f.read()
    
    shader = glCreateShader(shaderType)
    glShaderSource(shader, shaderData) # note that this is a simpler function call than in C
    
    # This shader compilation is more explicit than the one used in
    # framework.cpp, which relies on a glutil wrapper function.
    # This is made explicit here mainly to decrease dependence on pyOpenGL
    # utilities and wrappers, which docs caution may change in future versions.
    glCompileShader(shader)
    
    status = glGetShaderiv(shader, GL_COMPILE_STATUS)
    if status == GL_FALSE:
        # Note that getting the error log is much simpler in Python than in C/C++
        # and does not require explicit handling of the string buffer
        strInfoLog = glGetShaderInfoLog(shader)
        strShaderType = ""
        if shaderType is GL_VERTEX_SHADER:
            strShaderType = "vertex"
        elif shaderType is GL_GEOMETRY_SHADER:
            strShaderType = "geometry"
        elif shaderType is GL_FRAGMENT_SHADER:
            strShaderType = "fragment"
        
        print("Compilation failure for " + strShaderType + " shader:\n" + strInfoLog)
    
    return shader

# Function that accepts a list of shaders, compiles them, and returns a handle to the compiled program
def createProgram(shaderList):
    program = glCreateProgram()
    
    for shader in shaderList:
        glAttachShader(program, shader)
        
    glLinkProgram(program)
    
    status = glGetProgramiv(program, GL_LINK_STATUS)
    if status == GL_FALSE:
        # Note that getting the error log is much simpler in Python than in C/C++
        # and does not require explicit handling of the string buffer
        strInfoLog = glGetProgramInfoLog(program)
        print("Linker failure: \n" + strInfoLog)
        
    for shader in shaderList:
        glDetachShader(program, shader)
        
    return program
    
    
# Helper function to locate and open the target file (passed in as a string).
# Returns the full path to the file as a string.
def findFileOrThrow(strBasename):
    # Keep constant names in C-style convention, for readability
    # when comparing to C(/C++) code.
    LOCAL_FILE_DIR = "data" + os.sep
    GLOBAL_FILE_DIR = ".." + os.sep + "data" + os.sep
    
    strFilename = LOCAL_FILE_DIR + strBasename
    if os.path.isfile(strFilename):
        return strFilename
        
    strFilename = GLOBAL_FILE_DIR + strBasename
    if os.path.isfile(strFilename):
        return strFilename
        
    raise IOError('Could not find target file ' + strBasename)

Solution

  • Maybe check your datatypes. E.g. when you set

    indices = np.array(mesh.faces)
    

    this array has to have dtype == np.uint16, because you later render with

    glDrawElements(..., GL_UNSIGNED_SHORT, ...)
    

    So if your array of indices has dtype == np.uint32, you need to draw with GL_UNSIGNED_INT.

    Verify the data types for vertex data aswell, it has to be np.float32, because you define GL_FLOAT when you call glVertexAttribPointer