Browse Source

[pyassimp] Added a new pygame/sdl based OpenGL viewer

This viwer uses a shader-based pipeline and has working keyboard/mouse
controls.

Makes a starting point for 'modern' OpenGL apps.
Séverin Lemaignan 12 years ago
parent
commit
2cdda51455
1 changed files with 410 additions and 0 deletions
  1. 410 0
      port/PyAssimp/scripts/advanced_3d_viewer.py

+ 410 - 0
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__ + " <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