带帧缓冲的生活游戏#
game_of_life_colors.py#
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 187 188 189 190 191 192 193 194 195 196 197 198 199 200 201 202 203 204 205 206 207 208 209 210 211 212 213 214 | """ Game of Life - Shader Version We're doing this in in a simple way drawing to textures. We need two textures. One to keep the old state and another to draw the new state into. These textures are flipped around every frame. This version of Game of Life also use colors. Dominant colonies will keep spreading their color. Press SPACE to generate new initial data The cell and window size can be tweaked in the parameters below. """ import random from array import array from arcade import key import arcade from arcade.gl import geometry CELL_SIZE = 2 # Cell size in pixels WINDOW_WIDTH = 512 # Width of the window WINDOW_HEIGHT = 512 # Height of the window FRAME_DELAY = 2 # The game will only update every 2th frame class GameOfLife(arcade.Window): def __init__(self, width, height): super().__init__(width, height, "Game of Life - Shader Version") self.frame = 0 # Configure the size of the playfield (cells) self.size = width // CELL_SIZE, height // CELL_SIZE # Create two textures for the next and previous state (RGB textures) self.texture_1 = self.ctx.texture( self.size, components=3, filter=(self.ctx.NEAREST, self.ctx.NEAREST), ) self.texture_2 = self.ctx.texture( self.size, components=3, filter=(self.ctx.NEAREST, self.ctx.NEAREST) ) self.write_initial_state() # Add the textures to framebuffers so we can render to them self.fbo_1 = self.ctx.framebuffer(color_attachments=[self.texture_1]) self.fbo_2 = self.ctx.framebuffer(color_attachments=[self.texture_2]) # Fullscreen quad (using triangle strip) self.quad_fs = geometry.quad_2d_fs() # Shader to draw the texture self.display_program = self.ctx.program( vertex_shader=""" #version 330 in vec2 in_vert; in vec2 in_uv; out vec2 uv; void main() { gl_Position = vec4(in_vert, 0.0, 1.0); uv = in_uv; } """, fragment_shader=""" #version 330 uniform sampler2D texture0; out vec4 fragColor; in vec2 uv; void main() { fragColor = texture(texture0, uv); } """, ) # Shader for creating the next game state. # It takes the previous state as input (texture0) # and renders the next state directly into the second texture. self.life_program = self.ctx.program( vertex_shader=""" #version 330 in vec2 in_vert; void main() { gl_Position = vec4(in_vert, 0.0, 1.0); } """, fragment_shader=""" #version 330 uniform sampler2D texture0; out vec4 fragColor; // Check if something is living in the cell bool cell(vec4 fragment) { return length(fragment.xyz) > 0.1; } void main() { // Get the pixel position we are currently writing ivec2 pos = ivec2(gl_FragCoord.xy); // Grab neighbor fragments + current one vec4 v1 = texelFetch(texture0, pos + ivec2(-1, -1), 0); vec4 v2 = texelFetch(texture0, pos + ivec2( 0, -1), 0); vec4 v3 = texelFetch(texture0, pos + ivec2( 1, -1), 0); vec4 v4 = texelFetch(texture0, pos + ivec2(-1, 0), 0); vec4 v5 = texelFetch(texture0, pos, 0); vec4 v6 = texelFetch(texture0, pos + ivec2(1, 0), 0); vec4 v7 = texelFetch(texture0, pos + ivec2(-1, 1), 0); vec4 v8 = texelFetch(texture0, pos + ivec2( 0, 1), 0); vec4 v9 = texelFetch(texture0, pos + ivec2( 1, 1), 0); // Cell in current position is alive? bool living = cell(v5); // Count how many neighbors is alive int neighbours = 0; if (cell(v1)) neighbours++; if (cell(v2)) neighbours++; if (cell(v3)) neighbours++; if (cell(v4)) neighbours++; if (cell(v6)) neighbours++; if (cell(v7)) neighbours++; if (cell(v8)) neighbours++; if (cell(v9)) neighbours++; // Average color for all neighbors vec4 sum = (v1 + v2 + v3 + v4 + v6 + v7 + v8 + v9) / neighbours; if (living) { if (neighbours == 2 || neighbours == 3) { // The cell lives, but we write out the average color minus a small value fragColor = vec4(sum.rgb - vec3(1.0/255.0), 1.0); } else { // The cell dies when too few or too many neighbors fragColor = vec4(0.0, 0.0, 0.0, 1.0); } } else { if (neighbours == 3) { // A new cell was born fragColor = vec4(normalize(sum.rgb), 1.0); } else { // Still dead fragColor = vec4(0.0, 0.0, 0.0, 1.0); } } } """, ) def gen_initial_data(self, num_values: int): """ Generate initial data. We need to be careful about the initial state. Just throwing in lots of random numbers will make the entire system die in a few frames. We need to give enough room for life to exist. This might be the slowest possible way we would generate the initial data, but it works for this example :) """ choices = [0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 32, 64, 128, 192, 255] for i in range(num_values): yield random.choice(choices) def write_initial_state(self): """Write initial data to the source texture""" self.texture_1.write(array('B', self.gen_initial_data(self.size[0] * self.size[1] * 3))) def on_draw(self): self.clear() # Should we do an update this frame? if self.frame % FRAME_DELAY == 0: # Calculate the next state self.fbo_2.use() # Render to texture 2 self.texture_1.use() # Take texture 1 as input self.quad_fs.render(self.life_program) # Run the life program # Draw result to screen self.ctx.screen.use() # Switch back to redering to screen self.texture_2.use() # Take texture 2 as input self.quad_fs.render(self.display_program) # Display the texture # Swap things around for the next frame self.texture_1, self.texture_2 = self.texture_2, self.texture_1 self.fbo_1, self.fbo_2 = self.fbo_2, self.fbo_1 # Otherwise just draw the current texture else: # Draw the current texture to the screen self.ctx.screen.use() # Switch back to redering to screen self.texture_1.use() # Take texture 2 as input self.quad_fs.render(self.display_program) # Display the texture def on_update(self, delta_time: float): # Track the number of frames self.frame += 1 def on_key_press(self, symbol: int, modifiers: int): if symbol == key.SPACE: self.write_initial_state() GameOfLife(WINDOW_WIDTH, WINDOW_HEIGHT).run() |