class Boids(moderngl_window.WindowConfig): title = "Boids" resource_dir = (Path(__file__) / '../resources').absolute() aspect_ratio = None window_size = 1024, 1024 resizable = False samples = 4 def __init__(self, *args, **kwargs): super().__init__(*args, **kwargs) self.move_program = self.load_program('move_boids.glsl') self.render_boids = self.load_program('render_boids.glsl') self.boid_logic = self.load_program('locality_info.glsl') self.boid_points = self.load_program('boid_points.glsl') self.view_area = self.load_program('render_view_area.glsl') self.texture_1 = self.ctx.texture(self.wnd.buffer_size, 4, dtype='f4') self.fbo_1 = self.ctx.framebuffer(color_attachments=[self.texture_1]) n = 50 # 3**n self.render_boids['size'].value = 0.01 self.render_boids['num_boids'].value = n positions = ((np.random.random_sample((n, 2)) - .5) * 2.) # positions = np.zeros((n, 2)) velocities = f(n, .75) * 0.005 acceleration = np.zeros((n, 2)) pos_vel = np.array([ *zip(positions.tolist(), velocities.tolist(), acceleration.tolist()) ]).flatten().astype('f4') self.boids_buffer_1 = self.ctx.buffer(pos_vel) self.boids_buffer_2 = self.ctx.buffer(reserve=pos_vel.nbytes) self.boids_vao_1 = VAO(name='boids_1') self.boids_vao_1.buffer( self.boids_buffer_1, '2f 2f 2f', ['in_position', 'in_velocity', 'in_acceleration']) self.boids_vao_2 = VAO(name='boids_2') self.boids_vao_2.buffer( self.boids_buffer_2, '2f 2f 2f', ['in_position', 'in_velocity', 'in_acceleration']) self.boid_logic['texture0'].value = 0 # self.view_area['texture0'].value = 0 def render(self, time, frame_time): # self.program['time'].value = time self.ctx.clear(51 / 255, 51 / 255, 51 / 255) # render info to texture self.fbo_1.use() self.boids_vao_1.render(self.boid_points, mode=moderngl.POINTS) # output updated velocity self.boids_vao_1.transform(self.boid_logic, self.boids_buffer_2, mode=moderngl.POINTS) # update their positions self.boids_vao_2.transform(self.move_program, self.boids_buffer_1, mode=moderngl.POINTS) # render boids to screen self.wnd.fbo.use() self.boids_vao_1.render(self.render_boids, mode=moderngl.POINTS)
class FragmentPicking(moderngl_window.WindowConfig): """ Demonstrates how you can pick exact positions on a model efficiently using the gpu. We render the scene info to various layers in an offscreen framebuffer and use this to fetch the view position of the picked fragment. We can then apply the inverse modelview to create points positions that matches the original mesh. In this example we pick points on a mesh mapped with a texture containing heat information. We pick the position, heat value and the normal of the fragment. The normal is then used to hide points that point away from the camera. """ title = "Fragment Picking" gl_version = 3, 3 window_size = 1280, 720 aspect_ratio = None resizable = True resource_dir = (Path(__file__) / '../../resources').resolve() def __init__(self, **kwargs): super().__init__(**kwargs) print("window buffer size:", self.wnd.buffer_size) self.marker_file = Path('markers.bin') # Object rotation self.x_rot = 0 self.y_rot = 0 # Object position self.zoom = 0 # Load scene cached to speed up loading! self.scene = self.load_scene('scenes/fragment_picking/centered.obj', cache=True) # Grab the raw mesh/vertexarray self.mesh = self.scene.root_nodes[0].mesh.vao self.mesh_texture = self.scene.root_nodes[ 0].mesh.material.mat_texture.texture self.projection = Projection3D( fov=60, aspect_ratio=self.wnd.aspect_ratio, near=1.0, far=100.0, ) # --- Offscreen render target # RGBA color/diffuse layer self.offscreen_diffuse = self.ctx.texture(self.wnd.buffer_size, 4) # Textures for storing normals (16 bit floats) self.offscreen_normals = self.ctx.texture(self.wnd.buffer_size, 4, dtype='f2') # Texture for storing the view positions rendered to framebuffer self.offscreen_viewpos = self.ctx.texture(self.wnd.buffer_size, 4, dtype='f4') # Texture for storing depth values self.offscreen_depth = self.ctx.depth_texture(self.wnd.buffer_size) # Create a framebuffer we can render to self.offscreen = self.ctx.framebuffer( color_attachments=[ self.offscreen_diffuse, self.offscreen_normals, self.offscreen_viewpos, ], depth_attachment=self.offscreen_depth, ) # This is just for temp changing depth texture parameters # temporary so we can use it as a normal texture self.depth_sampler = self.ctx.sampler( filter=(moderngl.LINEAR, moderngl.LINEAR), compare_func='', ) # A fullscreen quad just for rendering offscreen textures to the window self.quad_fs = geometry.quad_fs() # --- Shaders # Simple program just rendering texture self.texture_program = self.load_program( 'programs/fragment_picking/texture.glsl') self.texture_program['texture0'].value = 0 # Geomtry shader writing to two offscreen layers (color, normal) + depth self.geometry_program = self.load_program( 'programs/fragment_picking/geometry.glsl') self.geometry_program['texture0'].value = 0 # use texture channel 0 # Shader for linearizing depth (debug visualization) self.linearize_depth_program = self.load_program( 'programs/linearize_depth.glsl') self.linearize_depth_program['texture0'].value = 0 self.linearize_depth_program['near'].value = self.projection.near self.linearize_depth_program['far'].value = self.projection.far # Shader for picking the world position of a fragment self.fragment_picker_program = self.load_program( 'programs/fragment_picking/picker.glsl') self.fragment_picker_program[ 'position_texture'].value = 0 # Read from texture channel 0 self.fragment_picker_program[ 'normal_texture'].value = 1 # Read from texture channel 1 self.fragment_picker_program[ 'diffuse_texture'].value = 2 # Read from texture channel 2 # Picker geometry self.marker_byte_size = 7 * 4 # position + normal + temperature (7 x 32bit floats) self.picker_output = self.ctx.buffer(reserve=self.marker_byte_size) self.picker_vao = VAO(mode=moderngl.POINTS) # Shader for rendering markers self.marker_program = self.load_program( 'programs/fragment_picking/markers.glsl') self.marker_program['color'].value = 1.0, 0.0, 0.0, 1.0 # Marker geometry self.marker_buffer = self.ctx.buffer( reserve=self.marker_byte_size * 1000) # Resever room for 1000 points self.marker_vao = VAO(name="markers", mode=moderngl.POINTS) self.marker_vao.buffer(self.marker_buffer, '3f 3f 1f', ['in_position', 'in_normal', 'temperature']) self.num_markers = 0 # Debug geometry self.quad_normals = geometry.quad_2d(size=(0.25, 0.25), pos=(0.75, 0.875)) self.quad_depth = geometry.quad_2d(size=(0.25, 0.25), pos=(0.5, 0.875)) self.quad_positions = geometry.quad_2d(size=(0.25, 0.25), pos=(0.25, 0.875)) def render(self, time, frametime): self.ctx.enable(moderngl.DEPTH_TEST | moderngl.CULL_FACE) translation = Matrix44.from_translation((0, 0, -45 + self.zoom), dtype='f4') rotation = Matrix44.from_eulers((self.y_rot, self.x_rot, 0), dtype='f4') self.modelview = translation * rotation # Render the scene to offscreen buffer self.offscreen.clear() self.offscreen.use() # Render the scene self.geometry_program['modelview'].write(self.modelview) self.geometry_program['projection'].write(self.projection.matrix) self.mesh_texture.use( location=0) # bind texture from obj file to channel 0 self.depth_sampler.use(location=0) self.mesh.render(self.geometry_program) # render mesh self.depth_sampler.clear(location=0) # Activate the window as the render target self.ctx.screen.use() self.ctx.disable(moderngl.DEPTH_TEST) # Render offscreen diffuse layer to screen self.offscreen_diffuse.use(location=0) self.quad_fs.render(self.texture_program) # Render markers if self.num_markers > 0: self.ctx.point_size = 6.0 # Specify fragment size of the markers self.marker_program['modelview'].write(self.modelview) self.marker_program['projection'].write(self.projection.matrix) self.marker_vao.render(self.marker_program, vertices=self.num_markers) self.render_debug() def render_debug(self): """Debug rendering. Offscreen buffers""" # Debug rendering of normal and depth buffer self.offscreen_normals.use() self.quad_normals.render(self.texture_program) self.offscreen_depth.use(location=0) # bind depth sampler to channel 0 self.depth_sampler.use(location=0) # temp override the parameters self.quad_depth.render(self.linearize_depth_program) self.depth_sampler.clear(location=0) # Remove the override self.offscreen_viewpos.use() self.quad_positions.render(self.texture_program) def mouse_drag_event(self, x, y, dx, dy): """Pick up mouse drag movements""" self.x_rot -= dx / 100 self.y_rot -= dy / 100 def mouse_press_event(self, x, y, button): """Attempts to get the view position from a fragment""" # only care about right mouse button clicks if button != self.wnd.mouse.right: return # mouse coordinates starts in upper left corner # pixel positions starts and lower left corner pos = int(x * self.wnd.pixel_ratio), int(self.wnd.buffer_height - (y * self.wnd.pixel_ratio)) print("Picking mouse position", x, y) print("Viewport position", pos) self.fragment_picker_program['texel_pos'].value = pos self.fragment_picker_program['modelview'].write(self.modelview) self.offscreen_viewpos.use(location=0) self.offscreen_normals.use(location=1) self.offscreen_diffuse.use(location=2) self.picker_vao.transform(self.fragment_picker_program, self.picker_output, vertices=1) # Print position x, y, z, nx, ny, nz, temperature = struct.unpack( '7f', self.picker_output.read()) if z == 0.0: print('Point is not on the mesh') return print(f"Position: {x} {y} {z}") print(f"Normal: {nx} {ny} {nz}") print( f"Temperature: {round(temperature * 255)} (byte) {temperature} (float)" ) self.marker_buffer.write(self.picker_output.read(), offset=self.marker_byte_size * self.num_markers) self.num_markers += 1 def mouse_scroll_event(self, x_offset, y_offset): self.zoom += y_offset def key_event(self, key, action, modifiers): keys = self.wnd.keys # Key presses if action == keys.ACTION_PRESS: if key == keys.F1: print('Loading marker file') self.load_markers(self.marker_file) if key == keys.F2: print('Saving marker file') self.save_markers(self.marker_file) def load_markers(self, path: Path): """Loads markers from file""" if not self.marker_file.exists(): return with open(self.marker_file, mode='rb') as fd: size = self.marker_file.stat().st_size self.num_markers = size // self.marker_byte_size self.marker_buffer.write(fd.read(), offset=0) def save_markers(self, path: Path): """Dump markers from file""" if self.num_markers == 0: return with open('markers.bin', mode='wb') as fd: fd.write( self.marker_buffer.read(size=self.num_markers * self.marker_byte_size))
class Boids(moderngl_window.WindowConfig): """ An attempt to make something boid-list with GL3.3. Not currently working as intended, but still creates and interesting result. For this to properly work we need to split the calculations into several passes. We are doing this the O(n^2) way with the gpu using transform feedback. To make the data avaialble to the vertex shader (looping through it) we copy the vertex buffer every frame to a texture. A better way in the future is to use compute shader. """ title = "Boids" resource_dir = (Path(__file__) / '../../resources').absolute() aspect_ratio = 3440 / 1440 def __init__(self, *args, **kwargs): super().__init__(*args, **kwargs) MAX_TEX_WIDTH = 8192 N = MAX_TEX_WIDTH * 1 def gen_initial_data(n, x_area=2.0, y_area=2.0): for n in range(n): # position yield (random.random() - 0.5) * x_area yield (random.random() - 0.5) * y_area # Velocity yield (random.random() - 0.5) yield (random.random() - 0.5) # Create geometry data gen = gen_initial_data(N, x_area=self.aspect_ratio * 2 * 0.9, y_area=2.0 * 0.95) data = numpy.fromiter(gen, count=N * 4, dtype='f4') self.boids_buffer_1 = self.ctx.buffer(data.tobytes()) self.boids_buffer_2 = self.ctx.buffer(data=self.boids_buffer_1.read()) self.boids_vao_1 = VAO(name='boids_1', mode=moderngl.POINTS) self.boids_vao_1.buffer(self.boids_buffer_1, '2f 2f', ['in_position', 'in_velocity']) self.boids_vao_2 = VAO(name='boids_2', mode=moderngl.POINTS) self.boids_vao_2.buffer(self.boids_buffer_2, '2f 2f', ['in_position', 'in_velocity']) self.boids_texture = self.ctx.texture( (MAX_TEX_WIDTH, N * 2 // MAX_TEX_WIDTH), components=2, dtype='f4') # Programs self.boids_render_program = self.load_program( 'programs/boids/boids_render.glsl') self.boids_transform_program = self.load_program( 'programs/boids/boids_transform.glsl') # Prepare for rendering self.m_proj = matrix44.create_orthogonal_projection( -self.aspect_ratio, self.aspect_ratio, -1.0, 1.0, -1.0, 1.0, dtype='f4', ) self.boids_render_program['m_proj'].write(self.m_proj.tobytes()) self.boids_transform_program['data'].value = 0 self.boids_transform_program['num_boids'].value = N self.boids_transform_program['tex_width'].value = MAX_TEX_WIDTH def render(self, time, frame_time): self.boids_texture.use(location=0) self.boids_transform_program[ 'timedelta'].value = frame_time # max(frame_time, 1.0 / 60.0) self.boids_vao_1.transform(self.boids_transform_program, self.boids_buffer_2) self.boids_vao_2.render(self.boids_render_program) # Swap around .. self.boids_vao_1, self.boids_vao_2 = self.boids_vao_2, self.boids_vao_1 self.boids_buffer_1, self.boids_buffer_2 = self.boids_buffer_2, self.boids_buffer_1 # Write vertex data into texture so we can interate it in shader self.boids_texture.write(self.boids_buffer_1.read())