Home
🏛️

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
;