diff --git a/port/PyAssimp/scripts/advanced_3d_viewer.py b/port/PyAssimp/scripts/advanced_3d_viewer.py new file mode 100755 index 000000000..33fa84524 --- /dev/null +++ b/port/PyAssimp/scripts/advanced_3d_viewer.py @@ -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__ + " ") + 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