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