You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.

3d_viewer.py 13 KiB

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