Game of Life shader

Here’s my “Game of Life” shader, with the shaders built in as strings for ease of import/export. Note the comment about retina displays - I forget, is there some setting that can test for this?

If anyone has any ideas for better starting conditions, I’d love to hear them.

It’s basically the program in:

http://www.youtube.com/watch?v=tZTIiKcqdtI

except without the torus as that needs some stuff from my library so it makes it hard to make into a self contained program.

There are actually two shaders here. One is the basic Game of Life shader. The idea of this is to have two images and to alternate between them, reading from one and drawing on the other. The colours of pixels determine whether or not it is alive, but as we can pack more information than simply “alive - dead”, we also record the time since it died (if it is dead), and whether or not it has ever been alive. The main technical issue in this shader is that we want to know exactly which pixel we are dealing with, but the fragment shader works on a scaled basis where the image is viewed as having size 1x1 so we have to know the image’s actual width and height to know how far across the next pixel lies. Loops and arrays are a little problematic in shaders (or at least, I haven’t yet figured out how to do them properly), so I unrolled the obvious loop.

The second shader is a renderer. It takes the raw GoL data and “pretty prints” it. So it uses all the information to decide how to colour a given pixel. This leads to the “firey” effect as pixels die. In the full program, I use the “was once alive” to do a “reveal”. If a pixel is dead but has been alive then the colour is taken from an image. Then it looks as though there’s a picture with a cover that is being eaten away by the GoL pixels.

The noSmooth() is essential to ensuring that the GoL shader works pixel-by-pixel and not fuzzy pixel-by-fuzzy pixel. You can put a smooth() before the m:draw() to get a more smooth rendering.

For those with retina displays, there’s an interesting lesson on precision in shaders in this code. If you change the vfract function to have either mediump or lowp precision (for the return value), then you get very bizarre behaviour:

http://www.youtube.com/watch?v=Nn21l7OAkXA

displayMode(FULLSCREEN)

function setup()
    local w,h = WIDTH,HEIGHT
    source = image(w,h)
    target = image(w,h)
    gol = mesh()
    local s = shader()
    s.vertexProgram, s.fragmentProgram = golshader()
    gol.shader = s
    gol.texture = source
    gol:addRect(w/2,h/2,w,h)
    -- Factor of 2 is for retina displays
    gol.shader.width = 2*w
    gol.shader.height = 2*h
    local nf = 50 + 5*math.random()
    local xd = math.random()
    local yd = math.random()
    for x = 1,w do
        for y = 1,h do
            if noise(nf*x/w + xd,nf*y/h + yd) > .4 then
                source:set(x,y,color(0,255,0,255))
            else
                source:set(x,y,color(0,0,0,255))
            end
        end
    end
    mm = mesh()
    mm:addRect(WIDTH/2,HEIGHT/2,WIDTH,HEIGHT)
    s = shader()
    s.vertexProgram, s.fragmentProgram = golrshader()
    mm.shader = s
    mm.texture = source
    mm.shader.colour = color(36, 45, 69, 255)
    mm.shader.alive = color(30, 164, 23, 255)
    mm:setColors(255,255,255,255)
end

function draw()
    background(56, 62, 61, 255)
    noSmooth()
    setContext(target)
    gol:draw()
    setContext()
    source, target = target, source
    gol.texture = source
    mm.texture = source
    mm:draw()
end

function touched(touch)
end

