Conway’s Game Of Life in Pixel Bender

Despite Pixel Bender being around for quite a while now, I hadn’t used it at all. Time to catch up! Pixel Bender is Adobe’s software to create pixel shaders, small programs which can be compiled into ultrafast image processing units. They can be used in several Adobe products: Photoshop, After Effects and … Flash!

The nice thing about pixel shaders is that they usually run on your GPU, which excels in mathematical operations on images, but unfortunately Pixel Bender’s exported shaderfiles don’t make use of that when they’re being run in the Flash Player. Boohoo. Still, they are way faster than doing the same stuff in Actionscript, and two advantages you do get when using them in Flash: they run as a separate thread (meaning you can for example have a responsive UI while doing extensive calculations on a lot of data at the same time) ├índ they will use multiple cores if you happen to have those in your machine.

So, more than enough reason to dive into the subject, and what better way to start than by creating good old Conway’s Game of Life. For the uninformed: Game of Life is a set of 4 rules that define whether a position on a board should become either “alive” (in this case: white) or “dead” (black) by looking at its 8 neighbours (head over to Wikipedia for the the exact rules). Apart from the interesting results that can evolve, that’s all there’s to it: pixels either become black or white, and the way that’s decided makes it perfect to put in a pixel shader.

And so I did. The result can be seen below, play around with it and/or read the notes underneath if you want some more details. Note: you can draw in the image when the shaderjobs are running, but it’s something I implemented very quickly – and poorly. One advice when drawing: set the FPS as high as possible, and don’t move your mouse too fast. (You will need FlashPlayer 10 to run the application).

[SWF]http://www.petervandernoord.nl/swf/pixelbender-life/main.swf, 640, 640[/SWF]

  • I used the compiled shader in a ShaderJob, which lets you run it as a separate task while you wait for it to complete. I supply it with BitmapData objects at both the input and the output. When the job is done, I use BitmapData’s clone-fucntion to copy the result into the data that’s being used as the input and move on to the next job.
  • Initially, I started a new job immediately after the previous one was done, but since I wanted to control the framerate by the SWF’s framerate-property, new jobs are now started on EnterFrame events. In case the current job isn’t done yet when entering a new frame, the program does nothing and will check again on the next frame. If this happens, you will see the words “frame skipped” blink (you should lower the FPS when you see that, because the shader can’t keep up).
  • The FPS you can set is not the actual frames per second but a request – the player will try to match that number. The left number is the actual FPS, the right one is the number you can adjust. So “60/90″ means you are currently getting 60 frames per second, while you requested 90.
  • The image used is 640 by 600 pixels, so in every job 384.000 pixels are being processed with each of them sampling 9 pixels from the input.

And last but not least: the Pixel Bender code:

[sourcecode language="csharp"]

kernel NewFilter
< namespace : "pvdn";
vendor : "Peter van der Noord";
version : 1;
description : "Conway's Game of Life";
>
{
input image4 src;
output pixel4 dest;

void evaluatePixel()
{
pixel4 color = sampleNearest(src,outCoord());

/*

1. Any live cell with fewer than two live neighbours dies, as if caused by under-population.
2. Any live cell with more than three live neighbours dies, as if by overcrowding.
3. Any live cell with two or three live neighbours lives on to the next generation.
4. Any dead cell with exactly three live neighbours becomes a live cell, as if by reproduction.

*/

pixel4 tl = sampleNearest(src, outCoord() + float2(-1.0,-1.0));
pixel4 t = sampleNearest(src, outCoord() + float2(0.0,-1.0));
pixel4 tr = sampleNearest(src, outCoord() + float2(1.0,-1.0));
pixel4 r = sampleNearest(src, outCoord() + float2(1.0,0.0));
pixel4 br = sampleNearest(src, outCoord() + float2(1.0,1.0));
pixel4 b = sampleNearest(src, outCoord() + float2(0.0,1.0));
pixel4 bl = sampleNearest(src, outCoord() + float2(-1.0,1.0));
pixel4 l = sampleNearest(src, outCoord() + float2(-1.0,0.0));
pixel4 me = sampleNearest(src, outCoord());

pixel4 total = tl + t + tr + r + br + b + bl + l;

if(me.r == 1.0)
{
// alive
if(total.r == 2.0 || total.r == 3.0)
{
dest = pixel4(1.0, 1.0, 1.0, 1.0);

}
else
{
dest = pixel4(0.0, 0.0, 0.0, 1.0);
}
}
else
{
// dead
if(total.r == 3.0)
{
dest = pixel4(1.0, 1.0, 1.0, 1.0);
}
else
{
dest = pixel4(0.0, 0.0, 0.0, 1.0);
}
}
}
}
[/sourcecode]

