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