advanced_3d_viewer.py 13 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410
  1. #!/usr/bin/env python
  2. #-*- coding: UTF-8 -*-
  3. """ This program loads a model with PyASSIMP, and display it.
  4. It make a large use of shaders to illustrate a 'modern' OpenGL pipeline.
  5. Based on:
  6. - pygame + mouselook code from http://3dengine.org/Spectator_%28PyOpenGL%29
  7. - http://www.lighthouse3d.com/tutorials
  8. - http://www.songho.ca/opengl/gl_transform.html
  9. - http://code.activestate.com/recipes/325391/
  10. - ASSIMP's C++ SimpleOpenGL viewer
  11. """
  12. import sys
  13. import logging
  14. logger = logging.getLogger("underworlds.3d_viewer")
  15. gllogger = logging.getLogger("OpenGL")
  16. gllogger.setLevel(logging.WARNING)
  17. logging.basicConfig(level=logging.INFO)
  18. import OpenGL
  19. #OpenGL.ERROR_CHECKING=False
  20. #OpenGL.ERROR_LOGGING = False
  21. #OpenGL.ERROR_ON_COPY = True
  22. #OpenGL.FULL_LOGGING = True
  23. from OpenGL.GL import *
  24. from OpenGL.error import GLError
  25. from OpenGL.GLU import *
  26. from OpenGL.GLUT import *
  27. from OpenGL.arrays import vbo
  28. from OpenGL.GL import shaders
  29. import pygame
  30. import math, random
  31. import numpy
  32. from numpy import linalg
  33. from pyassimp import core as pyassimp
  34. from pyassimp.postprocess import *
  35. from pyassimp.helper import *
  36. class DefaultCamera:
  37. def __init__(self, w, h, fov):
  38. self.clipplanenear = 0.001
  39. self.clipplanefar = 100000.0
  40. self.aspect = w/h
  41. self.horizontalfov = fov * math.pi/180
  42. self.transformation = [[ 0.68, -0.32, 0.65, 7.48],
  43. [ 0.73, 0.31, -0.61, -6.51],
  44. [-0.01, 0.89, 0.44, 5.34],
  45. [ 0., 0., 0., 1. ]]
  46. self.lookat = [0.0,0.0,-1.0]
  47. def __str__(self):
  48. return "Default camera"
  49. class PyAssimp3DViewer:
  50. base_name = "PyASSIMP 3D viewer"
  51. def __init__(self, model, w=1024, h=768, fov=75):
  52. pygame.init()
  53. pygame.display.set_caption(self.base_name)
  54. pygame.display.set_mode((w,h), pygame.OPENGL | pygame.DOUBLEBUF)
  55. self.prepare_shaders()
  56. self.cameras = [DefaultCamera(w,h,fov)]
  57. self.current_cam_index = 0
  58. self.load_model(model)
  59. # for FPS computation
  60. self.frames = 0
  61. self.last_fps_time = glutGet(GLUT_ELAPSED_TIME)
  62. self.cycle_cameras()
  63. def prepare_shaders(self):
  64. phong_weightCalc = """
  65. float phong_weightCalc(
  66. in vec3 light_pos, // light position
  67. in vec3 frag_normal // geometry normal
  68. ) {
  69. // returns vec2( ambientMult, diffuseMult )
  70. float n_dot_pos = max( 0.0, dot(
  71. frag_normal, light_pos
  72. ));
  73. return n_dot_pos;
  74. }
  75. """
  76. vertex = shaders.compileShader( phong_weightCalc +
  77. """
  78. uniform vec4 Global_ambient;
  79. uniform vec4 Light_ambient;
  80. uniform vec4 Light_diffuse;
  81. uniform vec3 Light_location;
  82. uniform vec4 Material_ambient;
  83. uniform vec4 Material_diffuse;
  84. attribute vec3 Vertex_position;
  85. attribute vec3 Vertex_normal;
  86. varying vec4 baseColor;
  87. void main() {
  88. gl_Position = gl_ModelViewProjectionMatrix * vec4(
  89. Vertex_position, 1.0
  90. );
  91. vec3 EC_Light_location = gl_NormalMatrix * Light_location;
  92. float diffuse_weight = phong_weightCalc(
  93. normalize(EC_Light_location),
  94. normalize(gl_NormalMatrix * Vertex_normal)
  95. );
  96. baseColor = clamp(
  97. (
  98. // global component
  99. (Global_ambient * Material_ambient)
  100. // material's interaction with light's contribution
  101. // to the ambient lighting...
  102. + (Light_ambient * Material_ambient)
  103. // material's interaction with the direct light from
  104. // the light.
  105. + (Light_diffuse * Material_diffuse * diffuse_weight)
  106. ), 0.0, 1.0);
  107. }""", GL_VERTEX_SHADER)
  108. fragment = shaders.compileShader("""
  109. varying vec4 baseColor;
  110. void main() {
  111. gl_FragColor = baseColor;
  112. }
  113. """, GL_FRAGMENT_SHADER)
  114. self.shader = shaders.compileProgram(vertex,fragment)
  115. self.set_shader_accessors( (
  116. 'Global_ambient',
  117. 'Light_ambient','Light_diffuse','Light_location',
  118. 'Material_ambient','Material_diffuse',
  119. ), (
  120. 'Vertex_position','Vertex_normal',
  121. ), self.shader)
  122. def set_shader_accessors(self, uniforms, attributes, shader):
  123. # add accessors to the shaders uniforms and attributes
  124. for uniform in uniforms:
  125. location = glGetUniformLocation( shader, uniform )
  126. if location in (None,-1):
  127. logger.warning('No uniform: %s'%( uniform ))
  128. setattr( shader, uniform, location )
  129. for attribute in attributes:
  130. location = glGetAttribLocation( shader, attribute )
  131. if location in (None,-1):
  132. logger.warning('No attribute: %s'%( attribute ))
  133. setattr( shader, attribute, location )
  134. def prepare_gl_buffers(self, mesh):
  135. mesh.gl = {}
  136. # Fill the buffer for vertex and normals positions
  137. v = numpy.array(mesh.vertices, 'f')
  138. n = numpy.array(mesh.normals, 'f')
  139. mesh.gl["vbo"] = vbo.VBO(numpy.hstack((v,n)))
  140. # Fill the buffer for vertex positions
  141. mesh.gl["faces"] = glGenBuffers(1)
  142. glBindBuffer(GL_ELEMENT_ARRAY_BUFFER, mesh.gl["faces"])
  143. glBufferData(GL_ELEMENT_ARRAY_BUFFER,
  144. mesh.faces,
  145. GL_STATIC_DRAW)
  146. glBindBuffer(GL_ELEMENT_ARRAY_BUFFER,0)
  147. def load_model(self, path, postprocess = aiProcessPreset_TargetRealtime_MaxQuality):
  148. logger.info("Loading model:" + path + "...")
  149. if postprocess:
  150. self.scene = pyassimp.load(path, postprocess)
  151. else:
  152. self.scene = pyassimp.load(path)
  153. logger.info("Done.")
  154. scene = self.scene
  155. #log some statistics
  156. logger.info(" meshes: %d" % len(scene.meshes))
  157. logger.info(" total faces: %d" % sum([len(mesh.faces) for mesh in scene.meshes]))
  158. logger.info(" materials: %d" % len(scene.materials))
  159. self.bb_min, self.bb_max = get_bounding_box(self.scene)
  160. logger.info(" bounding box:" + str(self.bb_min) + " - " + str(self.bb_max))
  161. self.scene_center = [(a + b) / 2. for a, b in zip(self.bb_min, self.bb_max)]
  162. for index, mesh in enumerate(scene.meshes):
  163. self.prepare_gl_buffers(mesh)
  164. # Finally release the model
  165. pyassimp.release(scene)
  166. logger.info("Ready for 3D rendering!")
  167. def cycle_cameras(self):
  168. if not self.cameras:
  169. logger.info("No camera in the scene")
  170. return None
  171. self.current_cam_index = (self.current_cam_index + 1) % len(self.cameras)
  172. self.current_cam = self.cameras[self.current_cam_index]
  173. self.set_camera(self.current_cam)
  174. logger.info("Switched to camera <%s>" % self.current_cam)
  175. def set_camera_projection(self, camera = None):
  176. if not camera:
  177. camera = self.cameras[self.current_cam_index]
  178. znear = camera.clipplanenear
  179. zfar = camera.clipplanefar
  180. aspect = camera.aspect
  181. fov = camera.horizontalfov
  182. glMatrixMode(GL_PROJECTION)
  183. glLoadIdentity()
  184. # Compute gl frustrum
  185. tangent = math.tan(fov/2.)
  186. h = znear * tangent
  187. w = h * aspect
  188. # params: left, right, bottom, top, near, far
  189. glFrustum(-w, w, -h, h, znear, zfar)
  190. # equivalent to:
  191. #gluPerspective(fov * 180/math.pi, aspect, znear, zfar)
  192. glMatrixMode(GL_MODELVIEW)
  193. glLoadIdentity()
  194. def set_camera(self, camera):
  195. self.set_camera_projection(camera)
  196. glMatrixMode(GL_MODELVIEW)
  197. glLoadIdentity()
  198. cam = transform([0.0, 0.0, 0.0], camera.transformation)
  199. at = transform(camera.lookat, camera.transformation)
  200. gluLookAt(cam[0], cam[2], -cam[1],
  201. at[0], at[2], -at[1],
  202. 0, 1, 0)
  203. def render(self, wireframe = False, twosided = False):
  204. glEnable(GL_DEPTH_TEST)
  205. glDepthFunc(GL_LEQUAL)
  206. glPolygonMode(GL_FRONT_AND_BACK, GL_LINE if wireframe else GL_FILL)
  207. glDisable(GL_CULL_FACE) if twosided else glEnable(GL_CULL_FACE)
  208. shader = self.shader
  209. glUseProgram(shader)
  210. glUniform4f( shader.Global_ambient, .4,.2,.2,.1 )
  211. glUniform4f( shader.Light_ambient, .4,.4,.4, 1.0 )
  212. glUniform4f( shader.Light_diffuse, 1,1,1,1 )
  213. glUniform3f( shader.Light_location, 2,2,10 )
  214. self.recursive_render(self.scene.rootnode, shader)
  215. glUseProgram( 0 )
  216. def recursive_render(self, node, shader):
  217. """ Main recursive rendering method.
  218. """
  219. # save model matrix and apply node transformation
  220. glPushMatrix()
  221. m = node.transformation.transpose() # OpenGL row major
  222. glMultMatrixf(m)
  223. for mesh in node.meshes:
  224. stride = 24 # 6 * 4 bytes
  225. glUniform4f( shader.Material_diffuse, *mesh.material.properties["diffuse"] )
  226. glUniform4f( shader.Material_ambient, *mesh.material.properties["ambient"] )
  227. vbo = mesh.gl["vbo"]
  228. vbo.bind()
  229. glEnableVertexAttribArray( shader.Vertex_position )
  230. glEnableVertexAttribArray( shader.Vertex_normal )
  231. glVertexAttribPointer(
  232. shader.Vertex_position,
  233. 3, GL_FLOAT,False, stride, vbo
  234. )
  235. glVertexAttribPointer(
  236. shader.Vertex_normal,
  237. 3, GL_FLOAT,False, stride, vbo+12
  238. )
  239. glBindBuffer(GL_ELEMENT_ARRAY_BUFFER, mesh.gl["faces"])
  240. glDrawElements(GL_TRIANGLES, len(mesh.faces) * 3, GL_UNSIGNED_INT, None)
  241. vbo.unbind()
  242. glDisableVertexAttribArray( shader.Vertex_position )
  243. glDisableVertexAttribArray( shader.Vertex_normal )
  244. glBindBuffer(GL_ELEMENT_ARRAY_BUFFER, 0)
  245. for child in node.children:
  246. self.recursive_render(child, shader)
  247. glPopMatrix()
  248. def loop(self):
  249. pygame.display.flip()
  250. pygame.event.pump()
  251. self.keys = [k for k, pressed in enumerate(pygame.key.get_pressed()) if pressed]
  252. glClear(GL_COLOR_BUFFER_BIT | GL_DEPTH_BUFFER_BIT)
  253. # Compute FPS
  254. gl_time = glutGet(GLUT_ELAPSED_TIME)
  255. self.frames += 1
  256. if gl_time - self.last_fps_time >= 1000:
  257. current_fps = self.frames * 1000 / (gl_time - self.last_fps_time)
  258. pygame.display.set_caption(self.base_name + " - %.0f fps" % current_fps)
  259. self.frames = 0
  260. self.last_fps_time = gl_time
  261. return True
  262. def controls_3d(self,
  263. mouse_button=1, \
  264. up_key=pygame.K_UP, \
  265. down_key=pygame.K_DOWN, \
  266. left_key=pygame.K_LEFT, \
  267. right_key=pygame.K_RIGHT):
  268. """ The actual camera setting cycle """
  269. mouse_dx,mouse_dy = pygame.mouse.get_rel()
  270. if pygame.mouse.get_pressed()[mouse_button]:
  271. look_speed = .2
  272. buffer = glGetDoublev(GL_MODELVIEW_MATRIX)
  273. c = (-1 * numpy.mat(buffer[:3,:3]) * \
  274. numpy.mat(buffer[3,:3]).T).reshape(3,1)
  275. # c is camera center in absolute coordinates,
  276. # we need to move it back to (0,0,0)
  277. # before rotating the camera
  278. glTranslate(c[0],c[1],c[2])
  279. m = buffer.flatten()
  280. glRotate(mouse_dx * look_speed, m[1],m[5],m[9])
  281. glRotate(mouse_dy * look_speed, m[0],m[4],m[8])
  282. # compensate roll
  283. glRotated(-math.atan2(-m[4],m[5]) * \
  284. 57.295779513082320876798154814105 ,m[2],m[6],m[10])
  285. glTranslate(-c[0],-c[1],-c[2])
  286. # move forward-back or right-left
  287. if up_key in self.keys:
  288. fwd = .1
  289. elif down_key in self.keys:
  290. fwd = -.1
  291. else:
  292. fwd = 0
  293. if left_key in self.keys:
  294. strafe = .1
  295. elif right_key in self.keys:
  296. strafe = -.1
  297. else:
  298. strafe = 0
  299. if abs(fwd) or abs(strafe):
  300. m = glGetDoublev(GL_MODELVIEW_MATRIX).flatten()
  301. glTranslate(fwd*m[2],fwd*m[6],fwd*m[10])
  302. glTranslate(strafe*m[0],strafe*m[4],strafe*m[8])
  303. if __name__ == '__main__':
  304. if not len(sys.argv) > 1:
  305. print("Usage: " + __file__ + " <model>")
  306. sys.exit(2)
  307. app = PyAssimp3DViewer(model = sys.argv[1], w = 1024, h = 768, fov = 75)
  308. while app.loop():
  309. app.render()
  310. app.controls_3d(0)
  311. if pygame.K_f in app.keys: pygame.display.toggle_fullscreen()
  312. if pygame.K_s in app.keys: app.screenshot()
  313. if pygame.K_v in app.keys: app.check_visibility()
  314. if pygame.K_TAB in app.keys: app.cycle_cameras()
  315. if pygame.K_ESCAPE in app.keys:
  316. break