Game of Life with Frame Buffers#

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