Since the new SDL based viewer is much better suited to interactive exploration, simplify the basic OpenGL viewer to make it an easy introductive read.
381 lines
12 KiB
Python
Executable File
381 lines
12 KiB
Python
Executable File
#!/usr/bin/env python
|
|
#-*- coding: UTF-8 -*-
|
|
|
|
""" This program demonstrates the use of pyassimp to load and
|
|
render objects with OpenGL.
|
|
|
|
'c' cycles between cameras (if any available)
|
|
'q' to quit
|
|
|
|
This example mixes 'old' OpenGL fixed-function pipeline with
|
|
Vertex Buffer Objects.
|
|
|
|
Materials are supported but textures are currently ignored.
|
|
|
|
For a more advanced example (with shaders + keyboard/mouse
|
|
controls), check scripts/sdl_viewer.py
|
|
|
|
Author: Séverin Lemaignan, 2012
|
|
|
|
This sample is based on several sources, including:
|
|
- http://www.lighthouse3d.com/tutorials
|
|
- http://www.songho.ca/opengl/gl_transform.html
|
|
- http://code.activestate.com/recipes/325391/
|
|
- ASSIMP's C++ SimpleOpenGL viewer
|
|
"""
|
|
|
|
import os, sys
|
|
from OpenGL.GLUT import *
|
|
from OpenGL.GLU import *
|
|
from OpenGL.GL import *
|
|
|
|
import logging;logger = logging.getLogger("pyassimp_opengl")
|
|
logging.basicConfig(level=logging.INFO)
|
|
|
|
import math
|
|
import numpy
|
|
|
|
from pyassimp import core as pyassimp
|
|
from pyassimp.postprocess import *
|
|
from pyassimp.helper import *
|
|
|
|
|
|
name = 'pyassimp OpenGL viewer'
|
|
height = 600
|
|
width = 900
|
|
|
|
class GLRenderer():
|
|
def __init__(self):
|
|
|
|
self.scene = None
|
|
|
|
self.using_fixed_cam = False
|
|
self.current_cam_index = 0
|
|
|
|
# store the global scene rotation
|
|
self.angle = 0.
|
|
|
|
# for FPS calculation
|
|
self.prev_time = 0
|
|
self.prev_fps_time = 0
|
|
self.frames = 0
|
|
|
|
def prepare_gl_buffers(self, mesh):
|
|
""" Creates 3 buffer objets for each mesh,
|
|
to store the vertices, the normals, and the faces
|
|
indices.
|
|
"""
|
|
|
|
mesh.gl = {}
|
|
|
|
# Fill the buffer for vertex positions
|
|
mesh.gl["vertices"] = glGenBuffers(1)
|
|
glBindBuffer(GL_ARRAY_BUFFER, mesh.gl["vertices"])
|
|
glBufferData(GL_ARRAY_BUFFER,
|
|
mesh.vertices,
|
|
GL_STATIC_DRAW)
|
|
|
|
# Fill the buffer for normals
|
|
mesh.gl["normals"] = glGenBuffers(1)
|
|
glBindBuffer(GL_ARRAY_BUFFER, mesh.gl["normals"])
|
|
glBufferData(GL_ARRAY_BUFFER,
|
|
mesh.normals,
|
|
GL_STATIC_DRAW)
|
|
|
|
|
|
# Fill the buffer for vertex positions
|
|
mesh.gl["triangles"] = glGenBuffers(1)
|
|
glBindBuffer(GL_ELEMENT_ARRAY_BUFFER, mesh.gl["triangles"])
|
|
glBufferData(GL_ELEMENT_ARRAY_BUFFER,
|
|
mesh.faces,
|
|
GL_STATIC_DRAW)
|
|
|
|
# Unbind buffers
|
|
glBindBuffer(GL_ARRAY_BUFFER,0)
|
|
glBindBuffer(GL_ELEMENT_ARRAY_BUFFER,0)
|
|
|
|
def load_model(self, path, postprocess = None):
|
|
logger.info("Loading model:" + path + "...")
|
|
|
|
if postprocess:
|
|
self.scene = pyassimp.load(path, postprocess)
|
|
else:
|
|
self.scene = pyassimp.load(path)
|
|
logger.info("Done.")
|
|
|
|
scene = self.scene
|
|
#log some statistics
|
|
logger.info(" meshes: %d" % len(scene.meshes))
|
|
logger.info(" total faces: %d" % sum([len(mesh.faces) for mesh in scene.meshes]))
|
|
logger.info(" materials: %d" % len(scene.materials))
|
|
self.bb_min, self.bb_max = get_bounding_box(self.scene)
|
|
logger.info(" bounding box:" + str(self.bb_min) + " - " + str(self.bb_max))
|
|
|
|
self.scene_center = [(a + b) / 2. for a, b in zip(self.bb_min, self.bb_max)]
|
|
|
|
for index, mesh in enumerate(scene.meshes):
|
|
self.prepare_gl_buffers(mesh)
|
|
|
|
# Finally release the model
|
|
pyassimp.release(scene)
|
|
|
|
def cycle_cameras(self):
|
|
self.current_cam_index
|
|
if not self.scene.cameras:
|
|
return None
|
|
self.current_cam_index = (self.current_cam_index + 1) % len(self.scene.cameras)
|
|
cam = self.scene.cameras[self.current_cam_index]
|
|
logger.info("Switched to camera " + str(cam))
|
|
return cam
|
|
|
|
def set_default_camera(self):
|
|
|
|
if not self.using_fixed_cam:
|
|
glLoadIdentity()
|
|
|
|
gluLookAt(0.,0.,3.,
|
|
0.,0.,-5.,
|
|
0.,1.,0.)
|
|
|
|
|
|
|
|
def set_camera(self, camera):
|
|
|
|
if not camera:
|
|
return
|
|
|
|
self.using_fixed_cam = True
|
|
|
|
znear = camera.clipplanenear
|
|
zfar = camera.clipplanefar
|
|
aspect = camera.aspect
|
|
fov = camera.horizontalfov
|
|
|
|
glMatrixMode(GL_PROJECTION)
|
|
glLoadIdentity()
|
|
|
|
# Compute gl frustrum
|
|
tangent = math.tan(fov/2.)
|
|
h = znear * tangent
|
|
w = h * aspect
|
|
|
|
# params: left, right, bottom, top, near, far
|
|
glFrustum(-w, w, -h, h, znear, zfar)
|
|
# equivalent to:
|
|
#gluPerspective(fov * 180/math.pi, aspect, znear, zfar)
|
|
|
|
glMatrixMode(GL_MODELVIEW)
|
|
glLoadIdentity()
|
|
|
|
cam = transform(camera.position, camera.transformation)
|
|
at = transform(camera.lookat, camera.transformation)
|
|
gluLookAt(cam[0], cam[2], -cam[1],
|
|
at[0], at[2], -at[1],
|
|
0, 1, 0)
|
|
|
|
def fit_scene(self, restore = False):
|
|
""" Compute a scale factor and a translation to fit and center
|
|
the whole geometry on the screen.
|
|
"""
|
|
|
|
x_max = self.bb_max[0] - self.bb_min[0]
|
|
y_max = self.bb_max[1] - self.bb_min[1]
|
|
tmp = max(x_max, y_max)
|
|
z_max = self.bb_max[2] - self.bb_min[2]
|
|
tmp = max(z_max, tmp)
|
|
|
|
if not restore:
|
|
tmp = 1. / tmp
|
|
|
|
logger.info("Scaling the scene by %.03f" % tmp)
|
|
glScalef(tmp, tmp, tmp)
|
|
|
|
# center the model
|
|
direction = -1 if not restore else 1
|
|
glTranslatef( direction * self.scene_center[0],
|
|
direction * self.scene_center[1],
|
|
direction * self.scene_center[2] )
|
|
|
|
return x_max, y_max, z_max
|
|
|
|
def apply_material(self, mat):
|
|
""" Apply an OpenGL, using one OpenGL display list per material to cache
|
|
the operation.
|
|
"""
|
|
|
|
if not hasattr(mat, "gl_mat"): # evaluate once the mat properties, and cache the values in a glDisplayList.
|
|
|
|
diffuse = numpy.array(mat.properties.get("diffuse", [0.8, 0.8, 0.8, 1.0]))
|
|
specular = numpy.array(mat.properties.get("specular", [0., 0., 0., 1.0]))
|
|
ambient = numpy.array(mat.properties.get("ambient", [0.2, 0.2, 0.2, 1.0]))
|
|
emissive = numpy.array(mat.properties.get("emissive", [0., 0., 0., 1.0]))
|
|
shininess = min(mat.properties.get("shininess", 1.0), 128)
|
|
wireframe = mat.properties.get("wireframe", 0)
|
|
twosided = mat.properties.get("twosided", 1)
|
|
|
|
setattr(mat, "gl_mat", glGenLists(1))
|
|
glNewList(mat.gl_mat, GL_COMPILE)
|
|
|
|
glMaterialfv(GL_FRONT_AND_BACK, GL_DIFFUSE, diffuse)
|
|
glMaterialfv(GL_FRONT_AND_BACK, GL_SPECULAR, specular)
|
|
glMaterialfv(GL_FRONT_AND_BACK, GL_AMBIENT, ambient)
|
|
glMaterialfv(GL_FRONT_AND_BACK, GL_EMISSION, emissive)
|
|
glMaterialf(GL_FRONT_AND_BACK, GL_SHININESS, shininess)
|
|
glPolygonMode(GL_FRONT_AND_BACK, GL_LINE if wireframe else GL_FILL)
|
|
glDisable(GL_CULL_FACE) if twosided else glEnable(GL_CULL_FACE)
|
|
|
|
glEndList()
|
|
|
|
glCallList(mat.gl_mat)
|
|
|
|
|
|
|
|
def do_motion(self):
|
|
|
|
gl_time = glutGet(GLUT_ELAPSED_TIME)
|
|
|
|
# Compute the new position of the camera and set it
|
|
self.x += self.dp * self.lx * 0.01 * (gl_time-self.prev_time)
|
|
self.z += self.dp * self.lz * 0.01 * (gl_time-self.prev_time)
|
|
self.angle += self.drot * 0.1 * (gl_time-self.prev_time)
|
|
self.lx = math.sin(self.angle)
|
|
self.lz = -math.cos(self.angle)
|
|
self.set_default_camera()
|
|
|
|
self.angle = (gl_time - self.prev_time) * 0.1
|
|
|
|
self.prev_time = gl_time
|
|
|
|
# Compute FPS
|
|
self.frames += 1
|
|
if gl_time - self.prev_fps_time >= 1000:
|
|
current_fps = self.frames * 1000 / (gl_time - self.prev_fps_time)
|
|
logger.info('%.0f fps' % current_fps)
|
|
self.frames = 0
|
|
self.prev_fps_time = gl_time
|
|
|
|
glutPostRedisplay()
|
|
|
|
def recursive_render(self, node):
|
|
""" Main recursive rendering method.
|
|
"""
|
|
|
|
# save model matrix and apply node transformation
|
|
glPushMatrix()
|
|
m = node.transformation.transpose() # OpenGL row major
|
|
glMultMatrixf(m)
|
|
|
|
for mesh in node.meshes:
|
|
self.apply_material(mesh.material)
|
|
|
|
glBindBuffer(GL_ARRAY_BUFFER, mesh.gl["vertices"])
|
|
glEnableClientState(GL_VERTEX_ARRAY)
|
|
glVertexPointer(3, GL_FLOAT, 0, None)
|
|
|
|
glBindBuffer(GL_ARRAY_BUFFER, mesh.gl["normals"])
|
|
glEnableClientState(GL_NORMAL_ARRAY)
|
|
glNormalPointer(GL_FLOAT, 0, None)
|
|
|
|
glBindBuffer(GL_ELEMENT_ARRAY_BUFFER, mesh.gl["triangles"])
|
|
glDrawElements(GL_TRIANGLES,len(mesh.faces) * 3, GL_UNSIGNED_INT, None)
|
|
|
|
glDisableClientState(GL_VERTEX_ARRAY)
|
|
glDisableClientState(GL_NORMAL_ARRAY)
|
|
|
|
glBindBuffer(GL_ARRAY_BUFFER, 0)
|
|
glBindBuffer(GL_ELEMENT_ARRAY_BUFFER, 0)
|
|
|
|
for child in node.children:
|
|
self.recursive_render(child)
|
|
|
|
glPopMatrix()
|
|
|
|
|
|
def display(self):
|
|
""" GLUT callback to redraw OpenGL surface
|
|
"""
|
|
glClear(GL_COLOR_BUFFER_BIT|GL_DEPTH_BUFFER_BIT)
|
|
|
|
glRotatef(self.angle,0.,1.,0.)
|
|
self.recursive_render(self.scene.rootnode)
|
|
|
|
glutSwapBuffers()
|
|
self.do_motion()
|
|
return
|
|
|
|
####################################################################
|
|
## GLUT keyboard and mouse callbacks ##
|
|
####################################################################
|
|
def onkeypress(self, key, x, y):
|
|
if key == 'c':
|
|
self.fit_scene(restore = True)
|
|
self.set_camera(self.cycle_cameras())
|
|
if key == 'q':
|
|
sys.exit(0)
|
|
|
|
def render(self, filename=None, fullscreen = False, autofit = True, postprocess = None):
|
|
"""
|
|
|
|
:param autofit: if true, scale the scene to fit the whole geometry
|
|
in the viewport.
|
|
"""
|
|
|
|
# First initialize the openGL context
|
|
glutInit(sys.argv)
|
|
glutInitDisplayMode(GLUT_DOUBLE | GLUT_RGB | GLUT_DEPTH)
|
|
if not fullscreen:
|
|
glutInitWindowSize(width, height)
|
|
glutCreateWindow(name)
|
|
else:
|
|
glutGameModeString("1024x768")
|
|
if glutGameModeGet(GLUT_GAME_MODE_POSSIBLE):
|
|
glutEnterGameMode()
|
|
else:
|
|
print("Fullscreen mode not available!")
|
|
sys.exit(1)
|
|
|
|
self.load_model(filename, postprocess = postprocess)
|
|
|
|
|
|
glClearColor(0.1,0.1,0.1,1.)
|
|
#glShadeModel(GL_SMOOTH)
|
|
|
|
glEnable(GL_LIGHTING)
|
|
|
|
glEnable(GL_CULL_FACE)
|
|
glEnable(GL_DEPTH_TEST)
|
|
|
|
glLightModeli(GL_LIGHT_MODEL_TWO_SIDE, GL_TRUE)
|
|
glEnable(GL_NORMALIZE)
|
|
glEnable(GL_LIGHT0)
|
|
|
|
glutDisplayFunc(self.display)
|
|
|
|
|
|
glMatrixMode(GL_PROJECTION)
|
|
glLoadIdentity()
|
|
gluPerspective(35.0, width/float(height) , 0.10, 100.0)
|
|
glMatrixMode(GL_MODELVIEW)
|
|
self.set_default_camera()
|
|
|
|
if autofit:
|
|
# scale the whole asset to fit into our view frustum·
|
|
self.fit_scene()
|
|
|
|
glPushMatrix()
|
|
|
|
glutKeyboardFunc(self.onkeypress)
|
|
glutIgnoreKeyRepeat(1)
|
|
|
|
glutMainLoop()
|
|
|
|
|
|
if __name__ == '__main__':
|
|
if not len(sys.argv) > 1:
|
|
print("Usage: " + __file__ + " <model>")
|
|
sys.exit(0)
|
|
|
|
glrender = GLRenderer()
|
|
glrender.render(sys.argv[1], fullscreen = False, postprocess = aiProcessPreset_TargetRealtime_MaxQuality)
|
|
|