class OpenGLRenderer(ABC): def __init__(self, src_fbuffer, src_default): self.default_prog = None self.fbuffer_prog = None self.fbuffer = None self.fbuffer_tex_front = None self.fbuffer_tex_back = None self.vertex_buffer = None self.index_buffer = None # Renderer Globals: STYLE/MATERIAL PROPERTIES # self.style = Style() # Renderer Globals: Curves self.stroke_weight = 1 self.stroke_cap = ROUND self.stroke_join = MITER # Renderer Globals # VIEW MATRICES, ETC # self.viewport = None self.texture_viewport = None self.transform_matrix = np.identity(4) self.projection_matrix = np.identity(4) # Renderer Globals: RENDERING self.draw_queue = [] # Shaders self.fbuffer_prog = Program(src_fbuffer.vert, src_fbuffer.frag) self.default_prog = Program(src_default.vert, src_default.frag) def initialize_renderer(self): self.fbuffer = FrameBuffer() vertices = np.array( [[-1.0, -1.0], [+1.0, -1.0], [-1.0, +1.0], [+1.0, +1.0]], np.float32) texcoords = np.array([[0.0, 0.0], [1.0, 0.0], [0.0, 1.0], [1.0, 1.0]], dtype=np.float32) self.fbuf_vertices = VertexBuffer(data=vertices) self.fbuf_texcoords = VertexBuffer(data=texcoords) self.fbuffer_prog['texcoord'] = self.fbuf_texcoords self.fbuffer_prog['position'] = self.fbuf_vertices self.vertex_buffer = VertexBuffer() self.index_buffer = IndexBuffer() def render_default(self, draw_type, draw_queue): # 1. Get the maximum number of vertices persent in the shapes # in the draw queue. # if len(draw_queue) == 0: return num_vertices = 0 for vertices, _, _ in draw_queue: num_vertices = num_vertices + len(vertices) # 2. Create empty buffers based on the number of vertices. # data = np.zeros(num_vertices, dtype=[('position', np.float32, 3), ('color', np.float32, 4)]) # 3. Loop through all the shapes in the geometry queue adding # it's information to the buffer. # sidx = 0 draw_indices = [] for vertices, idx, color in draw_queue: num_shape_verts = len(vertices) data['position'][sidx:(sidx + num_shape_verts), ] = np.array(vertices) color_array = np.array([color] * num_shape_verts) data['color'][sidx:sidx + num_shape_verts, :] = color_array draw_indices.append(sidx + idx) sidx += num_shape_verts self.vertex_buffer.set_data(data) self.index_buffer.set_data(np.hstack(draw_indices)) # 4. Bind the buffer to the shader. # self.default_prog.bind(self.vertex_buffer) # 5. Draw the shape using the proper shape type and get rid of # the buffers. # self.default_prog.draw(draw_type, indices=self.index_buffer) def cleanup(self): """Run the clean-up routine for the renderer. This method is called when all drawing has been completed and the program is about to exit. """ self.default_prog.delete() self.fbuffer_prog.delete() self.fbuffer.delete() def _transform_vertices(self, vertices, local_matrix, global_matrix): return np.dot(np.dot(vertices, local_matrix.T), global_matrix.T)[:, :3]
class Renderer2D: def __init__(self): self.default_prog = None self.fbuffer_prog = None self.texture_prog = None self.line_prog = None self.fbuffer = None self.fbuffer_tex_front = None self.fbuffer_tex_back = None self.vertex_buffer = None self.index_buffer = None ## Renderer Globals: USEFUL CONSTANTS self.COLOR_WHITE = (1, 1, 1, 1) self.COLOR_BLACK = (0, 0, 0, 1) self.COLOR_DEFAULT_BG = (0.8, 0.8, 0.8, 1.0) ## Renderer Globals: STYLE/MATERIAL PROPERTIES ## self.background_color = self.COLOR_DEFAULT_BG self.fill_color = self.COLOR_WHITE self.fill_enabled = True self.stroke_color = self.COLOR_BLACK self.stroke_enabled = True self.tint_color = self.COLOR_BLACK self.tint_enabled = False ## Renderer Globals: Curves self.stroke_weight = 1 self.stroke_cap = 2 self.stroke_join = 0 ## Renderer Globals ## VIEW MATRICES, ETC ## self.viewport = None self.texture_viewport = None self.transform_matrix = np.identity(4) self.modelview_matrix = np.identity(4) self.projection_matrix = np.identity(4) ## Renderer Globals: RENDERING self.draw_queue = [] def initialize_renderer(self): self.fbuffer = FrameBuffer() vertices = np.array( [[-1.0, -1.0], [+1.0, -1.0], [-1.0, +1.0], [+1.0, +1.0]], np.float32) texcoords = np.array([[0.0, 0.0], [1.0, 0.0], [0.0, 1.0], [1.0, 1.0]], dtype=np.float32) self.fbuf_vertices = VertexBuffer(data=vertices) self.fbuf_texcoords = VertexBuffer(data=texcoords) self.fbuffer_prog = Program(src_fbuffer.vert, src_fbuffer.frag) self.fbuffer_prog['texcoord'] = self.fbuf_texcoords self.fbuffer_prog['position'] = self.fbuf_vertices self.vertex_buffer = VertexBuffer() self.index_buffer = IndexBuffer() self.default_prog = Program(src_default.vert, src_default.frag) self.texture_prog = Program(src_texture.vert, src_texture.frag) self.texture_prog['texcoord'] = self.fbuf_texcoords self.reset_view() def reset_view(self): self.viewport = ( 0, 0, int(builtins.width * builtins.pixel_x_density), int(builtins.height * builtins.pixel_y_density), ) self.texture_viewport = ( 0, 0, builtins.width, builtins.height, ) gloo.set_viewport(*self.viewport) cz = (builtins.height / 2) / math.tan(math.radians(30)) self.projection_matrix = matrix.perspective_matrix( math.radians(60), builtins.width / builtins.height, 0.1 * cz, 10 * cz) self.modelview_matrix = matrix.translation_matrix(-builtins.width / 2, \ builtins.height / 2, \ -cz) self.modelview_matrix = self.modelview_matrix.dot( matrix.scale_transform(1, -1, 1)) self.transform_matrix = np.identity(4) self.default_prog['modelview'] = self.modelview_matrix.T.flatten() self.default_prog['projection'] = self.projection_matrix.T.flatten() self.texture_prog['modelview'] = self.modelview_matrix.T.flatten() self.texture_prog['projection'] = self.projection_matrix.T.flatten() self.line_prog = Program(src_line.vert, src_line.frag) self.line_prog['modelview'] = self.modelview_matrix.T.flatten() self.line_prog['projection'] = self.projection_matrix.T.flatten() self.line_prog["height"] = builtins.height self.fbuffer_tex_front = Texture2D( (builtins.height, builtins.width, 3)) self.fbuffer_tex_back = Texture2D((builtins.height, builtins.width, 3)) for buf in [self.fbuffer_tex_front, self.fbuffer_tex_back]: self.fbuffer.color_buffer = buf with self.fbuffer: self.clear() def clear(self, color=True, depth=True): """Clear the renderer background.""" gloo.set_state(clear_color=self.background_color) gloo.clear(color=color, depth=depth) def _comm_toggles(self, state=True): gloo.set_state(blend=state) gloo.set_state(depth_test=state) if state: gloo.set_state(blend_func=('src_alpha', 'one_minus_src_alpha')) gloo.set_state(depth_func='lequal') @contextmanager def draw_loop(self): """The main draw loop context manager. """ self.transform_matrix = np.identity(4) self.default_prog['modelview'] = self.modelview_matrix.T.flatten() self.default_prog['projection'] = self.projection_matrix.T.flatten() self.fbuffer.color_buffer = self.fbuffer_tex_back with self.fbuffer: gloo.set_viewport(*self.texture_viewport) self._comm_toggles() self.fbuffer_prog['texture'] = self.fbuffer_tex_front self.fbuffer_prog.draw('triangle_strip') yield self.flush_geometry() self.transform_matrix = np.identity(4) gloo.set_viewport(*self.viewport) self._comm_toggles(False) self.clear() self.fbuffer_prog['texture'] = self.fbuffer_tex_back self.fbuffer_prog.draw('triangle_strip') self.fbuffer_tex_front, self.fbuffer_tex_back = self.fbuffer_tex_back, self.fbuffer_tex_front def _transform_vertices(self, vertices, local_matrix, global_matrix): return np.dot(np.dot(vertices, local_matrix.T), global_matrix.T)[:, :3] def _add_to_draw_queue_simple(self, stype, vertices, idx, fill, stroke, stroke_weight, stroke_cap, stroke_join): """Adds shape of stype to draw queue """ if stype == 'lines': self.draw_queue.append( (stype, (vertices, idx, stroke, stroke_weight, stroke_cap, stroke_join))) else: self.draw_queue.append((stype, (vertices, idx, fill))) def render(self, shape): fill = shape.fill.normalized if shape.fill else None stroke = shape.stroke.normalized if shape.stroke else None stroke_weight = shape.stroke_weight stroke_cap = shape.stroke_cap stroke_join = shape.stroke_join obj_list = get_render_primitives(shape) for obj in obj_list: stype, vertices, idx = obj # Transform vertices vertices = self._transform_vertices( np.hstack([vertices, np.ones((len(vertices), 1))]), shape._matrix, self.transform_matrix) # Add to draw queue self._add_to_draw_queue_simple(stype, vertices, idx, fill, stroke, stroke_weight, stroke_cap, stroke_join) def flush_geometry(self): """Flush all the shape geometry from the draw queue to the GPU. """ current_queue = [] for index, shape in enumerate(self.draw_queue): current_shape = self.draw_queue[index][0] current_queue.append(self.draw_queue[index][1]) if current_shape == "lines": self.render_line(current_queue) else: self.render_default(current_shape, current_queue) current_queue = [] self.draw_queue = [] def render_default(self, draw_type, draw_queue): # 1. Get the maximum number of vertices persent in the shapes # in the draw queue. # if len(draw_queue) == 0: return num_vertices = 0 for vertices, _, _ in draw_queue: num_vertices = num_vertices + len(vertices) # 2. Create empty buffers based on the number of vertices. # data = np.zeros(num_vertices, dtype=[('position', np.float32, 3), ('color', np.float32, 4)]) # 3. Loop through all the shapes in the geometry queue adding # it's information to the buffer. # sidx = 0 draw_indices = [] for vertices, idx, color in draw_queue: num_shape_verts = len(vertices) data['position'][sidx:(sidx + num_shape_verts), ] = vertices color_array = np.array([color] * num_shape_verts) data['color'][sidx:sidx + num_shape_verts, :] = color_array draw_indices.append(sidx + idx) sidx += num_shape_verts self.vertex_buffer.set_data(data) self.index_buffer.set_data(np.hstack(draw_indices)) # 4. Bind the buffer to the shader. # self.default_prog.bind(self.vertex_buffer) # 5. Draw the shape using the proper shape type and get rid of # the buffers. # self.default_prog.draw(draw_type, indices=self.index_buffer) def render_line(self, queue): ''' This rendering algorithm works by tesselating the line into multiple triangles. Reference: https://blog.mapbox.com/drawing-antialiased-lines-with-opengl-8766f34192dc ''' if len(queue) == 0: return pos = [] posPrev = [] posCurr = [] posNext = [] markers = [] side = [] linewidth = [] join_type = [] cap_type = [] color = [] for line in queue: if len(line[1]) == 0: continue for segment in line[1]: for i in range( len(segment) - 1): # the data is sent to renderer in line segments for j in [0, 0, 1, 0, 1, 1]: # all the vertices of triangles if i + j - 1 >= 0: posPrev.append(line[0][segment[i + j - 1]]) else: posPrev.append(line[0][segment[i + j]]) if i + j + 1 < len(segment): posNext.append(line[0][segment[i + j + 1]]) else: posNext.append(line[0][segment[i + j]]) posCurr.append(line[0][segment[i + j]]) markers.extend( [1.0, -1.0, -1.0, -1.0, 1.0, -1.0]) # Is the vertex up/below the line segment side.extend([1.0, 1.0, -1.0, 1.0, -1.0, -1.0]) # Left or right side of the segment pos.extend([line[0][segment[i]]] * 6) # Left vertex of each segment linewidth.extend([line[3]] * 6) join_type.extend([line[5]] * 6) cap_type.extend([line[4]] * 6) color.extend([line[2]] * 6) if len(pos) == 0: return posPrev = np.array(posPrev, np.float32) posCurr = np.array(posCurr, np.float32) posNext = np.array(posNext, np.float32) markers = np.array(markers, np.float32) side = np.array(side, np.float32) pos = np.array(pos, np.float32) linewidth = np.array(linewidth, np.float32) join_type = np.array(join_type, np.float32) cap_type = np.array(cap_type, np.float32) color = np.array(color, np.float32) self.line_prog['pos'] = gloo.VertexBuffer(pos) self.line_prog['posPrev'] = gloo.VertexBuffer(posPrev) self.line_prog['posCurr'] = gloo.VertexBuffer(posCurr) self.line_prog['posNext'] = gloo.VertexBuffer(posNext) self.line_prog['marker'] = gloo.VertexBuffer(markers) self.line_prog['side'] = gloo.VertexBuffer(side) self.line_prog['linewidth'] = gloo.VertexBuffer(linewidth) self.line_prog['join_type'] = gloo.VertexBuffer(join_type) self.line_prog['cap_type'] = gloo.VertexBuffer(cap_type) self.line_prog["color"] = gloo.VertexBuffer(color) self.line_prog.draw('triangles') def render_image(self, image, location, size): """Render the image. :param image: image to be rendered :type image: builtins.Image :param location: top-left corner of the image :type location: tuple | list | builtins.Vector :param size: target size of the image to draw. :type size: tuple | list | builtins.Vector """ self.flush_geometry() self.texture_prog[ 'fill_color'] = self.tint_color if self.tint_enabled else self.COLOR_WHITE self.texture_prog['transform'] = self.transform_matrix.T.flatten() x, y = location sx, sy = size imx, imy = image.size data = np.zeros(4, dtype=[('position', np.float32, 2), ('texcoord', np.float32, 2)]) data['texcoord'] = np.array( [[0.0, 1.0], [1.0, 1.0], [0.0, 0.0], [1.0, 0.0]], dtype=np.float32) data['position'] = np.array( [[x, y + sy], [x + sx, y + sy], [x, y], [x + sx, y]], dtype=np.float32) self.texture_prog['texture'] = image._texture self.texture_prog.bind(VertexBuffer(data)) self.texture_prog.draw('triangle_strip') def cleanup(self): """Run the clean-up routine for the renderer. This method is called when all drawing has been completed and the program is about to exit. """ self.default_prog.delete() self.fbuffer_prog.delete() self.line_prog.delete() self.fbuffer.delete()
class VispyRenderer2D(OpenGLRenderer): def __init__(self): super().__init__(src_fbuffer, src_default) self.texture_prog = None self.line_prog = None self.modelview_matrix = np.identity(4) def initialize_renderer(self): super().initialize_renderer() self.texture_prog = Program(src_texture.vert, src_texture.frag) self.texture_prog['texcoord'] = self.fbuf_texcoords self.reset_view() def reset_view(self): self.viewport = ( 0, 0, int(builtins.width * builtins.pixel_x_density), int(builtins.height * builtins.pixel_y_density), ) self.texture_viewport = ( 0, 0, builtins.width, builtins.height, ) gloo.set_viewport(*self.viewport) # pylint: disable=no-member cz = (builtins.height / 2) / math.tan(math.radians(30)) self.projection_matrix = matrix.perspective_matrix( math.radians(60), builtins.width / builtins.height, 0.1 * cz, 10 * cz) self.modelview_matrix = matrix.translation_matrix( -builtins.width / 2, builtins.height / 2, -cz) self.modelview_matrix = self.modelview_matrix.dot( matrix.scale_transform(1, -1, 1)) self.transform_matrix = np.identity(4) self.default_prog['modelview'] = self.modelview_matrix.T.flatten() self.default_prog['projection'] = self.projection_matrix.T.flatten() self.texture_prog['modelview'] = self.modelview_matrix.T.flatten() self.texture_prog['projection'] = self.projection_matrix.T.flatten() self.line_prog = Program(src_line.vert, src_line.frag) self.line_prog['modelview'] = self.modelview_matrix.T.flatten() self.line_prog['projection'] = self.projection_matrix.T.flatten() self.line_prog["height"] = builtins.height self.fbuffer_tex_front = Texture2D( (builtins.height, builtins.width, 3)) self.fbuffer_tex_back = Texture2D((builtins.height, builtins.width, 3)) for buf in [self.fbuffer_tex_front, self.fbuffer_tex_back]: self.fbuffer.color_buffer = buf with self.fbuffer: self.clear() def clear(self, color=True, depth=True): """Clear the renderer background.""" gloo.set_state(clear_color=self.style.background_color) # pylint: disable=no-member gloo.clear(color=color, depth=depth) # pylint: disable=no-member def _comm_toggles(self, state=True): gloo.set_state(blend=state) # pylint: disable=no-member gloo.set_state(depth_test=state) # pylint: disable=no-member if state: gloo.set_state(blend_func=('src_alpha', 'one_minus_src_alpha')) # pylint: disable=no-member gloo.set_state(depth_func='lequal') # pylint: disable=no-member @contextmanager def draw_loop(self): """The main draw loop context manager. """ self.transform_matrix = np.identity(4) self.default_prog['modelview'] = self.modelview_matrix.T.flatten() self.default_prog['projection'] = self.projection_matrix.T.flatten() self.fbuffer.color_buffer = self.fbuffer_tex_back with self.fbuffer: gloo.set_viewport(*self.texture_viewport) # pylint: disable=no-member self._comm_toggles() self.fbuffer_prog['texture'] = self.fbuffer_tex_front self.fbuffer_prog.draw('triangle_strip') yield self.flush_geometry() self.transform_matrix = np.identity(4) gloo.set_viewport(*self.viewport) # pylint: disable=no-member self._comm_toggles(False) self.clear() self.fbuffer_prog['texture'] = self.fbuffer_tex_back self.fbuffer_prog.draw('triangle_strip') self.fbuffer_tex_front, self.fbuffer_tex_back = self.fbuffer_tex_back, self.fbuffer_tex_front def _add_to_draw_queue(self, stype, vertices, idx, fill, stroke, stroke_weight, stroke_cap, stroke_join): """Adds shape of stype to draw queue """ if stype == 'lines': self.draw_queue.append( (stype, (vertices, idx, stroke, stroke_weight, stroke_cap, stroke_join))) else: self.draw_queue.append((stype, (vertices, idx, fill))) def render(self, shape): fill = shape.fill.normalized if shape.fill else None stroke = shape.stroke.normalized if shape.stroke else None stroke_weight = shape.stroke_weight stroke_cap = shape.stroke_cap stroke_join = shape.stroke_join obj_list = get_render_primitives(shape) for obj in obj_list: stype, vertices, idx = obj # Convert 2D vertices to 3D by adding "0" column, needed for further transformations if len(vertices[0]) == 2: vertices = np.hstack([vertices, np.zeros((len(vertices), 1))]) # Transform vertices vertices = self._transform_vertices( np.hstack([vertices, np.ones((len(vertices), 1))]), shape._matrix, self.transform_matrix) # Add to draw queue self._add_to_draw_queue(stype, vertices, idx, fill, stroke, stroke_weight, stroke_cap, stroke_join) def flush_geometry(self): """Flush all the shape geometry from the draw queue to the GPU. """ current_queue = [] for index, shape in enumerate(self.draw_queue): current_shape = self.draw_queue[index][0] current_queue.append(self.draw_queue[index][1]) if current_shape == "lines": self.render_line(current_queue) else: self.render_default(current_shape, current_queue) current_queue = [] self.draw_queue = [] def render_line(self, queue): ''' This rendering algorithm works by tesselating the line into multiple triangles. Reference: https://blog.mapbox.com/drawing-antialiased-lines-with-opengl-8766f34192dc ''' if len(queue) == 0: return pos = [] posPrev = [] posCurr = [] posNext = [] markers = [] side = [] linewidth = [] join_type = [] cap_type = [] color = [] stroke_cap_codes = {'PROJECT': 0, 'SQUARE': 1, 'ROUND': 2} stroke_join_codes = {'MITER': 0, 'BEVEL': 1, 'ROUND': 2} for line in queue: if len(line[1]) == 0: continue for segment in line[1]: for i in range( len(segment) - 1): # the data is sent to renderer in line segments for j in [0, 0, 1, 0, 1, 1]: # all the vertices of triangles if i + j - 1 >= 0: posPrev.append(line[0][segment[i + j - 1]]) else: posPrev.append(line[0][segment[i + j]]) if i + j + 1 < len(segment): posNext.append(line[0][segment[i + j + 1]]) else: posNext.append(line[0][segment[i + j]]) posCurr.append(line[0][segment[i + j]]) # Is the vertex up/below the line segment markers.extend([1.0, -1.0, -1.0, -1.0, 1.0, -1.0]) # Left or right side of the segment side.extend([1.0, 1.0, -1.0, 1.0, -1.0, -1.0]) # Left vertex of each segment pos.extend([line[0][segment[i]]] * 6) linewidth.extend([line[3]] * 6) join_type.extend([stroke_join_codes[line[5]]] * 6) cap_type.extend([stroke_cap_codes[line[4]]] * 6) color.extend([line[2]] * 6) if len(pos) == 0: return posPrev = np.array(posPrev, np.float32) posCurr = np.array(posCurr, np.float32) posNext = np.array(posNext, np.float32) markers = np.array(markers, np.float32) side = np.array(side, np.float32) pos = np.array(pos, np.float32) linewidth = np.array(linewidth, np.float32) join_type = np.array(join_type, np.float32) cap_type = np.array(cap_type, np.float32) color = np.array(color, np.float32) self.line_prog['pos'] = gloo.VertexBuffer(pos) self.line_prog['posPrev'] = gloo.VertexBuffer(posPrev) self.line_prog['posCurr'] = gloo.VertexBuffer(posCurr) self.line_prog['posNext'] = gloo.VertexBuffer(posNext) self.line_prog['marker'] = gloo.VertexBuffer(markers) self.line_prog['side'] = gloo.VertexBuffer(side) self.line_prog['linewidth'] = gloo.VertexBuffer(linewidth) self.line_prog['join_type'] = gloo.VertexBuffer(join_type) self.line_prog['cap_type'] = gloo.VertexBuffer(cap_type) self.line_prog["color"] = gloo.VertexBuffer(color) self.line_prog.draw('triangles') def render_image(self, image, location, size): """Render the image. :param image: image to be rendered :type image: builtins.Image :param location: top-left corner of the image :type location: tuple | list | builtins.Vector :param size: target size of the image to draw. :type size: tuple | list | builtins.Vector """ self.flush_geometry() self.texture_prog[ 'fill_color'] = self.style.tint_color if self.style.tint_enabled else COLOR_WHITE self.texture_prog['transform'] = self.transform_matrix.T.flatten() x, y = location sx, sy = size imx, imy = image.size data = np.zeros(4, dtype=[('position', np.float32, 2), ('texcoord', np.float32, 2)]) data['texcoord'] = np.array( [[0.0, 1.0], [1.0, 1.0], [0.0, 0.0], [1.0, 0.0]], dtype=np.float32) data['position'] = np.array( [[x, y + sy], [x + sx, y + sy], [x, y], [x + sx, y]], dtype=np.float32) self.texture_prog['texture'] = image._texture self.texture_prog.bind(VertexBuffer(data)) self.texture_prog.draw('triangle_strip') def cleanup(self): """Run the clean-up routine for the renderer. This method is called when all drawing has been completed and the program is about to exit. """ OpenGLRenderer.cleanup(self) self.line_prog.delete() def render_shape(self, shape): self.render(shape) for child_shape in shape.children: self.render_shape(child_shape) def line(self, *args): path = args[0] self.render_shape(PShape(vertices=path, shape_type=SType.LINES)) def bezier(self, *args): vertices = args[0] self.render_shape( PShape(vertices=vertices, shape_type=SType.LINE_STRIP)) def curve(self, *args): vertices = args[0] self.render_shape( PShape(vertices=vertices, shape_type=SType.LINE_STRIP)) def triangle(self, *args): path = args[0] self.render_shape(PShape(vertices=path, shape_type=SType.TRIANGLES)) def quad(self, *args): path = args[0] self.render_shape(PShape(vertices=path, shape_type=SType.QUADS)) def arc(self, *args): center = args[0] dim = args[1] start_angle = args[2] stop_angle = args[3] mode = args[4] self.render_shape(Arc(center, dim, start_angle, stop_angle, mode)) def shape(self, vertices, contours, shape_type, *args): """Render a Pshape""" self.render_shape( PShape(vertices=vertices, contours=contours, shape_type=shape_type))
class Renderer3D(OpenGLRenderer): def __init__(self): super().__init__(src_fbuffer, src_default) self.style = Style3D() self.normal_prog = Program(src_normal.vert, src_normal.frag) self.phong_prog = Program(src_phong.vert, src_phong.frag) self.lookat_matrix = np.identity(4) # Camera position self.camera_pos = np.zeros(3) # Lights self.MAX_LIGHTS_PER_CATEGORY = 8 self.ambient_light_color = GlslList(self.MAX_LIGHTS_PER_CATEGORY, 3, np.float32) self.directional_light_dir = GlslList(self.MAX_LIGHTS_PER_CATEGORY, 3, np.float32) self.directional_light_color = GlslList(self.MAX_LIGHTS_PER_CATEGORY, 3, np.float32) self.directional_light_specular = GlslList( self.MAX_LIGHTS_PER_CATEGORY, 3, np.float32) self.point_light_color = GlslList(self.MAX_LIGHTS_PER_CATEGORY, 3, np.float32) self.point_light_pos = GlslList(self.MAX_LIGHTS_PER_CATEGORY, 3, np.float32) self.point_light_specular = GlslList(self.MAX_LIGHTS_PER_CATEGORY, 3, np.float32) self.const_falloff = GlslList(self.MAX_LIGHTS_PER_CATEGORY, 1, np.float32) self.linear_falloff = GlslList(self.MAX_LIGHTS_PER_CATEGORY, 1, np.float32) self.quadratic_falloff = GlslList(self.MAX_LIGHTS_PER_CATEGORY, 1, np.float32) self.curr_linear_falloff, self.curr_quadratic_falloff, self.curr_constant_falloff = 0.0, 0.0, 0.0 self.light_specular = np.array([0.0] * 3) def initialize_renderer(self): super().initialize_renderer() self.reset_view() def reset_view(self): self.viewport = ( 0, 0, int(builtins.width * builtins.pixel_x_density), int(builtins.height * builtins.pixel_y_density), ) self.texture_viewport = ( 0, 0, builtins.width, builtins.height, ) gloo.set_viewport(*self.viewport) # pylint: disable=no-member cz = (builtins.height / 2) / math.tan(math.radians(30)) self.projection_matrix = matrix.perspective_matrix( math.radians(60), builtins.width / builtins.height, 0.1 * cz, 10 * cz) self.transform_matrix = np.identity(4) self._update_shader_transforms() self.fbuffer_tex_front = Texture2D( (builtins.height, builtins.width, 3)) self.fbuffer_tex_back = Texture2D((builtins.height, builtins.width, 3)) self.fbuffer.depth_buffer = gloo.RenderBuffer( (builtins.height, builtins.width)) for buf in [self.fbuffer_tex_front, self.fbuffer_tex_back]: self.fbuffer.color_buffer = buf with self.fbuffer: self.clear() def clear(self, color=True, depth=True): """Clear the renderer background.""" gloo.set_state(clear_color=self.style.background_color) # pylint: disable=no-member gloo.clear(color=color, depth=depth) # pylint: disable=no-member def clear_lights(self): self.ambient_light_color.clear() self.directional_light_color.clear() self.directional_light_dir.clear() self.directional_light_specular.clear() self.point_light_color.clear() self.point_light_pos.clear() self.point_light_specular.clear() self.const_falloff.clear() self.linear_falloff.clear() self.quadratic_falloff.clear() def _comm_toggles(self, state=True): gloo.set_state(blend=state) # pylint: disable=no-member gloo.set_state(depth_test=state) # pylint: disable=no-member if state: gloo.set_state(blend_func=('src_alpha', 'one_minus_src_alpha')) # pylint: disable=no-member gloo.set_state(depth_func='lequal') # pylint: disable=no-member def _update_shader_transforms(self): # Default shader self.default_prog['projection'] = self.projection_matrix.T.flatten() self.default_prog['perspective_matrix'] = self.lookat_matrix.T.flatten( ) # Normal shader self.normal_prog['projection'] = self.projection_matrix.T.flatten() self.normal_prog['perspective'] = self.lookat_matrix.T.flatten() # This is a no-op, meaning that the normals stay in world space, which # matches the behavior in p5.js normal_transform = np.identity(3) # I think the transformation below takes the vertices to camera space, but # the results are funky, so it's probably incorrect? - ziyaointl, 2020/07/20 # normal_transform = np.linalg.inv(self.projection_matrix[:3, :3] @ self.lookat_matrix[:3, :3]) self.normal_prog['normal_transform'] = normal_transform.flatten() # Blinn-Phong Shader self.phong_prog['projection'] = self.projection_matrix.T.flatten() self.phong_prog['perspective'] = self.lookat_matrix.T.flatten() @contextmanager def draw_loop(self): """The main draw loop context manager. """ self.transform_matrix = np.identity(4) self._update_shader_transforms() self.fbuffer.color_buffer = self.fbuffer_tex_back with self.fbuffer: gloo.set_viewport(*self.texture_viewport) # pylint: disable=no-member self._comm_toggles() self.fbuffer_prog['texture'] = self.fbuffer_tex_front self.fbuffer_prog.draw('triangle_strip') self.clear(color=False, depth=True) self.clear_lights() yield self.flush_geometry() self.transform_matrix = np.identity(4) gloo.set_viewport(*self.viewport) # pylint: disable=no-member self._comm_toggles(False) self.clear() self.fbuffer_prog['texture'] = self.fbuffer_tex_back self.fbuffer_prog.draw('triangle_strip') self.fbuffer_tex_front, self.fbuffer_tex_back = self.fbuffer_tex_back, self.fbuffer_tex_front def _add_to_draw_queue_simple(self, stype, vertices, idx, color): """Adds shape of stype to draw queue """ self.draw_queue.append((stype, (vertices, idx, color, None, None))) def tnormals(self, shape): """Obtain a list of vertex normals in world coordinates """ if isinstance(shape.material, BasicMaterial): # Basic shader doesn't need this return None return shape.vertex_normals @ np.linalg.inv( to_3x3(self.transform_matrix) @ to_3x3(shape.matrix)) def render(self, shape): if isinstance(shape, Geometry): n = len(shape.vertices) # Perform model transform # TODO: Investigate moving model transform from CPU to the GPU tverts = self._transform_vertices( np.hstack([shape.vertices, np.ones((n, 1))]), shape.matrix, self.transform_matrix) tnormals = self.tnormals(shape) edges = shape.edges faces = shape.faces self.add_to_draw_queue('poly', tverts, edges, faces, self.style.fill_color, self.style.stroke_color, tnormals, self.style.material) elif isinstance(shape, PShape): fill = shape.fill.normalized if shape.fill else None stroke = shape.stroke.normalized if shape.stroke else None obj_list = get_render_primitives(shape) for obj in obj_list: stype, vertices, idx = obj # Transform vertices vertices = self._transform_vertices( np.hstack([vertices, np.ones((len(vertices), 1))]), shape._matrix, self.transform_matrix) # Add to draw queue self._add_to_draw_queue_simple( stype, vertices, idx, stroke if stype == 'lines' else fill) def add_to_draw_queue(self, stype, vertices, edges, faces, fill=None, stroke=None, normals=None, material=None): """Add the given vertex data to the draw queue. :param stype: type of shape to be added. Should be one of {'poly', 'path', 'point'} :type stype: str :param vertices: (N, 3) array containing the vertices to be drawn. :type vertices: np.ndarray :param edges: (N, 2) array containing edges as tuples of indices into the vertex array. This can be None when not appropriate (eg. for points) :type edges: None | np.ndarray :param faces: (N, 3) array containing faces as tuples of indices into the vertex array. For 'point' and 'path' shapes, this can be None :type faces: np.ndarray :param fill: Fill color of the shape as a normalized RGBA tuple. When set to `None` the shape doesn't get a fill (default: None) :type fill: None | tuple :param stroke: Stroke color of the shape as a normalized RGBA tuple. When set to `None` the shape doesn't get stroke (default: None) :type stroke: None | tuple // TODO: Update documentation // TODO: Unite style-related attributes for both 2D and 3D under one material class """ fill_shape = self.style.fill_enabled and not (fill is None) stroke_shape = self.style.stroke_enabled and not (stroke is None) if fill_shape and stype not in ['point', 'path']: idx = np.array(faces, dtype=np.uint32).ravel() self.draw_queue.append( ["triangles", (vertices, idx, fill, normals, material)]) if stroke_shape: if stype == 'point': idx = np.arange(0, len(vertices), dtype=np.uint32) self.draw_queue.append( ["points", (vertices, idx, stroke, normals, material)]) else: idx = np.array(edges, dtype=np.uint32).ravel() self.draw_queue.append( ["lines", (vertices, idx, stroke, normals, material)]) def render_with_shaders(self, draw_type, draw_obj): vertices, idx, color, normals, material = draw_obj """Like render_default but is aware of shaders other than the basic one""" # 0. If material does not need normals nor extra info, strip them out # and use the method from superclass if material is None or isinstance( material, BasicMaterial) or draw_type in ['points', 'lines']: OpenGLRenderer.render_default(self, draw_type, [draw_obj[:3]]) return # 1. Get the number of vertices num_vertices = len(vertices) # 2. Create empty buffers based on the number of vertices. # data = np.zeros(num_vertices, dtype=[('position', np.float32, 3), ('normal', np.float32, 3)]) # 3. Loop through all the shapes in the geometry queue adding # it's information to the buffer. # draw_indices = [] data['position'][0:num_vertices, ] = np.array(vertices) draw_indices.append(idx) data['normal'][0:num_vertices, ] = np.array(normals) self.vertex_buffer.set_data(data) self.index_buffer.set_data(np.hstack(draw_indices)) if isinstance(material, NormalMaterial): # 4. Bind the buffer to the shader. # self.normal_prog.bind(self.vertex_buffer) # 5. Draw the shape using the proper shape type and get rid of # the buffers. # self.normal_prog.draw(draw_type, indices=self.index_buffer) elif isinstance(material, BlinnPhongMaterial): self.phong_prog.bind(self.vertex_buffer) self.phong_prog['u_cam_pos'] = self.camera_pos # Material attributes self.phong_prog['u_ambient_color'] = material.ambient self.phong_prog['u_diffuse_color'] = material.diffuse self.phong_prog['u_specular_color'] = material.specular self.phong_prog['u_shininess'] = material.shininess # Directional lights self.phong_prog[ 'u_directional_light_count'] = self.directional_light_color.size self.phong_prog[ 'u_directional_light_dir'] = self.directional_light_dir.data self.phong_prog[ 'u_directional_light_color'] = self.directional_light_color.data self.phong_prog[ 'u_directional_light_specular'] = self.directional_light_specular.data # Ambient lights self.phong_prog[ 'u_ambient_light_count'] = self.ambient_light_color.size self.phong_prog[ 'u_ambient_light_color'] = self.ambient_light_color.data # Point lights self.phong_prog[ 'u_point_light_count'] = self.point_light_color.size self.phong_prog[ 'u_point_light_color'] = self.point_light_color.data self.phong_prog['u_point_light_pos'] = self.point_light_pos.data self.phong_prog[ 'u_point_light_specular'] = self.point_light_specular.data # Point light falloffs self.phong_prog['u_const_falloff'] = self.const_falloff.data self.phong_prog['u_linear_falloff'] = self.linear_falloff.data self.phong_prog[ 'u_quadratic_falloff'] = self.quadratic_falloff.data # Draw self.phong_prog.draw(draw_type, indices=self.index_buffer) else: raise NotImplementedError("Material not implemented") def flush_geometry(self): """Flush all the shape geometry from the draw queue to the GPU. """ for index, shape in enumerate(self.draw_queue): current_shape, current_obj = self.draw_queue[index][ 0], self.draw_queue[index][1] # If current_shape is lines, bring it to the front by epsilon # to resolve z-fighting if current_shape == 'lines': # line_transform is used whenever we render lines to break ties in depth # We transform the points to camera space, move them by # Z_EPSILON, and them move them back to world space line_transform = inv(self.lookat_matrix).dot( translation_matrix(0, 0, Z_EPSILON).dot(self.lookat_matrix)) vertices = current_obj[0] current_obj = (np.hstack([ vertices, np.ones((vertices.shape[0], 1)) ]).dot(line_transform.T)[:, :3], *current_obj[1:]) self.render_with_shaders(current_shape, current_obj) self.draw_queue = [] def cleanup(self): super(Renderer3D, self).cleanup() self.normal_prog.delete() self.phong_prog.delete() def add_ambient_light(self, r, g, b): self.ambient_light_color.add(np.array((r, g, b))) def add_directional_light(self, r, g, b, x, y, z): self.directional_light_color.add(np.array((r, g, b))) self.directional_light_dir.add(np.array((x, y, z))) self.directional_light_specular.add(self.light_specular) def add_point_light(self, r, g, b, x, y, z): self.point_light_color.add(np.array((r, g, b))) self.point_light_pos.add(np.array((x, y, z))) self.point_light_specular.add(self.light_specular) self.const_falloff.add(self.curr_constant_falloff) self.linear_falloff.add(self.curr_linear_falloff) self.quadratic_falloff.add(self.curr_quadratic_falloff)
class Renderer3D: def __init__(self): self.default_prog = None self.fbuffer = None self.fbuffer_tex_front = None self.fbuffer_tex_back = None self.vertex_buffer = None self.index_buffer = None ## Renderer Globals: USEFUL CONSTANTS self.COLOR_WHITE = (1, 1, 1, 1) self.COLOR_BLACK = (0, 0, 0, 1) self.COLOR_DEFAULT_BG = (0.8, 0.8, 0.8, 1.0) ## Renderer Globals: STYLE/MATERIAL PROPERTIES ## self.background_color = self.COLOR_DEFAULT_BG self.fill_color = self.COLOR_WHITE self.fill_enabled = True self.stroke_color = self.COLOR_BLACK self.stroke_enabled = True self.tint_color = self.COLOR_BLACK self.tint_enabled = False ## Renderer Globals: Curves self.stroke_weight = 1 self.stroke_cap = 2 self.stroke_join = 0 ## Renderer Globals ## VIEW MATRICES, ETC ## self.viewport = None self.texture_viewport = None self.transform_matrix = np.identity(4) self.projection_matrix = np.identity(4) self.lookat_matrix = np.identity(4) ## Renderer Globals: RENDERING self.draw_queue = [] def initialize_renderer(self): self.fbuffer = FrameBuffer() vertices = np.array( [[-1.0, -1.0], [+1.0, -1.0], [-1.0, +1.0], [+1.0, +1.0]], np.float32) texcoords = np.array([[0.0, 0.0], [1.0, 0.0], [0.0, 1.0], [1.0, 1.0]], dtype=np.float32) self.fbuf_vertices = VertexBuffer(data=vertices) self.fbuf_texcoords = VertexBuffer(data=texcoords) self.fbuffer_prog = Program(src_fbuffer.vert, src_fbuffer.frag) self.fbuffer_prog['texcoord'] = self.fbuf_texcoords self.fbuffer_prog['position'] = self.fbuf_vertices self.vertex_buffer = VertexBuffer() self.index_buffer = IndexBuffer() self.default_prog = Program(src_default.vert, src_default.frag) self.reset_view() def reset_view(self): self.viewport = ( 0, 0, int(builtins.width * builtins.pixel_x_density), int(builtins.height * builtins.pixel_y_density), ) self.texture_viewport = ( 0, 0, builtins.width, builtins.height, ) gloo.set_viewport(*self.viewport) cz = (builtins.height / 2) / math.tan(math.radians(30)) self.projection_matrix = matrix.perspective_matrix( math.radians(60), builtins.width / builtins.height, 0.1 * cz, 10 * cz) self.transform_matrix = np.identity(4) self.default_prog['projection'] = self.projection_matrix.T.flatten() self.default_prog['perspective_matrix'] = self.lookat_matrix.T.flatten( ) self.fbuffer_tex_front = Texture2D( (builtins.height, builtins.width, 3)) self.fbuffer_tex_back = Texture2D((builtins.height, builtins.width, 3)) for buf in [self.fbuffer_tex_front, self.fbuffer_tex_back]: self.fbuffer.color_buffer = buf with self.fbuffer: self.clear() self.fbuffer.depth_buffer = gloo.RenderBuffer( (builtins.height, builtins.width)) def clear(self, color=True, depth=True): """Clear the renderer background.""" gloo.set_state(clear_color=self.background_color) gloo.clear(color=color, depth=depth) def _comm_toggles(self, state=True): gloo.set_state(blend=state) gloo.set_state(depth_test=state) if state: gloo.set_state(blend_func=('src_alpha', 'one_minus_src_alpha')) gloo.set_state(depth_func='lequal') @contextmanager def draw_loop(self): """The main draw loop context manager. """ self.transform_matrix = np.identity(4) self.default_prog['projection'] = self.projection_matrix.T.flatten() self.default_prog['perspective_matrix'] = self.lookat_matrix.T.flatten( ) self.fbuffer.color_buffer = self.fbuffer_tex_back with self.fbuffer: gloo.set_viewport(*self.texture_viewport) self._comm_toggles() self.fbuffer_prog['texture'] = self.fbuffer_tex_front self.fbuffer_prog.draw('triangle_strip') yield self.flush_geometry() self.transform_matrix = np.identity(4) gloo.set_viewport(*self.viewport) self._comm_toggles(False) self.clear() self.fbuffer_prog['texture'] = self.fbuffer_tex_back self.fbuffer_prog.draw('triangle_strip') self.fbuffer_tex_front, self.fbuffer_tex_back = self.fbuffer_tex_back, self.fbuffer_tex_front def _transform_vertices(self, vertices, local_matrix, global_matrix): return np.dot(np.dot(vertices, local_matrix.T), global_matrix.T)[:, :3] def render(self, shape): if isinstance(shape, Geometry): n = len(shape.vertices) tverts = self._transform_vertices( np.hstack([shape.vertices, np.ones((n, 1))]), shape.matrix, self.transform_matrix) edges = shape.edges faces = shape.faces self.add_to_draw_queue('poly', tverts, edges, faces, self.fill_color, self.stroke_color) elif isinstance(shape, PShape): vertices = shape._draw_vertices n, _ = vertices.shape tverts = self._transform_vertices( np.hstack([vertices, np.zeros((n, 1)), np.ones((n, 1))]), shape._matrix, self.transform_matrix) fill = shape.fill.normalized if shape.fill else None stroke = shape.stroke.normalized if shape.stroke else None edges = shape._draw_edges faces = shape._draw_faces if edges is None: print(vertices) print("whale") exit() if 'open' in shape.attribs: overtices = shape._draw_outline_vertices no, _ = overtices.shape toverts = self._transform_vertices( np.hstack([overtices, np.zeros((no, 1)), np.ones((no, 1))]), shape._matrix, self.transform_matrix) self.add_to_draw_queue('poly', tverts, edges, faces, fill, None) self.add_to_draw_queue('path', toverts, edges[:-1], None, None, stroke) else: self.add_to_draw_queue(shape.kind, tverts, edges, faces, fill, stroke) def add_to_draw_queue(self, stype, vertices, edges, faces, fill=None, stroke=None): """Add the given vertex data to the draw queue. :param stype: type of shape to be added. Should be one of {'poly', 'path', 'point'} :type stype: str :param vertices: (N, 3) array containing the vertices to be drawn. :type vertices: np.ndarray :param edges: (N, 2) array containing edges as tuples of indices into the vertex array. This can be None when not appropriate (eg. for points) :type edges: None | np.ndarray :param faces: (N, 3) array containing faces as tuples of indices into the vertex array. For 'point' and 'path' shapes, this can be None :type faces: np.ndarray :param fill: Fill color of the shape as a normalized RGBA tuple. When set to `None` the shape doesn't get a fill (default: None) :type fill: None | tuple :param stroke: Stroke color of the shape as a normalized RGBA tuple. When set to `None` the shape doesn't get stroke (default: None) :type stroke: None | tuple """ fill_shape = self.fill_enabled and not (fill is None) stroke_shape = self.stroke_enabled and not (stroke is None) if fill_shape and stype not in ['point', 'path']: idx = np.array(faces, dtype=np.uint32).ravel() self.draw_queue.append(["triangles", (vertices, idx, fill)]) if stroke_shape: if stype == 'point': idx = np.arange(0, len(vertices), dtype=np.uint32) self.draw_queue.append(["points", (vertices, idx, stroke)]) else: idx = np.array(edges, dtype=np.uint32).ravel() self.draw_queue.append(["lines", (vertices, idx, stroke)]) def flush_geometry(self): """Flush all the shape geometry from the draw queue to the GPU. """ current_queue = [] for index, shape in enumerate(self.draw_queue): current_shape, current_obj = self.draw_queue[index][ 0], self.draw_queue[index][1] # If current_shape is lines, bring it to the front by epsilon # to resolve z-fighting if current_shape == 'lines': # line_transform is used whenever we render lines to break ties in depth # We transform the points to camera space, move them by Z_EPSILON, and them move them back to world space line_transform = inv(self.lookat_matrix).dot( translation_matrix(0, 0, Z_EPSILON).dot(self.lookat_matrix)) vertices = current_obj[0] current_obj = (np.hstack( [vertices, np.ones( (vertices.shape[0], 1))]).dot(line_transform.T)[:, :3], current_obj[1], current_obj[2]) current_queue.append(current_obj) if index < len(self.draw_queue) - 1: if self.draw_queue[index][0] == self.draw_queue[index + 1][0]: continue self.render_default(current_shape, current_queue) current_queue = [] self.draw_queue = [] def render_default(self, draw_type, draw_queue): # 1. Get the maximum number of vertices persent in the shapes # in the draw queue. # if len(draw_queue) == 0: return num_vertices = 0 for vertices, _, _ in draw_queue: num_vertices = num_vertices + len(vertices) # 2. Create empty buffers based on the number of vertices. # data = np.zeros(num_vertices, dtype=[('position', np.float32, 3), ('color', np.float32, 4)]) # 3. Loop through all the shapes in the geometry queue adding # it's information to the buffer. # sidx = 0 draw_indices = [] for vertices, idx, color in draw_queue: num_shape_verts = len(vertices) data['position'][sidx:(sidx + num_shape_verts), ] = np.array(vertices) color_array = np.array([color] * num_shape_verts) data['color'][sidx:sidx + num_shape_verts, :] = color_array draw_indices.append(sidx + idx) sidx += num_shape_verts self.vertex_buffer.set_data(data) self.index_buffer.set_data(np.hstack(draw_indices)) # 4. Bind the buffer to the shader. # self.default_prog.bind(self.vertex_buffer) # 5. Draw the shape using the proper shape type and get rid of # the buffers. # self.default_prog.draw(draw_type, indices=self.index_buffer) def cleanup(self): """Run the clean-up routine for the renderer. This method is called when all drawing has been completed and the program is about to exit. """ self.default_prog.delete() self.fbuffer_prog.delete() self.fbuffer.delete()