|
@@ -0,0 +1,410 @@
|
|
|
+#!/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
|
|
|
+"""
|
|
|
+import sys
|
|
|
+
|
|
|
+import logging
|
|
|
+logger = logging.getLogger("underworlds.3d_viewer")
|
|
|
+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
|
|
|
+
|
|
|
+from pyassimp import core as 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__ + " <model>")
|
|
|
+ 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_s in app.keys: app.screenshot()
|
|
|
+ if pygame.K_v in app.keys: app.check_visibility()
|
|
|
+ if pygame.K_TAB in app.keys: app.cycle_cameras()
|
|
|
+ if pygame.K_ESCAPE in app.keys:
|
|
|
+ break
|