function golshader()
    return [[
//
// A Game of Life vertex shader, does almost nothing.
//

//This is the current model * view * projection matrix
// Codea sets it automatically
uniform mat4 modelViewProjection;

//This is the current mesh vertex position, color and tex coord
// Set automatically
attribute vec4 position;
attribute vec4 color;
attribute vec2 texCoord;

//This is an output variable that will be passed to the fragment shader
varying lowp vec4 vColor;
varying highp vec2 vTexCoord;


void main()
{
    //Pass the everything to the fragment shader
    vColor = color;
    vTexCoord = texCoord;
    gl_Position = modelViewProjection * position;
}
]],
[[
//
// A Game of Life fragment shader, does the hard work.
//

//This represents the current texture on the mesh
uniform lowp sampler2D texture;

//The interpolated vertex color for this fragment
varying lowp vec4 vColor;

//The interpolated texture coordinate for this fragment
varying highp vec2 vTexCoord;

// width and height of the texture for finding neighbours
uniform highp float width;
uniform highp float height;
lowp float colstep = .05;
highp float w = 1./width;
highp float h = 1./height;

highp vec2 vfract(highp vec2 v)
{
    return vec2(fract(v.x),fract(v.y));
}

// get the neighbours' states
lowp int neighbours (lowp sampler2D s, highp vec2 p)
{
    lowp int alive;
    alive = 0;
    if (texture2D(s,vfract(p + vec2(w,0.))).g > .5)
        alive += 1;
    if (texture2D(s,vfract(p + vec2(w,h))).g > .5)
        alive += 1;
    if (texture2D(s,vfract(p + vec2(0.,h))).g > .5)
        alive += 1;
    if (texture2D(s,vfract(p + vec2(-w,h))).g > .5)
        alive += 1;
    if (texture2D(s,vfract(p + vec2(-w,0.))).g > .5)
        alive += 1;
    if (texture2D(s,vfract(p + vec2(-w,-h))).g > .5)
        alive += 1;
    if (texture2D(s,vfract(p + vec2(0.,-h))).g > .5)
        alive += 1;
    if (texture2D(s,vfract(p + vec2(w,-h))).g > .5)
        alive += 1;
    return alive;
}

void main()
{
    lowp int count;
    lowp vec4 col;
    count = neighbours(texture,vTexCoord);
    col = texture2D(texture,vTexCoord);

    if (col.r > colstep) {
        col.r -= colstep;
    } else {
        col.r = 0.;
    }
    if (col.g > .5) {
    // alive
        if (count < 2 || count > 3) {
        // lonely or overcrowded, kill it
            col.g = 0.;
            col.r = 1.;
        }
    } else {
    // dead
        if (count == 3) {
        // born
            col.g = 1.;
            col.r = 1.;
            col.b = 1.;
        }
    }

    //Set the output color to the texture color
    gl_FragColor = col;
}
]]
end

function golrshader()
    return [[
//
// Rendering vertex shader, does almost nothing.
//

//This is the current model * view * projection matrix
// Codea sets it automatically
uniform mat4 modelViewProjection;

//This is the current mesh vertex position, color and tex coord
// Set automatically
attribute vec4 position;
attribute vec4 color;
attribute vec2 texCoord;

//This is an output variable that will be passed to the fragment shader
varying lowp vec4 vColor;
varying highp vec2 vTexCoord;

void main()
{
    //Pass the everything to the fragment shader
    vColor = color;
    vTexCoord = texCoord;
    gl_Position = modelViewProjection * position;
}
]],[[
//
// Rendering fragment shader, interprets the raw GoL data.
//

//This is the raw data
uniform lowp sampler2D texture;

//Colours for rendering
uniform lowp vec4 colour;
uniform lowp vec4 alive;

//The interpolated vertex color for this fragment
varying lowp vec4 vColor;

//The interpolated texture coordinate for this fragment
varying highp vec2 vTexCoord;

void main()
{
    //Sample the data at the interpolated coordinate
    lowp vec4 col;
    col = texture2D( texture, vTexCoord );

    if (col.g == 1.) {
        // alive, fade from yellow to blue
        col.b = 0.;
        col = col.r*col + (1.-col.r)*alive;
    } else {
        // dead
        if (col.r > 0.) {
            // but recently so
            col.g = 0.;
            col.b = 0.;
            col.rgb = col.rgb + (1. - col.r)*colour.rgb;
        } else {
            col.rgb = colour.rgb;
        }
    }

    //Set the output color
    gl_FragColor = col;
}
]]
end

Impressive…

For non retina I had to tweak the following lines

gol.shader.width = 2*w
    gol.shader.height = 2*h

removing the “2*” from each.

Runs about 20fps on my Ipad2

.@spacemonkey You can trade resolution for speed. It’ll always display at full screen, but you can set w and h to something smaller for speed.

The “full” version (with the torus) at full size (ie WIDTH and HEIGHT) runs at about 29fps on my iPad 4.

Help me understand…it looks like you have a shader that is calling another shader? Or do I not have this correct?

I’m curious if I can have two shaders affect a geomerty at the same time, and it looks like you’ve done soomething along those lines.

.@aciolino No, not quite. I have two shaders but they work sequentially. What happens is this:

