#!/usr/bin/env python #-*- coding: UTF-8 -*- """ This program loads a model with PyASSIMP, and display it. It make a large use of shaders to illustrate a 'modern' OpenGL pipeline. Based on: - pygame + mouselook code from http://3dengine.org/Spectator_%28PyOpenGL%29 - http://www.lighthouse3d.com/tutorials - http://www.songho.ca/opengl/gl_transform.html - http://code.activestate.com/recipes/325391/ - ASSIMP's C++ SimpleOpenGL viewer Authors: Séverin Lemaignan, 2012-2013 """ import sys import logging logger = logging.getLogger("pyassimp") gllogger = logging.getLogger("OpenGL") gllogger.setLevel(logging.WARNING) logging.basicConfig(level=logging.INFO) import OpenGL OpenGL.ERROR_CHECKING=False OpenGL.ERROR_LOGGING = False #OpenGL.ERROR_ON_COPY = True #OpenGL.FULL_LOGGING = True from OpenGL.GL import * from OpenGL.error import GLError from OpenGL.GLU import * from OpenGL.GLUT import * from OpenGL.arrays import vbo from OpenGL.GL import shaders import pygame import math, random import numpy from numpy import linalg import pyassimp from pyassimp.postprocess import * from pyassimp.helper import * class DefaultCamera: def __init__(self, w, h, fov): self.clipplanenear = 0.001 self.clipplanefar = 100000.0 self.aspect = w/h self.horizontalfov = fov * math.pi/180 self.transformation = [[ 0.68, -0.32, 0.65, 7.48], [ 0.73, 0.31, -0.61, -6.51], [-0.01, 0.89, 0.44, 5.34], [ 0., 0., 0., 1. ]] self.lookat = [0.0,0.0,-1.0] def __str__(self): return "Default camera" class PyAssimp3DViewer: base_name = "PyASSIMP 3D viewer" def __init__(self, model, w=1024, h=768, fov=75): pygame.init() pygame.display.set_caption(self.base_name) pygame.display.set_mode((w,h), pygame.OPENGL | pygame.DOUBLEBUF) self.prepare_shaders() self.cameras = [DefaultCamera(w,h,fov)] self.current_cam_index = 0 self.load_model(model) # for FPS computation self.frames = 0 self.last_fps_time = glutGet(GLUT_ELAPSED_TIME) self.cycle_cameras() def prepare_shaders(self): phong_weightCalc = """ float phong_weightCalc( in vec3 light_pos, // light position in vec3 frag_normal // geometry normal ) { // returns vec2( ambientMult, diffuseMult ) float n_dot_pos = max( 0.0, dot( frag_normal, light_pos )); return n_dot_pos; } """ vertex = shaders.compileShader( phong_weightCalc + """ uniform vec4 Global_ambient; uniform vec4 Light_ambient; uniform vec4 Light_diffuse; uniform vec3 Light_location; uniform vec4 Material_ambient; uniform vec4 Material_diffuse; attribute vec3 Vertex_position; attribute vec3 Vertex_normal; varying vec4 baseColor; void main() { gl_Position = gl_ModelViewProjectionMatrix * vec4( Vertex_position, 1.0 ); vec3 EC_Light_location = gl_NormalMatrix * Light_location; float diffuse_weight = phong_weightCalc( normalize(EC_Light_location), normalize(gl_NormalMatrix * Vertex_normal) ); baseColor = clamp( ( // global component (Global_ambient * Material_ambient) // material's interaction with light's contribution // to the ambient lighting... + (Light_ambient * Material_ambient) // material's interaction with the direct light from // the light. + (Light_diffuse * Material_diffuse * diffuse_weight) ), 0.0, 1.0); }""", GL_VERTEX_SHADER) fragment = shaders.compileShader(""" varying vec4 baseColor; void main() { gl_FragColor = baseColor; } """, GL_FRAGMENT_SHADER) self.shader = shaders.compileProgram(vertex,fragment) self.set_shader_accessors( ( 'Global_ambient', 'Light_ambient','Light_diffuse','Light_location', 'Material_ambient','Material_diffuse', ), ( 'Vertex_position','Vertex_normal', ), self.shader) def set_shader_accessors(self, uniforms, attributes, shader): # add accessors to the shaders uniforms and attributes for uniform in uniforms: location = glGetUniformLocation( shader, uniform ) if location in (None,-1): logger.warning('No uniform: %s'%( uniform )) setattr( shader, uniform, location ) for attribute in attributes: location = glGetAttribLocation( shader, attribute ) if location in (None,-1): logger.warning('No attribute: %s'%( attribute )) setattr( shader, attribute, location ) def prepare_gl_buffers(self, mesh): mesh.gl = {} # Fill the buffer for vertex and normals positions v = numpy.array(mesh.vertices, 'f') n = numpy.array(mesh.normals, 'f') mesh.gl["vbo"] = vbo.VBO(numpy.hstack((v,n))) # Fill the buffer for vertex positions mesh.gl["faces"] = glGenBuffers(1) glBindBuffer(GL_ELEMENT_ARRAY_BUFFER, mesh.gl["faces"]) glBufferData(GL_ELEMENT_ARRAY_BUFFER, mesh.faces, GL_STATIC_DRAW) glBindBuffer(GL_ELEMENT_ARRAY_BUFFER,0) def load_model(self, path, postprocess = aiProcessPreset_TargetRealtime_MaxQuality): 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) logger.info("Ready for 3D rendering!") def cycle_cameras(self): if not self.cameras: logger.info("No camera in the scene") return None self.current_cam_index = (self.current_cam_index + 1) % len(self.cameras) self.current_cam = self.cameras[self.current_cam_index] self.set_camera(self.current_cam) logger.info("Switched to camera <%s>" % self.current_cam) def set_camera_projection(self, camera = None): if not camera: camera = self.cameras[self.current_cam_index] 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() def set_camera(self, camera): self.set_camera_projection(camera) glMatrixMode(GL_MODELVIEW) glLoadIdentity() cam = transform([0.0, 0.0, 0.0], 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 render(self, wireframe = False, twosided = False): glEnable(GL_DEPTH_TEST) glDepthFunc(GL_LEQUAL) glPolygonMode(GL_FRONT_AND_BACK, GL_LINE if wireframe else GL_FILL) glDisable(GL_CULL_FACE) if twosided else glEnable(GL_CULL_FACE) shader = self.shader glUseProgram(shader) glUniform4f( shader.Global_ambient, .4,.2,.2,.1 ) glUniform4f( shader.Light_ambient, .4,.4,.4, 1.0 ) glUniform4f( shader.Light_diffuse, 1,1,1,1 ) glUniform3f( shader.Light_location, 2,2,10 ) self.recursive_render(self.scene.rootnode, shader) glUseProgram( 0 ) def recursive_render(self, node, shader): """ 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: stride = 24 # 6 * 4 bytes glUniform4f( shader.Material_diffuse, *mesh.material.properties["diffuse"] ) glUniform4f( shader.Material_ambient, *mesh.material.properties["ambient"] ) vbo = mesh.gl["vbo"] vbo.bind() glEnableVertexAttribArray( shader.Vertex_position ) glEnableVertexAttribArray( shader.Vertex_normal ) glVertexAttribPointer( shader.Vertex_position, 3, GL_FLOAT,False, stride, vbo ) glVertexAttribPointer( shader.Vertex_normal, 3, GL_FLOAT,False, stride, vbo+12 ) glBindBuffer(GL_ELEMENT_ARRAY_BUFFER, mesh.gl["faces"]) glDrawElements(GL_TRIANGLES, len(mesh.faces) * 3, GL_UNSIGNED_INT, None) vbo.unbind() glDisableVertexAttribArray( shader.Vertex_position ) glDisableVertexAttribArray( shader.Vertex_normal ) glBindBuffer(GL_ELEMENT_ARRAY_BUFFER, 0) for child in node.children: self.recursive_render(child, shader) glPopMatrix() def loop(self): pygame.display.flip() pygame.event.pump() self.keys = [k for k, pressed in enumerate(pygame.key.get_pressed()) if pressed] glClear(GL_COLOR_BUFFER_BIT | GL_DEPTH_BUFFER_BIT) # Compute FPS gl_time = glutGet(GLUT_ELAPSED_TIME) self.frames += 1 if gl_time - self.last_fps_time >= 1000: current_fps = self.frames * 1000 / (gl_time - self.last_fps_time) pygame.display.set_caption(self.base_name + " - %.0f fps" % current_fps) self.frames = 0 self.last_fps_time = gl_time return True def controls_3d(self, mouse_button=1, \ up_key=pygame.K_UP, \ down_key=pygame.K_DOWN, \ left_key=pygame.K_LEFT, \ right_key=pygame.K_RIGHT): """ The actual camera setting cycle """ mouse_dx,mouse_dy = pygame.mouse.get_rel() if pygame.mouse.get_pressed()[mouse_button]: look_speed = .2 buffer = glGetDoublev(GL_MODELVIEW_MATRIX) c = (-1 * numpy.mat(buffer[:3,:3]) * \ numpy.mat(buffer[3,:3]).T).reshape(3,1) # c is camera center in absolute coordinates, # we need to move it back to (0,0,0) # before rotating the camera glTranslate(c[0],c[1],c[2]) m = buffer.flatten() glRotate(mouse_dx * look_speed, m[1],m[5],m[9]) glRotate(mouse_dy * look_speed, m[0],m[4],m[8]) # compensate roll glRotated(-math.atan2(-m[4],m[5]) * \ 57.295779513082320876798154814105 ,m[2],m[6],m[10]) glTranslate(-c[0],-c[1],-c[2]) # move forward-back or right-left if up_key in self.keys: fwd = .1 elif down_key in self.keys: fwd = -.1 else: fwd = 0 if left_key in self.keys: strafe = .1 elif right_key in self.keys: strafe = -.1 else: strafe = 0 if abs(fwd) or abs(strafe): m = glGetDoublev(GL_MODELVIEW_MATRIX).flatten() glTranslate(fwd*m[2],fwd*m[6],fwd*m[10]) glTranslate(strafe*m[0],strafe*m[4],strafe*m[8]) if __name__ == '__main__': if not len(sys.argv) > 1: print("Usage: " + __file__ + " ") sys.exit(2) app = PyAssimp3DViewer(model = sys.argv[1], w = 1024, h = 768, fov = 75) while app.loop(): app.render() app.controls_3d(0) if pygame.K_f in app.keys: pygame.display.toggle_fullscreen() if pygame.K_TAB in app.keys: app.cycle_cameras() if pygame.K_ESCAPE in app.keys: break