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:
vbos
- a list of dictionaries each representing the vertices of a meshcbos
- a list of dictionaries each representing the vertex colors of a meshibos
- a list of dictionaries each representing the indices of all faces of a meshvaos
- a list of VAO referencesThe 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)
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