The Game of Life is a grid-based simulation of cells thatevolves through discrete time steps based on simple rules, resulting in intricate and often unpredictable patterns. The emerging patterns from the interactions of live and dead cells showcases how complex structures and behaviors can arise from a simple set of rules and deterministic processes.
It consists of a 2D grid of cells - each cell has a value that represents 'life'. Value of 0 is dead (no life), but higher values indicate population in that cell.
To visualize the cells, you'd typically draw empty cells black which are considered 'dead', while non-zero cells are drawn (white or some other color) to indiciate they're 'alive'.
At each time step, all the cells reevaluate their state according to a set of rules.
Each cell has 3 choices:
1. Survives
2. Birth
3. Dies
These are the rules:
• Any live cell with fewer than two live neighbors dies (underpopulation) [9 neighbours around a cell]
• Any live cell with two or three live neighbors continues to live.
• Any live cell with more than three live neighbors dies (overpopulation).
• Any dead cell with exactly three live neighbors becomes a live cell (reproduction).
You iteratively calculate the state for each cell - causing the cells to evolve (live and die in a lifelike pattern) - hence the 'name'.
We can implement the game of life on the compute shader using two buffers (two textures buffers for the previous and next state).
Once it's implemented on the compute shader, it's just a matter of performing iterative updates, flipping the buffers for the input and output.
For the game to work, the initial texture needs to have some values - some noise - so it can be initialzied in the first frame. Pass a 'timer' counter - if the counter is '0' - then it's assumed that this is the first frame - and we set the output to a random value.
// Frame counter updated after each iteration @binding(0) @group(0) var<uniform> mytimer : f32; // Texture binding for rendering @binding(1) @group(0) var myTexture0: texture_storage_2d<rgba8unorm, read>; @binding(2) @group(0) var myTexture1: texture_storage_2d<rgba8unorm, write>;
// Main compute shader function @compute @workgroup_size(8, 8) fn main( @builtin(global_invocation_id) globalId: vec3<u32>, @builtin(local_invocation_id) localId: vec3<u32>, @builtin(workgroup_id) workgroupId: vec3<u32>, @builtin(num_workgroups) workgroupSize: vec3<u32> ) { if ( mytimer <= 0.0 ) { var r = random( vec2<f32>( f32(globalId.x), f32(globalId.y) ) ); if ( r < 0.94 ) { r = 0.0; } else { r = 1.0; } textureStore(myTexture1, vec2<i32>(globalId.xy), vec4<f32>( f32(r), 0, 0, 1.0)); return; }
// Calculate the index of the cell let index = ( globalId.y * u32(gridSize) ) + globalId.x;
// Read the current state of the cell let currentState = u32( textureLoad(myTexture0, vec2<i32>(globalId.xy) ).r );
// Count the number of alive neighbors var aliveNeighbors = 0u; for (var dx : i32 = -1; dx <= 1; dx++) { for (var dy : i32 = -1; dy <= 1; dy++) { if (dx == 0 && dy == 0) { continue; // Skip the current cell }
let neighborX = i32(globalId.x) + dx; let neighborY = i32(globalId.y) + dy;
// Apply Conway's Game of Life rules // 3 options // 1 - survive // 2 - birth // 3 - death var nextState = 0u; if ( currentState == 1u && (aliveNeighbors == 2u || aliveNeighbors == 3u)) { nextState = 1u; // Cell survives } else if ( u32(currentState) == 0u && aliveNeighbors == 3u) { nextState = 1u; // Cell is born } // death // Overcrowding: if a cell is alive and 4 or more of its neighbors are also alive - the cell will be dead // Exposure: If a live cell has only 1 live neighbor or no live neighbors, it will be dead
var g = 0.0; var b = 0.0; if ( aliveNeighbors > 3 ) { b = 1.0; } if ( currentState != nextState ) { g = 1.0; }
// Write the next state to the output buffer textureStore(myTexture1, vec2<i32>(globalId.xy), vec4<f32>( f32(nextState), g, b, 1.0)); }
Game of life output after a few minutes.
Things to Try
• Starting value - instead of random noise all over the texture - focus it in a small circle in the middle (so that the life works outwards)
• Initialise the random starting texture pattern to be in the shape of 'text' using the canvas write to initize the texture before it's passed to the shader (e.g., words 'LIFE' written in random points)
• Try adding in some additional logic (random changing of states)
• Create a larger texture, e.g., 1024, split the texture into 'regions' different regions have different parameters (add some extra control logic/probabilities)