Game of Life with Frame Buffers

Screenshot of game of life
game_of_life_colors.py
  1"""
  2Game of Life - Shader Version
  3
  4This version uses shaders which draws to textures to
  5run Conway's Game of Life with an added twist: colors.
  6
  7Press SPACE to reset the simulation with random data.
  8
  9It uses two textures: One to keep the old state and
 10a second to draw the new state into. These textures are
 11flipped around after every update.
 12
 13You can configure the cell and window size by changing
 14the constants at the top of the file after the imports.
 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"""
 19
 20import random
 21from array import array
 22
 23import arcade
 24from arcade import key
 25from arcade.gl import geometry
 26
 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 2nd 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        # Calculate how many cells we need to simulate at this pixel size
 41        self.texture_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.texture_size,
 46            components=3,
 47            filter=(self.ctx.NEAREST, self.ctx.NEAREST),
 48        )
 49        self.texture_2 = self.ctx.texture(
 50            self.texture_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 which calculates the next game state.
 91        # It uses the previous state as input (texture0) and
 92        # renders the next state 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        """Generate initial data.
170
171        We need to be careful about the initial game state. Carelessly
172        random numbers will make the simulation die in only a few frames.
173        Instead, we need to generate values which leave room for life
174        to exist.
175
176        The implementtation below is one of the slowest possible ways to
177        would generate the initial data, but it keeps things simple for
178        this example.
179        """
180        choices = [0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 32, 64, 128, 192, 255]
181        for i in range(num_values):
182            yield random.choice(choices)
183
184    def write_initial_state(self):
185        """Write initial data to the source texture."""
186        size = self.texture_size
187        self.texture_1.write(array("B", self.gen_initial_data(size[0] * size[1] * 3)))
188
189    def on_draw(self):
190        self.clear()
191
192        # Should we do an update this frame?
193        if self.frame % FRAME_DELAY == 0:
194            # Calculate the next state
195            self.fbo_2.use()  # Render to texture 2
196            self.texture_1.use()  # Take texture 1 as input
197            self.quad_fs.render(self.life_program)  # Run the life program
198
199            # Draw result to screen
200            self.ctx.screen.use()  # Switch back to rendering to screen
201            self.texture_2.use()  # Take texture 2 as input
202            self.quad_fs.render(self.display_program)  # Display the texture
203
204            # Swap things around for the next frame
205            self.texture_1, self.texture_2 = self.texture_2, self.texture_1
206            self.fbo_1, self.fbo_2 = self.fbo_2, self.fbo_1
207        # Otherwise just draw the current texture
208        else:
209            # Draw the current texture to the screen
210            self.ctx.screen.use()  # Switch back to rendering to screen
211            self.texture_1.use()  # Take texture 2 as input
212            self.quad_fs.render(self.display_program)  # Display the texture
213
214    def on_update(self, delta_time: float):
215        # Track the number of frames
216        self.frame += 1
217
218    def on_key_press(self, symbol: int, modifiers: int):
219        if symbol == key.SPACE:
220            self.write_initial_state()
221
222
223if __name__ == "__main__":
224    game = GameOfLife(WINDOW_WIDTH, WINDOW_HEIGHT)
225    game.run()