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)
|
|
|