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