Game of Life Tower
This sketch renders Conway’s Game of Life with a twist, each generation is a column on an ever-growing stack.
The cells render on a torus so they wrap around the edges, and I’ve found a resolution of 64x64 keeps the tower going in interesting ways.
Toggle shader source Remix this
:: wrap-cell ( cell -- wrapped )
cell world-size mod
;
:: seed-at ( cell -- alive )
0.69 cell noise step
;
:: state-at ( cell -- alive )
cell wrap-cell 0.5 + previous-at .a
;
:: current-state ( cell -- alive )
cell seed-at
cell state-at
0.5 frame step
mix
;
:: neighbors ( cell -- count )
cell -1.0 -1.0 vec2 + current-state
cell 0.0 -1.0 vec2 + current-state +
cell 1.0 -1.0 vec2 + current-state +
cell -1.0 0.0 vec2 + current-state +
cell 1.0 0.0 vec2 + current-state +
cell -1.0 1.0 vec2 + current-state +
cell 0.0 1.0 vec2 + current-state +
cell 1.0 1.0 vec2 + current-state +
;
:: equals ( value target -- mask )
target 0.5 - value step
value target 0.5 + step
*
;
:: life-rule ( alive count -- next )
count 3.0 equals
count 2.0 equals alive *
+
0.0 1.0 clamp
;
:: next-state ( cell -- alive )
cell current-state
cell neighbors
life-rule
;
\ Advance once every eight rendered frames.
: advance
1.0 0.5 frame layer-frames mod step -
;
\ Keep the next generation ready in alpha; advance it only on ticks.
:: stored-state ( cell -- alive )
cell state-at
cell next-state
advance
mix
;
: cell-span
resolution .x world-size /
render-scale *
;
: cube-height
cell-span 3.0 /
1.0 max
;
: layer-frames
cube-height floor
1.0 max
;
: scroll-step
cube-height layer-frames /
;
\ Inverse isometric projection for the newest layer.
:: iso-x ( p -- amount )
p .x resolution .x 0.5 * -
cell-span /
;
:: iso-y ( p -- amount )
resolution .y 0.82 * p .y -
cell-span 0.5 * /
;
:: iso-grid ( p -- grid )
p iso-y p iso-x +
p iso-y p iso-x -
vec2
;
:: range-mask ( value -- mask )
0.0 value step
value world-size step
*
;
:: inset-mask ( value -- mask )
0.06 value fract step
value fract 0.94 step
*
;
:: grid-mask ( grid -- mask )
grid .x range-mask
grid .y range-mask *
grid .x inset-mask *
grid .y inset-mask *
;
\ A live cell is the inset isometric top face.
:: live-mask ( p -- mask )
p iso-grid dup floor current-state
swap grid-mask
*
;
\ Extrude the diamond downward to form two visible cube sides.
:: cube-masks ( p -- shadow body top )
p cell-span -0.58 * cube-height 1.12 * vec2 + live-mask
p 0.0 cube-height 0.25 * vec2 + live-mask
p 0.0 cube-height 0.50 * vec2 + live-mask max
p 0.0 cube-height 0.75 * vec2 + live-mask max
p 0.0 cube-height vec2 + live-mask max
p live-mask
;
:: face-split ( p -- amount )
p 0.0 cube-height vec2 + iso-grid .x fract
p 0.0 cube-height vec2 + iso-grid .y fract
step
;
:: side-color ( p -- color )
0.12 0.27 0.46 vec3
0.24 0.46 0.68 vec3
p face-split
mix
;
\ Paint shadow first, then the side faces, then the lit top.
:: paint-cube ( p shadow body top -- color mask )
0.003 0.006 0.012 vec3
p side-color body mix
0.82 0.91 1.0 vec3 top mix
shadow 0.42 *
body max
top max
;
:: new-layer ( p -- color mask )
p
p cube-masks
paint-cube
;
: background 0.018 0.024 0.040 vec3 ;
\ Shift the existing RGB history down by one full cube layer.
:: history ( p -- color )
background
background
p 0.0 scroll-step vec2 + previous-at .rgb
p .y scroll-step + resolution .y step
mix
0.5 frame step
mix
;
\ Width and height of the square cellular world.
: world-size
64.0
;
\ The full grid occupies this fraction of the canvas width.
: render-scale 0.78 ;
: main
frag-coord history
frag-coord new-layer
advance *
mix
\ Replicate the state through alpha across the framebuffer.
frag-coord world-size mod floor stored-state
vec4
;