Current OBJLoader:
Using pygame's obj loader as a base, I created a new obj loader for Python OpenGL. The loader includes the following functionality:
- Collect vertices, normals, and texture to draw
- Divide objects based on materials
- Store each material's vertices, normals, colours and textures in separate VAO's
- Have an instance vbo ready for each material
- Call an appropriate shader for each material
- Draw the whole Object as if it is one object
This means that you can call a single object to be drawn once, i.e:
obj_m = ObjLoader()
obj_m.load_model("C:/Projects/PythonScripts/GIS/obj_pins/KFC.obj")
obj_KFC = obj_m.load_obj()
Or you can draw the object in multiple places using instancing, i.e:
instance_list = [[0,0,0], [-10,0,0], [10,0,0]]
obj_m = ObjLoader()
obj_m.load_model("C:/Projects/PythonScripts/GIS/obj_pins/KFC.obj", *instance_list)
obj_KFC = obj_m.load_obj()
Then you can call the object in your drawing loop with or without transformation, i.e:
obj_m.draw_call(obj_KFC, view, projection, model)
or
transform = glm.mat4(1.0)
transform = glm.rotate(transform, glfw.get_time(), glm.vec3(0.0, 1.0, 1.0))
transform = glm.scale(transform, glm.vec3(1.0, 1.0, 1.0))
obj_m.draw_call(obj_KFC, view, projection, model, transform)
Current Drawbacks
I only recently started with OpenGL for fun and I'm not a programmer per se. Therefore I implemented the object loader's functionality as I went along. The code is therefore not "pretty", nor tested extensively and mostly without added error checking code. These are the known drawbacks:
The .obj file should be triangulated i.e using Blender:
The .obj, .mtl, and image (if a material contains a texture) needs to be in the same folder
The link to the texture in the .mtl folder should be relative, therefore containing only the name of the image i.e: map_Kd 3D_kfc.png
Improvements
I thought to share the code because as I stated, I'm not a programmer per se, but I think others can benefit from this especially when the code is "cleaner". So the questions:
- Are there obvious structure improvements?
- Are there obvious improvements to the code?
- Are there any wanted improvements i.e lighting?
OBJLOADER CODE
import numpy as np
from OpenGL.GL import *
from OpenGL.GL.shaders import compileProgram, compileShader
import os
import glm
from PIL import Image
class ObjLoader:
def __init__(self):
self.vert_coords = []
self.text_coords = []
self.norm_coords = []
self.object_parts = {}
self.setup_index()
self.obj_shaders()
def setup_index(self):
self.material_index = []
self.vertex_index = []
self.texture_index = []
self.normal_index = []
self.model = []
self.material_index = []
self.vertex_index = []
self.texture_index = []
self.normal_index = []
def load_model(self, file, *args):
for line in open(file, 'r'):
if line.startswith('#'): continue
values = line.split()
if not values: continue
if values[0] == 'v':
self.vert_coords.append(values[1:4])
if values[0] == 'vt':
self.text_coords.append(values[1:3])
if values[0] == 'vn':
self.norm_coords.append(values[1:4])
if values[0] == 'mtllib':
self.mtl = values[1]
matarials = self.mat_in_file(file)
m_counter = 0
for line in open(file, 'r'): # open(file, 'r'):
if line.startswith('#'): continue
values = line.split()
if not values: continue
if values[0] == 'usemtl':
if values[1] != matarials[m_counter][0]:
self.define_i_t_u(file, matarials[m_counter], args)
m_counter += 1
self.setup_index()
if values[0] == 'f':
face_i = []
text_i = []
norm_i = []
for v in values[1:4]:
w = v.split('/')
face_i.append(int(w[0]) - 1)
text_i.append(int(w[1]) - 1)
norm_i.append(int(w[2]) - 1)
self.vertex_index.append(face_i)
self.texture_index.append(text_i)
self.normal_index.append(norm_i)
self.define_i_t_u(file, matarials[m_counter], args)
def define_i_t_u(self, file, mat, args):
self.vertex_index = [y for x in self.vertex_index for y in x]
self.texture_index = [y for x in self.texture_index for y in x]
self.normal_index = [y for x in self.normal_index for y in x]
for i in self.vertex_index:
self.model.extend(self.vert_coords[i])
for i in self.texture_index:
self.model.extend(self.text_coords[i])
for i in self.normal_index:
self.model.extend(self.norm_coords[i])
self.model = np.array(self.model, dtype='float32')
glUseProgram(mat[1])
texid = self.MTL(file)
l_vao = self.vao_creator(self.vertex_index, self.model, mat[1], mat[2], texid, args)
self.object_parts["{0}".format(mat[0])] = l_vao
def MTL(self, filename):
filename = os.path.splitext(filename)[0] + '.mtl'
filepath = os.path.dirname(filename)
base = os.path.basename(filename)
file_only_name = os.path.splitext(base)[0]
contents = {}
mtl = None
image = None
texid = None
surf = None
values = []
for line in open(filename, "r"):
if line.startswith('#'): continue
values = line.split()
if not values: continue
if values[0] == 'newmtl':
mtl = contents[values[1]] = {}
elif mtl is None:
raise ValueError("mtl file doesn't start with newmtl stmt")
elif values[0] == 'map_Kd':
self.object_shader = self.shader_obj_text
# load the texture referred to by this declaration
mtl[values[0]] = values[1]
texid = mtl['texture_Kd'] = glGenTextures(1)
glBindTexture(GL_TEXTURE_2D, texid)
glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MIN_FILTER,
GL_LINEAR)
glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MAG_FILTER,
GL_LINEAR)
image = Image.open(os.path.join(filepath, mtl['map_Kd']))
flipped_image = image.transpose(Image.FLIP_TOP_BOTTOM)
img_data = flipped_image.convert("RGBA").tobytes()
glTexImage2D(GL_TEXTURE_2D, 0, GL_RGBA, image.width, image.height, 0, GL_RGBA, GL_UNSIGNED_BYTE,
img_data)
return texid
def vao_creator(self, vertex_index, model, shader, defuse_col, texid, args):
texture_offset = len(vertex_index) * 12
VAO = glGenVertexArrays(1)
glBindVertexArray(VAO)
VBO = glGenBuffers(1)
glBindBuffer(GL_ARRAY_BUFFER, VBO)
glBufferData(GL_ARRAY_BUFFER, model.itemsize * len(model), model, GL_STATIC_DRAW)
# position
glVertexAttribPointer(0, 3, GL_FLOAT, GL_FALSE, model.itemsize * 3, ctypes.c_void_p(0))
glEnableVertexAttribArray(0)
# texture
glVertexAttribPointer(1, 2, GL_FLOAT, GL_FALSE, model.itemsize * 2, ctypes.c_void_p(texture_offset))
glEnableVertexAttribArray(1)
# Normals
glVertexAttribPointer(2, 3, GL_FLOAT, GL_FALSE, model.itemsize * 3, ctypes.c_void_p(12))
glEnableVertexAttribArray(2)
# Instancing
if len(args) == 0:
amount = 1
vert_instance = np.array([[0.0, 0.0, 0.0], [1.0, 1.0, 1.0]])
else:
amount = len(args)
vert_instance = np.array(args)
modelMatrices = []
# Storring the objects in a list
for i in range(0, amount):
mod_list = []
model_o = glm.mat4(1.0)
model_o = glm.translate(model_o, glm.vec3(vert_instance[i][0], vert_instance[i][1], vert_instance[i][2]))
# model_o = glm.scale(model_o, glm.vec3(0.00005, 0.00005, 0.00005))
for i in range(4):
for j in range(4):
mod_list.append(model_o[i][j])
modelMatrices.append(mod_list)
modelMatrices = np.array(modelMatrices, dtype="f")
instanceVBO = glGenBuffers(1)
glBindBuffer(GL_ARRAY_BUFFER, instanceVBO)
glBufferData(GL_ARRAY_BUFFER, amount * glm.sizeof(glm.mat4), modelMatrices, GL_STATIC_DRAW)
# Bind each vertex attrib array of matrices (4 vectors in Matrix)
glEnableVertexAttribArray(3)
glVertexAttribPointer(3, 4, GL_FLOAT, GL_FALSE, glm.sizeof(glm.mat4), ctypes.c_void_p(0))
glEnableVertexAttribArray(4)
glVertexAttribPointer(4, 4, GL_FLOAT, GL_FALSE, glm.sizeof(glm.mat4), ctypes.c_void_p(glm.sizeof(glm.vec4)))
glEnableVertexAttribArray(5)
glVertexAttribPointer(5, 4, GL_FLOAT, GL_FALSE, glm.sizeof(glm.mat4),
ctypes.c_void_p((2 * glm.sizeof(glm.vec4))))
glEnableVertexAttribArray(6)
glVertexAttribPointer(6, 4, GL_FLOAT, GL_FALSE, glm.sizeof(glm.mat4),
ctypes.c_void_p((3 * glm.sizeof(glm.vec4))))
# Set instancing
glVertexAttribDivisor(3, 1)
glVertexAttribDivisor(4, 1)
glVertexAttribDivisor(5, 1)
glVertexAttribDivisor(6, 1)
glBindVertexArray(0)
return ([VAO, len(vertex_index), shader, defuse_col, texid, len(vert_instance)])
def mat_in_file(self, filename):
filename = os.path.splitext(filename)[0] + '.mtl'
mtl = None
contents = {}
mat_list = []
for line in open(filename, "r"):
if line.startswith('#'): continue
values = line.split()
if not values: continue
if values[0] == 'newmtl':
object_shader = self.shader_obj
mtl = contents[values[1]] = {}
matname = values[1]
elif mtl is None:
raise ValueError("mtl file doesn't start with newmtl stmt")
elif values[0] == 'Kd':
defuse_col = [float(values[1]), float(values[2]), float(values[3])]
elif values[0] == 'd':
defuse_col = defuse_col + [float(values[1])]
mat_list.append([matname, object_shader, defuse_col])
elif values[0] == 'map_Kd':
object_shader = self.shader_obj_text
mat_list.pop()
mat_list.append([matname, object_shader, defuse_col])
return mat_list
def load_obj(self):
obj_list = [self.object_parts[ind] for ind in self.object_parts]
return obj_list
def draw_call(self, vao_list, view, projection, model, *transf):
if len(transf) == 0:
transform = glm.mat4(1.0)
else:
transform = transf[0]
for key in vao_list:
VAO = key[0]
vertex_len = key[1]
shader = key[2]
d_colour = key[3]
texid = key[4]
len_instanced = key[5]
glUseProgram(shader)
glBindTexture(GL_TEXTURE_2D, texid)
view_loc = glGetUniformLocation(shader, "view")
proj_loc = glGetUniformLocation(shader, "projection")
model_loc = glGetUniformLocation(shader, "model")
col_loc = glGetUniformLocation(shader, "mycolor")
glUniformMatrix4fv(view_loc, 1, GL_FALSE, view)
glUniformMatrix4fv(proj_loc, 1, GL_FALSE, projection)
glUniformMatrix4fv(model_loc, 1, GL_FALSE, model)
glUniform4fv(col_loc, 1, glm.value_ptr(glm.vec4(d_colour)))
transformLoc = glGetUniformLocation(shader, "transform")
glUniformMatrix4fv(transformLoc, 1, GL_FALSE, glm.value_ptr(transform))
glBindVertexArray(VAO)
glDrawArraysInstanced(GL_TRIANGLES, 0, vertex_len, len_instanced)
glBindVertexArray(0)
def obj_shaders(self):
vertex_obj = """
# version 330
in layout(location = 0) vec3 position;
in layout(location = 1) vec2 textureCoords;
in layout(location = 2) vec3 normalCoords;
in layout (location = 3) mat4 instanceMatrix;
uniform mat4 transform;
uniform mat4 view;
uniform mat4 model;
uniform mat4 projection;
out vec2 newTexture;
out vec3 newNorm;
void main()
{
gl_Position = projection * view *instanceMatrix *model * transform *vec4(position, 1.0f);
newTexture = textureCoords;
newNorm = normalCoords;
}
"""
fragment_obj = """
#version 330
in vec2 newTexture;
in vec3 newNorm;
uniform vec4 mycolor;
out vec4 outColor;
uniform sampler2D samplerTexture;
void main()
{
outColor = mycolor;
}
"""
fragment_obj_text = """
#version 330
in vec2 newTexture;
in vec3 newNorm;
in vec3 out_colours;
uniform vec4 mycolor;
out vec4 outColor;
uniform sampler2D samplerTexture;
void main()
{
outColor = texture(samplerTexture, newTexture);
}
"""
self.shader_obj = compileProgram(compileShader(vertex_obj, GL_VERTEX_SHADER),
compileShader(fragment_obj, GL_FRAGMENT_SHADER))
self.shader_obj_text = compileProgram(compileShader(vertex_obj, GL_VERTEX_SHADER),
compileShader(fragment_obj_text, GL_FRAGMENT_SHADER))
Example code
import glfw
import pyrr
from ObjLoader import *
def window_resize(window, width, height):
glViewport(0, 0, width, height)
def main():
# initialize glfw
if not glfw.init():
return
w_width, w_height = 800, 600
window = glfw.create_window(w_width, w_height, "My OpenGL window", None, None)
if not window:
glfw.terminate()
return
glfw.make_context_current(window)
glfw.set_window_size_callback(window, window_resize)
instance_list = [[0,0,0], [-10,0,0], [10,0,0]]
obj_m = ObjLoader()
obj_m.load_model("C:/Projects/PythonScripts/GIS/obj_pins/KFC.obj", *instance_list)
obj_KFC = obj_m.load_obj()
glClearColor(0.2, 0.3, 0.2, 1.0)
glEnable(GL_DEPTH_TEST)
view = pyrr.matrix44.create_from_translation(pyrr.Vector3([0.0, 0.0, -20.0]))
projection = pyrr.matrix44.create_perspective_projection_matrix(65.0, w_width / w_height, 0.1, 100.0)
model = pyrr.matrix44.create_from_translation(pyrr.Vector3([0.0, 0.0, 0.0]))
while not glfw.window_should_close(window):
glfw.poll_events()
glClear(GL_COLOR_BUFFER_BIT | GL_DEPTH_BUFFER_BIT)
#obj_m.draw_call(obj_KFC, view, projection, model)
transform = glm.mat4(1.0)
transform = glm.rotate(transform, glfw.get_time(), glm.vec3(0.0, 1.0, 1.0))
transform = glm.scale(transform, glm.vec3(1.0, 1.0, 1.0))
obj_m.draw_call(obj_KFC, view, projection, model, transform)
glfw.swap_buffers(window)
glfw.terminate()
if __name__ == "__main__":
main()