4 thoughts on “Conway’s Game Of Life in Pixel Bender

  1. Ryan Stemen

    Very cool! I’ve implemented my own version of Conway’s game of life in HLSL before with great results, but I’m struggling with the AS3 version. Perhaps I am missing something obvious but how are you copying the output of the pixel shader back as the input?

    Reply
    1. Peter

      First of all, you have to use PixelBender as a ShaderJob. This way, you can just run it as a separate task with some data (either ByteArray, BitmapData or a Vector, if i recall correctly).

      When you’ve loaded the shader, you create a new ShaderJob, supply an input, add a listener to it and start it.


      // set the input bitmapdata for the shader
      ShaderInput(this._shader.data[_SHADER_INPUT_NAME]).input = this._inputBitmapData;
      // use the shader to create the shaderjob with the correct bitmapdata to write in
      this._shaderJob = new ShaderJob(this._shader, this._outputBitmapData);
      this._shaderJob.addEventListener(Event.COMPLETE, handleShaderJobComplete);
      // start it
      this._shaderJob.start(false);

      The _oututBitmapData that’s supplied as parameter is being used by the ShaderJob to write to, and will be filled when the ShaderJob is done. So if you just clone that output to the input again in the eventhandler…

      this._inputBitmapData = this._outputBitmapData.clone();

      … you can just run the top code again, since the input has been updated. I don’t know if this is the fastest way (the clone function can be quite expensive), but this is how i did it.

      Reply
  2. Ryan Stemen

    Thanks for the reply, Peter. I actually found a different way of feeding the output of a shader back into the input. I did it with the BitmapData.applyFilter() function. I simply use my currently displayed BitmapData object as the source BitmapData for the filter to be applied. The applyFilter is nice because I can run it on data that is currently being displayed in a bitmap on stage–no need for the expensive clone() function call.

    Here is the code for my class:

    package
    {
    import flash.display.*;
    import flash.events.*;
    import flash.filters.ShaderFilter;
    import flash.geom.*;
    import flash.utils.ByteArray;

    /**
    * ConwaysGameOfLifeTest class
    * @author Forest Giant, Inc. - Ryan Stemen
    */
    public class ConwaysGameOfLifeTest extends Sprite
    {
    [Embed("CGOL6.pbj", mimeType="application/octet-stream")]
    private var CGOLFilter : Class;

    private static const HEIGHT : int = 480;
    private static const WIDTH : int = 320;

    private var bmpData : BitmapData;

    private var bmp1 : Bitmap;

    private var srcRect : Rectangle = new Rectangle(0,0, WIDTH, HEIGHT);
    private var loc : Point = new Point();

    private var conwayFilter : ShaderFilter;

    public function ConwaysGameOfLifeTest()
    {
    super();

    this.stage.nativeWindow.height = HEIGHT;
    this.stage.nativeWindow.width = WIDTH;
    this.stage.nativeWindow.visible = true;
    this.stage.scaleMode = StageScaleMode.NO_SCALE;
    this.stage.frameRate = 30;
    this.stage.align = StageAlign.TOP_LEFT;

    //declare a new shader filter from my embedded PBJ
    conwayFilter = new ShaderFilter(new Shader(new CGOLFilter() as ByteArray));

    //declare the bitmap data object to the same size as the stage.
    bmpData = new BitmapData(WIDTH, HEIGHT);
    initData();

    //set the bitmap's data
    bmp1 = new Bitmap(bmpData);

    this.addChild(bmp1);
    this.stage.addEventListener(Event.ENTER_FRAME, onEnterFrame);
    this.stage.addEventListener(MouseEvent.CLICK, initData);

    }

    private function initData(event:MouseEvent = null):void{
    for( var x : int = 0; x < bmpData.width; x++)
    for( var y : int = 0; y 0.75) ? 0xFFFFFFFF : 0xFF000000;
    bmpData.setPixel(x, y, value);
    }
    }

    private function onEnterFrame(event:Event):void{
    //keep re-applying the filter to the current data (i.e. current frame)
    bmpData.applyFilter(bmpData, srcRect, loc, conwayFilter);
    }

    } //end ConwaysGameOfLifeTest class
    } //end package

    Also, I think you might be able to improve the efficiency of your shader with this small optimization:

    if( total.r >= 4.0 || total.r = 3.0 && total.r < 4.0){
    me.argb = float4(1.0, 1.0, 1.0, 1.0);
    }
    dest = me;

    This works because the conditionals are only checking for cases where the current pixel/cell needs to change state, otherwise the current state is automatically re-written to the output.

    Thanks for the great post!

    Reply
    1. Ryan Stemen

      Oops, messed up that shader code when I copied it in. Here is the real version:


      if( total.r >= 4.0 || total.r < 2.0){
      me.argb = float4(1.0, 0.0, 0.0, 0.0);
      } else if(total.r = 3.0 && total.r < 4.0){
      me.argb = float4(1.0, 1.0, 1.0, 1.0);
      }
      dest = me;

      Reply

Leave a Reply

Your email address will not be published. Required fields are marked *

You may use these HTML tags and attributes: <a href="" title=""> <abbr title=""> <acronym title=""> <b> <blockquote cite=""> <cite> <code> <del datetime=""> <em> <i> <q cite=""> <strike> <strong>