text: #!/usr/bin/env python
  # -*- coding: UTF-8 -*-
  
  """ This program loads a model with PyASSIMP, and display it.
  
  Based on:
  - pygame 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-2016
  """
  import sys
  import logging
  
  from functools import reduce
  
  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.arrays import vbo
  from OpenGL.GL import shaders
  
  import pygame
  import pygame.font
  import pygame.image
  
  import math, random
  from numpy import linalg
  
  import pyassimp
  from pyassimp.postprocess import *
  from pyassimp.helper import *
  import transformations
  
  ROTATION_180_X = numpy.array([[1, 0, 0, 0], [0, -1, 0, 0], [0, 0, -1, 0], [0, 0, 0, 1]], dtype=numpy.float32)
  
  # rendering mode
  BASE = "BASE"
  COLORS = "COLORS"
  SILHOUETTE = "SILHOUETTE"
  HELPERS = "HELPERS"
  
  # Entities type
  ENTITY = "entity"
  CAMERA = "camera"
  MESH = "mesh"
  
  FLAT_VERTEX_SHADER_120 = """
  #version 120
  
  uniform mat4 u_viewProjectionMatrix;
  uniform mat4 u_modelMatrix;
  
  uniform vec4 u_materialDiffuse;
  
  attribute vec3 a_vertex;
  
  varying vec4 v_color;
  
  void main(void)
  {
      v_color = u_materialDiffuse;
      gl_Position = u_viewProjectionMatrix * u_modelMatrix * vec4(a_vertex, 1.0);
  }
  """
  
  FLAT_VERTEX_SHADER_130 = """
  #version 130
  
  uniform mat4 u_viewProjectionMatrix;
  uniform mat4 u_modelMatrix;
  
  uniform vec4 u_materialDiffuse;
  
  in vec3 a_vertex;
  
  out vec4 v_color;
  
  void main(void)
  {
      v_color = u_materialDiffuse;
      gl_Position = u_viewProjectionMatrix * u_modelMatrix * vec4(a_vertex, 1.0);
  }
  """
  
  BASIC_VERTEX_SHADER_120 = """
  #version 120
  
  uniform mat4 u_viewProjectionMatrix;
  uniform mat4 u_modelMatrix;
  uniform mat3 u_normalMatrix;
  uniform vec3 u_lightPos;
  
  uniform vec4 u_materialDiffuse;
  
  attribute vec3 a_vertex;
  attribute vec3 a_normal;
  
  varying vec4 v_color;
  
  void main(void)
  {
      // Now the normal is in world space, as we pass the light in world space.
      vec3 normal = u_normalMatrix * a_normal;
  
      float dist = distance(a_vertex, u_lightPos);
  
      // go to https://www.desmos.com/calculator/nmnaud1hrw to play with the parameters
      // att is not used for now
      float att=1.0/(1.0+0.8*dist*dist);
  
      vec3 surf2light = normalize(u_lightPos - a_vertex);
      vec3 norm = normalize(normal);
      float dcont=max(0.0,dot(norm,surf2light));
  
      float ambient = 0.3;
      float intensity = dcont + 0.3 + ambient;
  
      v_color = u_materialDiffuse  * intensity;
  
      gl_Position = u_viewProjectionMatrix * u_modelMatrix * vec4(a_vertex, 1.0);
  }
  """
  
  BASIC_VERTEX_SHADER_130 = """
  #version 130
  
  uniform mat4 u_viewProjectionMatrix;
  uniform mat4 u_modelMatrix;
  uniform mat3 u_normalMatrix;
  uniform vec3 u_lightPos;
  
  uniform vec4 u_materialDiffuse;
  
  in vec3 a_vertex;
  in vec3 a_normal;
  
  out vec4 v_color;
  
  void main(void)
  {
      // Now the normal is in world space, as we pass the light in world space.
      vec3 normal = u_normalMatrix * a_normal;
  
      float dist = distance(a_vertex, u_lightPos);
  
      // go to https://www.desmos.com/calculator/nmnaud1hrw to play with the parameters
      // att is not used for now
      float att=1.0/(1.0+0.8*dist*dist);
  
      vec3 surf2light = normalize(u_lightPos - a_vertex);
      vec3 norm = normalize(normal);
      float dcont=max(0.0,dot(norm,surf2light));
  
      float ambient = 0.3;
      float intensity = dcont + 0.3 + ambient;
  
      v_color = u_materialDiffuse  * intensity;
  
      gl_Position = u_viewProjectionMatrix * u_modelMatrix * vec4(a_vertex, 1.0);
  }
  """
  
  BASIC_FRAGMENT_SHADER_120 = """
  #version 120
  
  varying vec4 v_color;
  
  void main() {
      gl_FragColor = v_color;
  }
  """
  
  BASIC_FRAGMENT_SHADER_130 = """
  #version 130
  
  in vec4 v_color;
  
  void main() {
      gl_FragColor = v_color;
  }
  """
  
  GOOCH_VERTEX_SHADER_120 = """
  #version 120
  
  // attributes
  attribute vec3 a_vertex; // xyz - position
  attribute vec3 a_normal; // xyz - normal
  
  // uniforms
  uniform mat4 u_modelMatrix;
  uniform mat4 u_viewProjectionMatrix;
  uniform mat3 u_normalMatrix;
  uniform vec3 u_lightPos;
  uniform vec3 u_camPos;
  
  // output data from vertex to fragment shader
  varying vec3 o_normal;
  varying vec3 o_lightVector;
  
  ///////////////////////////////////////////////////////////////////
  
  void main(void)
  {
     // transform position and normal to world space
     vec4 positionWorld = u_modelMatrix * vec4(a_vertex, 1.0);
     vec3 normalWorld = u_normalMatrix * a_normal;
  
     // calculate and pass vectors required for lighting
     o_lightVector = u_lightPos - positionWorld.xyz;
     o_normal = normalWorld;
  
     // project world space position to the screen and output it
     gl_Position = u_viewProjectionMatrix * positionWorld;
  }
  """
  
  GOOCH_VERTEX_SHADER_130 = """
  #version 130
  
  // attributes
  in vec3 a_vertex; // xyz - position
  in vec3 a_normal; // xyz - normal
  
  // uniforms
  uniform mat4 u_modelMatrix;
  uniform mat4 u_viewProjectionMatrix;
  uniform mat3 u_normalMatrix;
  uniform vec3 u_lightPos;
  uniform vec3 u_camPos;
  
  // output data from vertex to fragment shader
  out vec3 o_normal;
  out vec3 o_lightVector;
  
  ///////////////////////////////////////////////////////////////////
  
  void main(void)
  {
     // transform position and normal to world space
     vec4 positionWorld = u_modelMatrix * vec4(a_vertex, 1.0);
     vec3 normalWorld = u_normalMatrix * a_normal;
  
     // calculate and pass vectors required for lighting
     o_lightVector = u_lightPos - positionWorld.xyz;
     o_normal = normalWorld;
  
     // project world space position to the screen and output it
     gl_Position = u_viewProjectionMatrix * positionWorld;
  }
  """
  
  GOOCH_FRAGMENT_SHADER_120 = """
  #version 120
  
  // data from vertex shader
  varying vec3 o_normal;
  varying vec3 o_lightVector;
  
  // diffuse color of the object
  uniform vec4 u_materialDiffuse;
  // cool color of gooch shading
  uniform vec3 u_coolColor;
  // warm color of gooch shading
  uniform vec3 u_warmColor;
  // how much to take from object color in final cool color
  uniform float u_alpha;
  // how much to take from object color in final warm color
  uniform float u_beta;
  
  ///////////////////////////////////////////////////////////
  
  void main(void)
  {
     // normlize vectors for lighting
     vec3 normalVector = normalize(o_normal);
     vec3 lightVector = normalize(o_lightVector);
     // intensity of diffuse lighting [-1, 1]
     float diffuseLighting = dot(lightVector, normalVector);
     // map intensity of lighting from range [-1; 1] to [0, 1]
     float interpolationValue = (1.0 + diffuseLighting)/2;
  
     //////////////////////////////////////////////////////////////////
  
     // cool color mixed with color of the object
     vec3 coolColorMod = u_coolColor + vec3(u_materialDiffuse) * u_alpha;
     // warm color mixed with color of the object
     vec3 warmColorMod = u_warmColor + vec3(u_materialDiffuse) * u_beta;
     // interpolation of cool and warm colors according
     // to lighting intensity. The lower the light intensity,
     // the larger part of the cool color is used
     vec3 colorOut = mix(coolColorMod, warmColorMod, interpolationValue);
  
     //////////////////////////////////////////////////////////////////
  
     // save color
     gl_FragColor.rgb = colorOut;
     gl_FragColor.a = 1;
  }
  """
  
  GOOCH_FRAGMENT_SHADER_130 = """
  #version 130
  
  // data from vertex shader
  in vec3 o_normal;
  in vec3 o_lightVector;
  
  // diffuse color of the object
  uniform vec4 u_materialDiffuse;
  // cool color of gooch shading
  uniform vec3 u_coolColor;
  // warm color of gooch shading
  uniform vec3 u_warmColor;
  // how much to take from object color in final cool color
  uniform float u_alpha;
  // how much to take from object color in final warm color
  uniform float u_beta;
  
  // output to framebuffer
  out vec4 resultingColor;
  
  ///////////////////////////////////////////////////////////
  
  void main(void)
  {
     // normlize vectors for lighting
     vec3 normalVector = normalize(o_normal);
     vec3 lightVector = normalize(o_lightVector);
     // intensity of diffuse lighting [-1, 1]
     float diffuseLighting = dot(lightVector, normalVector);
     // map intensity of lighting from range [-1; 1] to [0, 1]
     float interpolationValue = (1.0 + diffuseLighting)/2;
  
     //////////////////////////////////////////////////////////////////
  
     // cool color mixed with color of the object
     vec3 coolColorMod = u_coolColor + vec3(u_materialDiffuse) * u_alpha;
     // warm color mixed with color of the object
     vec3 warmColorMod = u_warmColor + vec3(u_materialDiffuse) * u_beta;
     // interpolation of cool and warm colors according
     // to lighting intensity. The lower the light intensity,
     // the larger part of the cool color is used
     vec3 colorOut = mix(coolColorMod, warmColorMod, interpolationValue);
  
     //////////////////////////////////////////////////////////////////
  
     // save color
     resultingColor.rgb = colorOut;
     resultingColor.a = 1;
  }
  """
  
  SILHOUETTE_VERTEX_SHADER_120 = """
  #version 120
  
  attribute vec3 a_vertex; // xyz - position
  attribute vec3 a_normal; // xyz - normal
  
  uniform mat4 u_modelMatrix;
  uniform mat4 u_viewProjectionMatrix;
  uniform mat4 u_modelViewMatrix;
  uniform vec4 u_materialDiffuse;
  uniform float u_bordersize; // width of the border
  
  varying vec4 v_color;
  
  void main(void){
     v_color = u_materialDiffuse;
     float distToCamera = -(u_modelViewMatrix * vec4(a_vertex, 1.0)).z;
     vec4 tPos   = vec4(a_vertex + a_normal * u_bordersize * distToCamera, 1.0);
     gl_Position = u_viewProjectionMatrix * u_modelMatrix * tPos;
  }
  """
  
  SILHOUETTE_VERTEX_SHADER_130 = """
  #version 130
  
  in vec3 a_vertex; // xyz - position
  in vec3 a_normal; // xyz - normal
  
  uniform mat4 u_modelMatrix;
  uniform mat4 u_viewProjectionMatrix;
  uniform mat4 u_modelViewMatrix;
  uniform vec4 u_materialDiffuse;
  uniform float u_bordersize; // width of the border
  
  out vec4 v_color;
  
  void main(void){
     v_color = u_materialDiffuse;
     float distToCamera = -(u_modelViewMatrix * vec4(a_vertex, 1.0)).z;
     vec4 tPos   = vec4(a_vertex + a_normal * u_bordersize * distToCamera, 1.0);
     gl_Position = u_viewProjectionMatrix * u_modelMatrix * tPos;
  }
  """
  DEFAULT_CLIP_PLANE_NEAR = 0.001
  DEFAULT_CLIP_PLANE_FAR = 1000.0
  
  
  def get_world_transform(scene, node):
      if node == scene.rootnode:
          return numpy.identity(4, dtype=numpy.float32)
  
      parents = reversed(_get_parent_chain(scene, node, []))
      parent_transform = reduce(numpy.dot, [p.transformation for p in parents])
      return numpy.dot(parent_transform, node.transformation)
  
  
  def _get_parent_chain(scene, node, parents):
      parent = node.parent
  
      parents.append(parent)
  
      if parent == scene.rootnode:
          return parents
  
      return _get_parent_chain(scene, parent, parents)
  
  
  class DefaultCamera:
      def __init__(self, w, h, fov):
          self.name = "default camera"
          self.type = CAMERA
          self.clipplanenear = DEFAULT_CLIP_PLANE_NEAR
          self.clipplanefar = DEFAULT_CLIP_PLANE_FAR
          self.aspect = w / h
          self.horizontalfov = fov * math.pi / 180
          self.transformation = numpy.array([[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.]], dtype=numpy.float32)
  
          self.transformation = numpy.dot(self.transformation, ROTATION_180_X)
  
      def __str__(self):
          return self.name
  
  
  class PyAssimp3DViewer:
      base_name = "PyASSIMP 3D viewer"
  
      def __init__(self, model, w=1024, h=768):
  
          self.w = w
          self.h = h
  
          pygame.init()
          pygame.display.set_caption(self.base_name)
          pygame.display.set_mode((w, h), pygame.OPENGL | pygame.DOUBLEBUF)
  
          glClearColor(0.18, 0.18, 0.18, 1.0)
  
          shader_compilation_succeeded = False
          try:
              self.set_shaders_v130()
              self.prepare_shaders()
          except RuntimeError as message:
              sys.stderr.write("%s\n" % message)
              sys.stdout.write("Could not compile shaders in version 1.30, trying version 1.20\n")
  
          if not shader_compilation_succeeded:
              self.set_shaders_v120()
              self.prepare_shaders()
  
          self.scene = None
          self.meshes = {}  # stores the OpenGL vertex/faces/normals buffers pointers
  
          self.node2colorid = {}  # stores a color ID for each node. Useful for mouse picking and visibility checking
          self.colorid2node = {}  # reverse dict of node2colorid
  
          self.currently_selected = None
          self.moving = False
          self.moving_situation = None
  
          self.default_camera = DefaultCamera(self.w, self.h, fov=70)
          self.cameras = [self.default_camera]
  
          self.current_cam_index = 0
          self.current_cam = self.default_camera
          self.set_camera_projection()
  
          self.load_model(model)
  
          # user interactions
          self.focal_point = [0, 0, 0]
          self.is_rotating = False
          self.is_panning = False
          self.is_zooming = False
  
      def set_shaders_v120(self):
        self.BASIC_VERTEX_SHADER = BASIC_VERTEX_SHADER_120
        self.FLAT_VERTEX_SHADER = FLAT_VERTEX_SHADER_120
        self.SILHOUETTE_VERTEX_SHADER = SILHOUETTE_VERTEX_SHADER_120
        self.GOOCH_VERTEX_SHADER = GOOCH_VERTEX_SHADER_120
  
        self.BASIC_FRAGMENT_SHADER = BASIC_FRAGMENT_SHADER_120
        self.GOOCH_FRAGMENT_SHADER = GOOCH_FRAGMENT_SHADER_120
  
      def set_shaders_v130(self):
        self.BASIC_VERTEX_SHADER = BASIC_VERTEX_SHADER_130
        self.FLAT_VERTEX_SHADER = FLAT_VERTEX_SHADER_130
        self.SILHOUETTE_VERTEX_SHADER = SILHOUETTE_VERTEX_SHADER_130
        self.GOOCH_VERTEX_SHADER = GOOCH_VERTEX_SHADER_130
  
        self.BASIC_FRAGMENT_SHADER = BASIC_FRAGMENT_SHADER_130
        self.GOOCH_FRAGMENT_SHADER = GOOCH_FRAGMENT_SHADER_130
  
      def prepare_shaders(self):
  
          ### Base shader
          vertex = shaders.compileShader(self.BASIC_VERTEX_SHADER, GL_VERTEX_SHADER)
          fragment = shaders.compileShader(self.BASIC_FRAGMENT_SHADER, GL_FRAGMENT_SHADER)
  
          self.shader = shaders.compileProgram(vertex, fragment)
  
          self.set_shader_accessors(('u_modelMatrix',
                                     'u_viewProjectionMatrix',
                                     'u_normalMatrix',
                                     'u_lightPos',
                                     'u_materialDiffuse'),
                                    ('a_vertex',
                                     'a_normal'), self.shader)
  
          ### Flat shader
          flatvertex = shaders.compileShader(self.FLAT_VERTEX_SHADER, GL_VERTEX_SHADER)
          self.flatshader = shaders.compileProgram(flatvertex, fragment)
  
          self.set_shader_accessors(('u_modelMatrix',
                                     'u_viewProjectionMatrix',
                                     'u_materialDiffuse',),
                                    ('a_vertex',), self.flatshader)
  
          ### Silhouette shader
          silh_vertex = shaders.compileShader(self.SILHOUETTE_VERTEX_SHADER, GL_VERTEX_SHADER)
          self.silhouette_shader = shaders.compileProgram(silh_vertex, fragment)
  
          self.set_shader_accessors(('u_modelMatrix',
                                     'u_viewProjectionMatrix',
                                     'u_modelViewMatrix',
                                     'u_materialDiffuse',
                                     'u_bordersize'  # width of the silhouette
                                     ),
                                    ('a_vertex',
                                     'a_normal'), self.silhouette_shader)
  
          ### Gooch shader
          gooch_vertex = shaders.compileShader(self.GOOCH_VERTEX_SHADER, GL_VERTEX_SHADER)
          gooch_fragment = shaders.compileShader(self.GOOCH_FRAGMENT_SHADER, GL_FRAGMENT_SHADER)
          self.gooch_shader = shaders.compileProgram(gooch_vertex, gooch_fragment)
  
          self.set_shader_accessors(('u_modelMatrix',
                                     'u_viewProjectionMatrix',
                                     'u_normalMatrix',
                                     'u_lightPos',
                                     'u_materialDiffuse',
                                     'u_coolColor',
                                     'u_warmColor',
                                     'u_alpha',
                                     'u_beta'
                                     ),
                                    ('a_vertex',
                                     'a_normal'), self.gooch_shader)
  
      @staticmethod
      def set_shader_accessors(uniforms, attributes, shader):
          # add accessors to the shaders uniforms and attributes
          for uniform in uniforms:
              location = glGetUniformLocation(shader, uniform)
              if location in (None, -1):
                  raise RuntimeError('No uniform: %s (maybe it is not used '
                                     'anymore and has been optimized out by'
                                     ' the shader compiler)' % uniform)
              setattr(shader, uniform, location)
  
          for attribute in attributes:
              location = glGetAttribLocation(shader, attribute)
              if location in (None, -1):
                  raise RuntimeError('No attribute: %s' % attribute)
              setattr(shader, attribute, location)
  
      @staticmethod
      def prepare_gl_buffers(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,
                       numpy.array(mesh.faces, dtype=numpy.int32),
                       GL_STATIC_DRAW)
  
          mesh.gl["nbfaces"] = len(mesh.faces)
  
          # Unbind buffers
          glBindBuffer(GL_ARRAY_BUFFER, 0)
          glBindBuffer(GL_ELEMENT_ARRAY_BUFFER, 0)
  
      @staticmethod
      def get_rgb_from_colorid(colorid):
          r = (colorid >> 0) & 0xff
          g = (colorid >> 8) & 0xff
          b = (colorid >> 16) & 0xff
  
          return r, g, b
  
      def get_color_id(self):
          id = random.randint(0, 256 * 256 * 256)
          if id not in self.colorid2node:
              return id
          else:
              return self.get_color_id()
  
      def glize(self, scene, node):
  
          logger.info("Loading node <%s>" % node)
          node.selected = True if self.currently_selected and self.currently_selected == node else False
  
          node.transformation = node.transformation.astype(numpy.float32)
  
          if node.meshes:
              node.type = MESH
              colorid = self.get_color_id()
              self.colorid2node[colorid] = node
              self.node2colorid[node.name] = colorid
  
          elif node.name in [c.name for c in scene.cameras]:
  
              # retrieve the ASSIMP camera object
              [cam] = [c for c in scene.cameras if c.name == node.name]
              node.type = CAMERA
              logger.info("Added camera <%s>" % node.name)
              logger.info("Camera position: %.3f, %.3f, %.3f" % tuple(node.transformation[:, 3][:3].tolist()))
              self.cameras.append(node)
              node.clipplanenear = cam.clipplanenear
              node.clipplanefar = cam.clipplanefar
  
              if numpy.allclose(cam.lookat, [0, 0, -1]) and numpy.allclose(cam.up, [0, 1, 0]):  # Cameras in .blend files
  
                  # Rotate by 180deg around X to have Z pointing forward
                  node.transformation = numpy.dot(node.transformation, ROTATION_180_X)
              else:
                  raise RuntimeError(
                      "I do not know how to normalize this camera orientation: lookat=%s, up=%s" % (cam.lookat, cam.up))
  
              if cam.aspect == 0.0:
                  logger.warning("Camera aspect not set. Setting to default 4:3")
                  node.aspect = 1.333
              else:
                  node.aspect = cam.aspect
  
              node.horizontalfov = cam.horizontalfov
  
          else:
              node.type = ENTITY
  
          for child in node.children:
              self.glize(scene, child)
  
      def load_model(self, path, postprocess=aiProcessPreset_TargetRealtime_MaxQuality):
          logger.info("Loading model:" + path + "...")
  
          if postprocess:
              self.scene = pyassimp.load(path, processing=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)
  
          self.glize(scene, scene.rootnode)
  
          # Finally release the model
          pyassimp.release(scene)
          logger.info("Ready for 3D rendering!")
  
      def cycle_cameras(self):
  
          self.current_cam_index = (self.current_cam_index + 1) % len(self.cameras)
          self.current_cam = self.cameras[self.current_cam_index]
          self.set_camera_projection(self.current_cam)
          logger.info("Switched to camera <%s>" % self.current_cam)
  
      def set_overlay_projection(self):
          glViewport(0, 0, self.w, self.h)
          glMatrixMode(GL_PROJECTION)
          glLoadIdentity()
          glOrtho(0.0, self.w - 1.0, 0.0, self.h - 1.0, -1.0, 1.0)
          glMatrixMode(GL_MODELVIEW)
          glLoadIdentity()
  
      def set_camera_projection(self, camera=None):
  
          if not camera:
              camera = self.current_cam
  
          znear = camera.clipplanenear or DEFAULT_CLIP_PLANE_NEAR
          zfar = camera.clipplanefar or DEFAULT_CLIP_PLANE_FAR
          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)
  
          self.projection_matrix = glGetFloatv(GL_PROJECTION_MATRIX).transpose()
  
          glMatrixMode(GL_MODELVIEW)
          glLoadIdentity()
  
      def render_colors(self):
  
          glEnable(GL_DEPTH_TEST)
          glDepthFunc(GL_LEQUAL)
  
          glPolygonMode(GL_FRONT_AND_BACK, GL_FILL)
          glEnable(GL_CULL_FACE)
  
          glUseProgram(self.flatshader)
  
          glUniformMatrix4fv(self.flatshader.u_viewProjectionMatrix, 1, GL_TRUE,
                             numpy.dot(self.projection_matrix, self.view_matrix))
  
          self.recursive_render(self.scene.rootnode, self.flatshader, mode=COLORS)
  
          glUseProgram(0)
  
      def get_hovered_node(self, mousex, mousey):
          """
          Attention: The performances of this method relies heavily on the size of the display!
          """
  
          # mouse out of the window?
          if mousex < 0 or mousex >= self.w or mousey < 0 or mousey >= self.h:
              return None
  
          self.render_colors()
          # Capture image from the OpenGL buffer
          buf = (GLubyte * (3 * self.w * self.h))(0)
          glReadPixels(0, 0, self.w, self.h, GL_RGB, GL_UNSIGNED_BYTE, buf)
  
          # Reinterpret the RGB pixel buffer as a 1-D array of 24bits colors
          a = numpy.ndarray(len(buf), numpy.dtype('>u1'), buf)
          colors = numpy.zeros(len(buf) // 3, numpy.dtype('<u4'))
          for i in range(3):
              colors.view(dtype='>u1')[i::4] = a.view(dtype='>u1')[i::3]
  
          colorid = colors[mousex + mousey * self.w]
  
          glClear(GL_COLOR_BUFFER_BIT | GL_DEPTH_BUFFER_BIT)
  
          if colorid in self.colorid2node:
              return self.colorid2node[colorid]
  
      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)
  
          self.render_grid()
  
          self.recursive_render(self.scene.rootnode, None, mode=HELPERS)
  
          ### First, the silhouette
  
          if False:
              shader = self.silhouette_shader
  
              # glDepthMask(GL_FALSE)
              glCullFace(GL_FRONT)  # cull front faces
  
              glUseProgram(shader)
              glUniform1f(shader.u_bordersize, 0.01)
  
              glUniformMatrix4fv(shader.u_viewProjectionMatrix, 1, GL_TRUE,
                                 numpy.dot(self.projection_matrix, self.view_matrix))
  
              self.recursive_render(self.scene.rootnode, shader, mode=SILHOUETTE)
  
              glUseProgram(0)
  
          ### Then, inner shading
          # glDepthMask(GL_TRUE)
          glCullFace(GL_BACK)
  
          use_gooch = False
          if use_gooch:
              shader = self.gooch_shader
  
              glUseProgram(shader)
              glUniform3f(shader.u_lightPos, -.5, -.5, .5)
  
              ##### GOOCH specific
              glUniform3f(shader.u_coolColor, 159.0 / 255, 148.0 / 255, 255.0 / 255)
              glUniform3f(shader.u_warmColor, 255.0 / 255, 75.0 / 255, 75.0 / 255)
              glUniform1f(shader.u_alpha, .25)
              glUniform1f(shader.u_beta, .25)
              #########
          else:
              shader = self.shader
              glUseProgram(shader)
              glUniform3f(shader.u_lightPos, -.5, -.5, .5)
  
          glUniformMatrix4fv(shader.u_viewProjectionMatrix, 1, GL_TRUE,
                             numpy.dot(self.projection_matrix, self.view_matrix))
  
          self.recursive_render(self.scene.rootnode, shader)
  
          glUseProgram(0)
  
      def render_axis(self,
                      transformation=numpy.identity(4, dtype=numpy.float32),
                      label=None,
                      size=0.2,
                      selected=False):
          m = transformation.transpose()  # OpenGL row major
  
          glPushMatrix()
          glMultMatrixf(m)
  
          glLineWidth(3 if selected else 1)
  
          size = 2 * size if selected else size
  
          glBegin(GL_LINES)
  
          # draw line for x axis
          glColor3f(1.0, 0.0, 0.0)
          glVertex3f(0.0, 0.0, 0.0)
          glVertex3f(size, 0.0, 0.0)
  
          # draw line for y axis
          glColor3f(0.0, 1.0, 0.0)
          glVertex3f(0.0, 0.0, 0.0)
          glVertex3f(0.0, size, 0.0)
  
          # draw line for Z axis
          glColor3f(0.0, 0.0, 1.0)
          glVertex3f(0.0, 0.0, 0.0)
          glVertex3f(0.0, 0.0, size)
  
          glEnd()
  
          if label:
              self.showtext(label)
  
          glPopMatrix()
  
      @staticmethod
      def render_camera(camera, transformation):
  
          m = transformation.transpose()  # OpenGL row major
  
          aspect = camera.aspect
  
          u = 0.1  # unit size (in m)
          l = 3 * u  # length of the camera cone
          f = 3 * u  # aperture of the camera cone
  
          glPushMatrix()
          glMultMatrixf(m)
  
          glLineWidth(2)
          glBegin(GL_LINE_STRIP)
  
          glColor3f(.2, .2, .2)
  
          glVertex3f(u, u, -u)
          glVertex3f(u, -u, -u)
          glVertex3f(-u, -u, -u)
          glVertex3f(-u, u, -u)
          glVertex3f(u, u, -u)
  
          glVertex3f(u, u, 0.0)
          glVertex3f(u, -u, 0.0)
          glVertex3f(-u, -u, 0.0)
          glVertex3f(-u, u, 0.0)
          glVertex3f(u, u, 0.0)
  
          glVertex3f(f * aspect, f, l)
          glVertex3f(f * aspect, -f, l)
          glVertex3f(-f * aspect, -f, l)
          glVertex3f(-f * aspect, f, l)
          glVertex3f(f * aspect, f, l)
  
          glEnd()
  
          glBegin(GL_LINE_STRIP)
          glVertex3f(u, -u, -u)
          glVertex3f(u, -u, 0.0)
          glVertex3f(f * aspect, -f, l)
          glEnd()
  
          glBegin(GL_LINE_STRIP)
          glVertex3f(-u, -u, -u)
          glVertex3f(-u, -u, 0.0)
          glVertex3f(-f * aspect, -f, l)
          glEnd()
  
          glBegin(GL_LINE_STRIP)
          glVertex3f(-u, u, -u)
          glVertex3f(-u, u, 0.0)
          glVertex3f(-f * aspect, f, l)
          glEnd()
  
          glPopMatrix()
  
      @staticmethod
      def render_grid():
  
          glLineWidth(1)
          glColor3f(0.5, 0.5, 0.5)
          glBegin(GL_LINES)
          for i in range(-10, 11):
              glVertex3f(i, -10.0, 0.0)
              glVertex3f(i, 10.0, 0.0)
  
          for i in range(-10, 11):
              glVertex3f(-10.0, i, 0.0)
              glVertex3f(10.0, i, 0.0)
          glEnd()
  
      def recursive_render(self, node, shader, mode=BASE, with_normals=True):
          """ Main recursive rendering method.
          """
  
          normals = with_normals
  
          if mode == COLORS:
              normals = False
  
  
          if not hasattr(node, "selected"):
              node.selected = False
  
          m = get_world_transform(self.scene, node)
  
          # HELPERS mode
          ###
          if mode == HELPERS:
              # if node.type == ENTITY:
              self.render_axis(m,
                               label=node.name if node != self.scene.rootnode else None,
                               selected=node.selected if hasattr(node, "selected") else False)
  
              if node.type == CAMERA:
                  self.render_camera(node, m)
  
              for child in node.children:
                      self.recursive_render(child, shader, mode)
  
              return
  
          # Mesh rendering modes
          ###
          if node.type == MESH:
  
              for mesh in node.meshes:
  
                  stride = 24  # 6 * 4 bytes
  
                  if node.selected and mode == SILHOUETTE:
                      glUniform4f(shader.u_materialDiffuse, 1.0, 0.0, 0.0, 1.0)
                      glUniformMatrix4fv(shader.u_modelViewMatrix, 1, GL_TRUE,
                                         numpy.dot(self.view_matrix, m))
  
                  else:
                      if mode == COLORS:
                          colorid = self.node2colorid[node.name]
                          r, g, b = self.get_rgb_from_colorid(colorid)
                          glUniform4f(shader.u_materialDiffuse, r / 255.0, g / 255.0, b / 255.0, 1.0)
                      elif mode == SILHOUETTE:
                          glUniform4f(shader.u_materialDiffuse, .0, .0, .0, 1.0)
                      else:
                          if node.selected:
                              diffuse = (1.0, 0.0, 0.0, 1.0)  # selected nodes in red
                          else:
                              diffuse = mesh.material.properties["diffuse"]
                          if len(diffuse) == 3:  # RGB instead of expected RGBA
                              diffuse.append(1.0)
                          glUniform4f(shader.u_materialDiffuse, *diffuse)
                          # if ambient:
                          #    glUniform4f( shader.Material_ambient, *mat["ambient"] )
  
                  if mode == BASE:  # not in COLORS or SILHOUETTE
                      normal_matrix = linalg.inv(numpy.dot(self.view_matrix, m)[0:3, 0:3]).transpose()
                      glUniformMatrix3fv(shader.u_normalMatrix, 1, GL_TRUE, normal_matrix)
  
                  glUniformMatrix4fv(shader.u_modelMatrix, 1, GL_TRUE, m)
  
                  vbo = mesh.gl["vbo"]
                  vbo.bind()
  
                  glEnableVertexAttribArray(shader.a_vertex)
                  if normals:
                      glEnableVertexAttribArray(shader.a_normal)
  
                  glVertexAttribPointer(
                      shader.a_vertex,
                      3, GL_FLOAT, False, stride, vbo
                  )
  
                  if normals:
                      glVertexAttribPointer(
                          shader.a_normal,
                          3, GL_FLOAT, False, stride, vbo + 12
                      )
  
                  glBindBuffer(GL_ELEMENT_ARRAY_BUFFER, mesh.gl["faces"])
                  glDrawElements(GL_TRIANGLES, mesh.gl["nbfaces"] * 3, GL_UNSIGNED_INT, None)
  
                  vbo.unbind()
                  glDisableVertexAttribArray(shader.a_vertex)
  
                  if normals:
                      glDisableVertexAttribArray(shader.a_normal)
  
                  glBindBuffer(GL_ELEMENT_ARRAY_BUFFER, 0)
  
          for child in node.children:
              self.recursive_render(child, shader, mode)
  
  
      def switch_to_overlay(self):
          glPushMatrix()
          self.set_overlay_projection()
  
      def switch_from_overlay(self):
          self.set_camera_projection()
          glPopMatrix()
  
      def select_node(self, node):
          self.currently_selected = node
          self.update_node_select(self.scene.rootnode)
  
      def update_node_select(self, node):
          if node is self.currently_selected:
              node.selected = True
          else:
              node.selected = False
  
          for child in node.children:
              self.update_node_select(child)
  
      def loop(self):
  
          pygame.display.flip()
  
          if not self.process_events():
              return False  # ESC has been pressed
  
          glClear(GL_COLOR_BUFFER_BIT | GL_DEPTH_BUFFER_BIT)
  
          return True
  
      def process_events(self):
  
          LEFT_BUTTON = 1
          MIDDLE_BUTTON = 2
          RIGHT_BUTTON = 3
          WHEEL_UP = 4
          WHEEL_DOWN = 5
  
          dx, dy = pygame.mouse.get_rel()
          mousex, mousey = pygame.mouse.get_pos()
  
          zooming_one_shot = False
  
          ok = True
  
          for evt in pygame.event.get():
              if evt.type == pygame.MOUSEBUTTONDOWN and evt.button == LEFT_BUTTON:
                  hovered = self.get_hovered_node(mousex, self.h - mousey)
                  if hovered:
                      if self.currently_selected and self.currently_selected == hovered:
                          self.select_node(None)
                      else:
                          logger.info("Node %s selected" % hovered)
                          self.select_node(hovered)
                  else:
                      self.is_rotating = True
              if evt.type == pygame.MOUSEBUTTONUP and evt.button == LEFT_BUTTON:
                  self.is_rotating = False
  
              if evt.type == pygame.MOUSEBUTTONDOWN and evt.button == MIDDLE_BUTTON:
                  self.is_panning = True
              if evt.type == pygame.MOUSEBUTTONUP and evt.button == MIDDLE_BUTTON:
                  self.is_panning = False
  
              if evt.type == pygame.MOUSEBUTTONDOWN and evt.button == RIGHT_BUTTON:
                  self.is_zooming = True
              if evt.type == pygame.MOUSEBUTTONUP and evt.button == RIGHT_BUTTON:
                  self.is_zooming = False
  
              if evt.type == pygame.MOUSEBUTTONDOWN and evt.button in [WHEEL_UP, WHEEL_DOWN]:
                  zooming_one_shot = True
                  self.is_zooming = True
                  dy = -10 if evt.button == WHEEL_UP else 10
  
              if evt.type == pygame.KEYDOWN:
                  ok = (ok and self.process_keystroke(evt.key, evt.mod))
  
          self.controls_3d(dx, dy, zooming_one_shot)
  
          return ok
  
      def process_keystroke(self, key, mod):
  
          # process arrow keys if an object is selected
          if self.currently_selected:
              up = 0
              strafe = 0
  
              if key == pygame.K_UP:
                  up = 1
              if key == pygame.K_DOWN:
                  up = -1
              if key == pygame.K_LEFT:
                  strafe = -1
              if key == pygame.K_RIGHT:
                  strafe = 1
  
              self.move_selected_node(up, strafe)
  
          if key == pygame.K_f:
              pygame.display.toggle_fullscreen()
  
          if key == pygame.K_TAB:
              self.cycle_cameras()
  
          if key in [pygame.K_ESCAPE, pygame.K_q]:
              return False
  
          return True
  
      def controls_3d(self, dx, dy, zooming_one_shot=False):
          """ Orbiting the camera is implemented the following way:
  
          - the rotation is split into a rotation around the *world* Z axis
            (controlled by the horizontal mouse motion along X) and a
            rotation around the *X* axis of the camera (pitch) *shifted to
            the focal origin* (the world origin for now). This is controlled
            by the vertical motion of the mouse (Y axis).
          - as a result, the resulting transformation of the camera in the
            world frame C' is:
              C' = (T · Rx · T⁻¹ · (Rz · C)⁻¹)⁻¹
            where:
            - C is the original camera transformation in the world frame,
            - Rz is the rotation along the Z axis (in the world frame)
            - T is the translation camera -> world (ie, the inverse of the
              translation part of C
            - Rx is the rotation around X in the (translated) camera frame """
  
          CAMERA_TRANSLATION_FACTOR = 0.01
          CAMERA_ROTATION_FACTOR = 0.01
  
          if not (self.is_rotating or self.is_panning or self.is_zooming):
              return
  
          current_pos = self.current_cam.transformation[:3, 3].copy()
          distance = numpy.linalg.norm(self.focal_point - current_pos)
  
          if self.is_rotating:
              rotation_camera_x = dy * CAMERA_ROTATION_FACTOR
              rotation_world_z = dx * CAMERA_ROTATION_FACTOR
              world_z_rotation = transformations.euler_matrix(0, 0, rotation_world_z)
              cam_x_rotation = transformations.euler_matrix(rotation_camera_x, 0, 0)
  
              after_world_z_rotation = numpy.dot(world_z_rotation, self.current_cam.transformation)
  
              inverse_transformation = transformations.inverse_matrix(after_world_z_rotation)
  
              translation = transformations.translation_matrix(
                  transformations.decompose_matrix(inverse_transformation)[3])
              inverse_translation = transformations.inverse_matrix(translation)
  
              new_inverse = numpy.dot(inverse_translation, inverse_transformation)
              new_inverse = numpy.dot(cam_x_rotation, new_inverse)
              new_inverse = numpy.dot(translation, new_inverse)
  
              self.current_cam.transformation = transformations.inverse_matrix(new_inverse).astype(numpy.float32)
  
          if self.is_panning:
              tx = -dx * CAMERA_TRANSLATION_FACTOR * distance
              ty = dy * CAMERA_TRANSLATION_FACTOR * distance
              cam_transform = transformations.translation_matrix((tx, ty, 0)).astype(numpy.float32)
              self.current_cam.transformation = numpy.dot(self.current_cam.transformation, cam_transform)
  
          if self.is_zooming:
              tz = dy * CAMERA_TRANSLATION_FACTOR * distance
              cam_transform = transformations.translation_matrix((0, 0, tz)).astype(numpy.float32)
              self.current_cam.transformation = numpy.dot(self.current_cam.transformation, cam_transform)
  
          if zooming_one_shot:
              self.is_zooming = False
  
          self.update_view_camera()
  
      def update_view_camera(self):
  
          self.view_matrix = linalg.inv(self.current_cam.transformation)
  
          # Rotate by 180deg around X to have Z pointing backward (OpenGL convention)
          self.view_matrix = numpy.dot(ROTATION_180_X, self.view_matrix)
  
          glMatrixMode(GL_MODELVIEW)
          glLoadIdentity()
          glMultMatrixf(self.view_matrix.transpose())
  
      def move_selected_node(self, up, strafe):
          self.currently_selected.transformation[0][3] += strafe
          self.currently_selected.transformation[2][3] += up
  
      @staticmethod
      def showtext(text, x=0, y=0, z=0, size=20):
  
          # TODO: alpha blending does not work...
          # glEnable(GL_BLEND)
          # glBlendFunc(GL_SRC_ALPHA, GL_ONE_MINUS_SRC_ALPHA)
  
          font = pygame.font.Font(None, size)
          text_surface = font.render(text, True, (10, 10, 10, 255),
                                    (255 * 0.18, 255 * 0.18, 255 * 0.18, 0))
          text_data = pygame.image.tostring(text_surface, "RGBA", True)
          glRasterPos3d(x, y, z)
          glDrawPixels(text_surface.get_width(),
                       text_surface.get_height(),
                       GL_RGBA, GL_UNSIGNED_BYTE,
                       text_data)
  
          # glDisable(GL_BLEND)
  
  
  def main(model, width, height):
      app = PyAssimp3DViewer(model, w=width, h=height)
  
      clock = pygame.time.Clock()
  
      while app.loop():
  
          app.update_view_camera()
  
          ## Main rendering
          app.render()
  
          ## GUI text display
          app.switch_to_overlay()
          app.showtext("Active camera: %s" % str(app.current_cam), 10, app.h - 30)
          if app.currently_selected:
              app.showtext("Selected node: %s" % app.currently_selected, 10, app.h - 50)
              pos = app.h - 70
  
              app.showtext("(%sm, %sm, %sm)" % (app.currently_selected.transformation[0, 3],
                                                app.currently_selected.transformation[1, 3],
                                                app.currently_selected.transformation[2, 3]), 30, pos)
  
          app.switch_from_overlay()
  
          # Make sure we do not go over 30fps
          clock.tick(30)
  
      logger.info("Quitting! Bye bye!")
  
  
  #########################################################################
  #########################################################################
  
  if __name__ == '__main__':
      if not len(sys.argv) > 1:
          print("Usage: " + __file__ + " <model>")
          sys.exit(2)
  
      main(model=sys.argv[1], width=1024, height=768)
  