source --GoL Shader--> target --Renderer--> screen

where source and target are two images, and each iteration they get swapped. (Actually the swap happens before the renderer shader gets to work so in the code it looks like the renderer is working on the source image.) The key is in having the first shader output its stuff to an image rather than to the screen.

This is only really possible with fragment shaders. With vertex shaders then you can’t easily save the vertex locations without a lot of bother.

Oh, I see - in the draw loop you switch your buffers. Wierd.

.@aciolino It’s because the Game of Life needs to read in data from the previous run, but can’t overwrite the previous data until the current run is complete. So you need two images and need to keep alternating them.

Something just seems wrong with having two “void main()” entries. :smiley:

.@jlslate I only have one per shader program …

Hello @Andrew_Stacey. Were you thinking of the (undocumented) deviceMetrics() function?

if deviceMetrics().platformName == "iPad 1G" etc

As an iPad 2 owner, I cannot tell you if it has kept up with the iPad 4. When I have a moment, I’ll write it up on the Wiki.

I added what I know about deviceMetrics() to the wiki here, but I do not if or how it works with the more recent models of the iPad.

@andrew_stacey, is your technique effectively the only way we can do multi pass shaders in this version of Codea? I am trying to model fur, but I only have a sample that relies on multipass texturing, and I can’t simply put a loop in the vertex shader…

@Andrew_ Stacey, I was looking at this project of yours because redacted inspired me to think about life simulations.

I tried doing one myself but was frustrated by how few creatures I could simulate without seeing massive slowdown. So I have been playing with yours, which seems to be able to handle hundreds of creatures with no problem.

I’m trying to modify your setup so that, in the shader that does the logic (the gol shader) the whole screen is broken into 4 pixel by 4 pixel units, arranged like this:

C  M
D  D
  • C : the Color pixel: encodes the creature’s color; can be any color but the R value must always be over 8–this marks it as the color pixel–all the others are always under
  • M: the Move pixel: its R value is always in the range 0 through 8; what value it has determines the movement instruction for the creature, with 0-8 being the possible directions to each neighboring pixel (and 0 being stillness).
  • D: other Data pixels: not used for this example but will be available to store other information as needed
  • For a location that has no ‘creature’ in it, only the Color pixel would have any value at all.

Then, in the shader that does the drawing to screen (the golr shader), it should read the C value at every 4-pixel set and draw all the other pixels that color too. So golr essentially scales just the C pixels up 2x without changing the total image size. This hides the data the gol shader uses to decide where to move things.

If it worked, I think we could code some pretty interesting behavior, and see it rapidly propagate across hundreds of ‘individuals’.

But I’m getting stuck right at the beginning. My first goal was to get the scaling working–so that the data pixels would be invisible to the user.

I have tried to do it by using your routine to detect neighboring colors. I’ve managed to set the C pixels at every four-pixel block, and now I’m trying to get the golr shader to try fill all the non-C blocks with the C color.

It’s only kinda sorta working. I’m detecting the C pixel orthogonally but not diagonally. And there seem to be two pixels between every C pixel laterally, not one–even though visually it looks like just one. So I’m a little lost. I’d love any insight you can offer. Code in next post.

Here’s the code.

function setup()
    showBuffer = true
    readPixelCode = true
    local w,h = WIDTH,HEIGHT
    local w,h = 20,20
    source = image(w,h)
    target = image(w,h)
    gol = mesh()
    local s = shader()
    s.vertexProgram, s.fragmentProgram = golshader()
    gol.shader = s
    gol.texture = source
    gol:addRect(w/2,h/2,w,h)
    -- Factor of 2 is for retina displays
    gol.shader.width = 2*w
    gol.shader.height = 2*h
    local nf = 50 + 5*math.random()
    local xd = math.random()
    local yd = math.random()
    if readPixelCode then
        for x = 1,w do
            for y = 1,h do
                if math.fmod(y,2) == 0 and math.fmod(x,2) == 1 then
                    source:set(x,y,color(100,255,0,255)) --processor color for aliveness
                -- elseif math.fmod(y,2) == 0 and math.fmod(x,2) ==  0 then
     else
                    source:set(x,y,color(0,0,0,255)) --processor marker color
                end
            end
        end
    else
        for x = 1,w do
            for y = 1,h do
                if noise(nf*x/w + xd,nf*y/h + yd) > .4 then
                    source:set(x,y,color(0,255,0,255))
                else
                    source:set(x,y,color(0,0,0,255))
                end
            end
        end
    end
    mm = mesh()
    if readPixelCode then
        mm:addRect(WIDTH/2,HEIGHT/2,WIDTH,HEIGHT)
        -- mm:addRect(WIDTH/2,HEIGHT/2,w*2,h*2)
    else
        mm:addRect(WIDTH/2,HEIGHT/2,WIDTH,HEIGHT)
    end
    s = shader()
    s.vertexProgram, s.fragmentProgram = golrshader()
    mm.shader = s
    mm.texture = source
    mm.shader.colour = color(36, 45, 69, 255)
    mm.shader.alive = color(30, 164, 23, 255)
    mm:setColors(255,255,255,255)
    -------------
    if readPixelCode then 
        mm.shader.width = gol.shader.width
        mm.shader.height = gol.shader.height
        mm.shader.pixelReading = true;
    end
