#!/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('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__ + " ") sys.exit(2) main(model=sys.argv[1], width=1024, height=768)