Game of Life with Frame Buffers

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()