end

function draw() 
    background(56, 62, 61, 255)
    noSmooth()
    setContext(target)
    gol:draw()
    setContext()
    source, target = target, source
    gol.texture = source
    mm.texture = source
    mm:draw()
    if showBuffer then
        ortho()
        viewMatrix(matrix())
        sprite(target,170,170,320,320)
    end
end

function touched(touch)
end

function golshader()
    return [[
//
// A Game of Life vertex shader, does almost nothing.
//

//This is the current model * view * projection matrix
// Codea sets it automatically
uniform mat4 modelViewProjection;

//This is the current mesh vertex position, color and tex coord
// Set automatically
attribute vec4 position;
attribute vec4 color;
attribute vec2 texCoord;

//This is an output variable that will be passed to the fragment shader
varying lowp vec4 vColor;
varying highp vec2 vTexCoord;


void main()
{
    //Pass the everything to the fragment shader
    vColor = color;
    vTexCoord = texCoord;
    gl_Position = modelViewProjection * position;
}
]],
[[
//
// A Game of Life fragment shader, does the hard work.
//

//This represents the current texture on the mesh
uniform lowp sampler2D texture;

//The interpolated vertex color for this fragment
varying lowp vec4 vColor;

//The interpolated texture coordinate for this fragment
varying highp vec2 vTexCoord;

// width and height of the texture for finding neighbours
uniform highp float width;
uniform highp float height;
lowp float colstep = .05;
highp float w = 1./width;
highp float h = 1./height;

highp vec2 vfract(highp vec2 v)
{
    return vec2(fract(v.x),fract(v.y)); //fract(whatever) returns just the decimal part?
}

// get the neighbours' states
lowp int neighbours (lowp sampler2D s, highp vec2 p)
{
    lowp int alive;
    alive = 0;
    if (texture2D(s,vfract(p + vec2(w,0.))).g > .5) 
        alive += 1;
    if (texture2D(s,vfract(p + vec2(w,h))).g > .5)
        alive += 1;
    if (texture2D(s,vfract(p + vec2(0.,h))).g > .5)
        alive += 1;
    if (texture2D(s,vfract(p + vec2(-w,h))).g > .5)
        alive += 1;
    if (texture2D(s,vfract(p + vec2(-w,0.))).g > .5)
        alive += 1;
    if (texture2D(s,vfract(p + vec2(-w,-h))).g > .5)
        alive += 1;
    if (texture2D(s,vfract(p + vec2(0.,-h))).g > .5)
        alive += 1;
    if (texture2D(s,vfract(p + vec2(w,-h))).g > .5)
        alive += 1;
    return alive;
}

void main()
{
    lowp int count;
    lowp vec4 col;
    count = neighbours(texture,vTexCoord);
    col = texture2D(texture,vTexCoord);


    if (col.r > colstep) {
        col.r -= colstep;
    } else {
        col.r = 0.;
    }

    if (col.g > .5) {
    // alive
        if (count < 2 || count > 3) {
        // lonely or overcrowded, kill it
            col.g = 0.;
            col.r = 1.;
        }
    } else {
    // dead
        if (count == 3) {
        // born
            col.g = 1.;
            col.r = 1.;
            col.b = 1.;
        }
    }


    //Set the output color to the texture color
    gl_FragColor = col;
}
]]
end

function golrshader()
    return [[
//
// Rendering vertex shader, does almost nothing.
//

//This is the current model * view * projection matrix
// Codea sets it automatically
uniform mat4 modelViewProjection;

//This is the current mesh vertex position, color and tex coord
// Set automatically
attribute vec4 position;
attribute vec4 color;
attribute vec2 texCoord;

//This is an output variable that will be passed to the fragment shader
varying lowp vec4 vColor;
varying highp vec2 vTexCoord;

void main()
{
    //Pass the everything to the fragment shader
    vColor = color;
    vTexCoord = texCoord;
    gl_Position = modelViewProjection * position;
}
]],[[
//
// Rendering fragment shader, interprets the raw GoL data.
//

//This is the raw data
uniform lowp sampler2D texture;

//Colours for rendering
uniform lowp vec4 colour;
uniform lowp vec4 alive;

//The interpolated vertex color for this fragment
varying lowp vec4 vColor;

//The interpolated texture coordinate for this fragment
varying highp vec2 vTexCoord;

    
uniform bool pixelReading;
uniform highp float width;
uniform highp float height;
highp float w = 1./width;
highp float h = 1./height;

highp vec2 vfract(highp vec2 v)
{
    return vec2(fract(v.x),fract(v.y)); //fract(whatever) returns just the decimal part?
}

//-----------------trying to read other colors
    
// get the color to the left
lowp vec4 toLeft ()
{
    //lowp vec4 found = texture2D(texture,vfract(vTexCoord + vec2(0.,-h))); //this is color above or below
    lowp vec4 found = texture2D(texture,vfract(vTexCoord + vec2(-w,0.))); //
    return found;
}
    
// get the color TWO to the left
lowp vec4 twoToLeft (lowp sampler2D s, highp vec2 p)
{
    //lowp vec4 found = texture2D(s,vfract(p + vec2(0.,-h))); //this is color above or below
    lowp vec4 found = texture2D(s,vfract(p + vec2(-w*2.,0.))); //
    return found;
}
    
// get the color above (or below? not sure)
lowp vec4 toUpOrDown (lowp sampler2D s, highp vec2 p)
{
    lowp vec4 found = texture2D(s,vfract(p + vec2(0.,h))); //this is color above or below
    return found;
}
    
// get the color TWO above (or below? not sure)
lowp vec4 twoUpOrDown (lowp sampler2D s, highp vec2 p)
{
    lowp vec4 found = texture2D(s,vfract(p + vec2(0.,h*2.))); //this is color above or below
    return found;
}
    
// get the color in vector direction
lowp vec4 colorAt(highp vec2 direction)
{
    lowp vec4 found = texture2D(texture,vfract(vTexCoord + vec2(w*direction.x,h*direction.y)));
    return found;
}

//-----------------
    
void main()
{
    //Sample the data at the interpolated coordinate
    lowp vec4 col;
    col = texture2D( texture, vTexCoord );

    if (pixelReading == true){
    
        if (col.g > 0.5) {
            //leave color as is
        } else {
            lowp vec4 leftColor = colorAt(vec2(w,0.));
            lowp vec4 leftColor2 = twoToLeft(texture, vTexCoord);
            if (leftColor.g > 0.5 || leftColor2.g > 0.5) {
                col.r = 0.;
                col.g = 1.;
                col.b = 0.;
            } else if (toUpOrDown(texture, vTexCoord).g > 0.5 || twoUpOrDown(texture, vTexCoord).g > 0.5) {
                col.r = 0.;
                col.g = 1.;
                col.b = 0.;
            } else if (colorAt(vec2(2.*w,-2.*h)).g > 0.5 ) {
                col.r = 0.;
                col.g = 1.;
                col.b = 0.;
            } else {
                col.r = 0.;
                col.g = 0.;
                col.b = 1.;
            }
    
        }
    
    } else {
    
        if (col.g == 1.) {
            // alive, fade from yellow to blue
            col.b = 0.;
            col = col.r*col + (1.-col.r)*alive;
        } else {
            // dead
            if (col.r > 0.) {
                // but recently so
                col.g = 0.;
                col.b = 0.;
                col.rgb = col.rgb + (1. - col.r)*colour.rgb;
            } else {
                col.rgb = colour.rgb;
            }
        }
    
    }
    
    //Set the output color
    gl_FragColor = col;
}
]]
end