 |
       |
| |
Part III
|
|
Simple system - particle system |
|
L-system and turtle graphics |
|
Complex system - cellular automata |
|
| |
| Sol LeWitt died |

Image from nytimes.com
Sol LeWitt was one of the great conceptual artists and had influenced many generative art practices in contemporary arts.
Here is an article from the NY Times.

Image from http://www.artline.com/
|
| Reference on Cellular Automata |
Stephen Wolfram
|
| Understanding pixel operation in Processing |
In the previous 2 lessons, we have looked into simple system of particles and recursive definition of a graphical language. In this class, we start working on a simple implementation of a complex system.
Before that, we use image processing techniques to display the visual output.
The display canvas in Processing is stored in an one dimensional array - pixels. If the dimension for the canvas is width by height, the pixels array has width x height number of entries. Each of them is a color value.
Try out the following test to be familiar with the pixels array.
void setup() {
size(200,200);
background(255);
loadPixels();
noLoop();
}
void draw() {
for (int r=0;r<height;r++) {
for (int c=0;c<width;c++) {
pixels[r*width+c] = color(0);
}
}
updatePixels();
} |
loadPixels()
- store the current canvas image into the pixels array.
r*width+c
- for a pixel located at row - r and column - c, its position in the pixels array is r*width+c.
|
0 |
1 |
2 |
3 |
0 |
0 |
1 |
2 |
3 |
1 |
4 |
5 |
6 |
7 |
2 |
8 |
9 |
10 |
11 |
3 |
12 |
13 |
14 |
15 |
updatePixels()
- refresh the canvas on screen with the pixel information from the pixels array.
|
| Row by row animation with pixels |
If we remove the noLoop() and use a global variable row to control in each frame, the row of pixels we would like to paint black, it can be a simple animation of black paint.
int row;
void setup() {
size(200,200);
background(255);
loadPixels();
row = 0;
}
void draw() {
for (int c=0;c<width;c++) {
pixels[row*width+c] = color(0);
}
updatePixels();
row++;
if (row==height) {
row = 0;
background(255);
loadPixels();
}
} |
|
| 1D Cellular Automata |
The number in a linear array (1D) will change in time, i.e. a dynamic system. In this simple example, it has only two values 0 and 1. For each pixel (cell), its next value will depend on its existing value and its neighboring cells' value. In this case, it depends on its left and right neighbours.
And we can derive rules to describe the changes. For a cell with two neighbours, it can have 8 eight rules.
Rule 1
Rule 2
Rule 3
Rule 4
Rule 5
Rule 6
Rule 7
Rule 8
| Rule |
Left |
Old |
Right |
New |
| 1 |
0 |
0 |
0 |
|
| 2 |
0 |
0 |
1 |
|
| 3 |
0 |
1 |
0 |
|
| 4 |
0 |
1 |
1 |
|
| 5 |
1 |
0 |
0 |
|
| 6 |
1 |
0 |
1 |
|
| 7 |
1 |
1 |
0 |
|
| 8 |
1 |
1 |
1 |
|
We can use the wrap around technique to take care of the boundary cells. The rules are apparently simple but the results generated can be unexpected.
Before implementing the rules, we write a simple program to copy previous row of pixels onto the next row of pixels. This one has a single black pixel in the middle of the first row.
int row;
void setup() {
size(200,200);
background(255);
loadPixels();
row = 0;
initFirst();
}
void draw() {
if (row<height-1) {
copyRow(row);
row++;
}
}
void initFirst() {
pixels[0*width+width/2] = color(0);
updatePixels();
}
void copyRow(int r) {
int t1 = r*width;
int t2 = (r+1)*width;
for (int c=0;c<width;c++) {
color col = pixels[t1+c];
pixels[t2+c] = col;
}
updatePixels();
} |
And this one has the first row filled with random pixels.
int row;
void setup() {
size(200,200);
background(255);
loadPixels();
row = 0;
initFirst();
}
void draw() {
if (row<height-1) {
copyRow(row);
row++;
}
}
void initFirst() {
for (int c=0;c<width;c++) {
color col;
if (random(0,1)<0.5) {
col = color(0);
} else {
col = color(255);
}
pixels[c] = col;
}
updatePixels();
}
void copyRow(int _r) {
int r1 = _r*width;
int r2 = (_r+1)*width;
for (int c=0;c<width;c++) {
color col = pixels[r1+c];
pixels[r2+c] = col;
}
updatePixels();
} |
|
| Single pixel as initial condition |
We are going to use a two dimensional array to implement the rules.
| Rule |
Left |
Old |
Right |
New |
| 1 |
0 |
0 |
0 |
1 |
| 2 |
0 |
0 |
1 |
1 |
| 3 |
0 |
1 |
0 |
0 |
| 4 |
0 |
1 |
1 |
0 |
| 5 |
1 |
0 |
0 |
1 |
| 6 |
1 |
0 |
1 |
1 |
| 7 |
1 |
1 |
0 |
0 |
| 8 |
1 |
1 |
1 |
1 |
int [][] rules = { { 0,0,0,1 } , { 0,0,1,1 } , { 0,1,0,0 } , { 0,1,1,0 } , { 1,0,0,1 } , { 1,0,1,1 } , { 1,1,0,0 } , { 1,1,1,1 } }; |
Each entry in the 1st dimension of the array rules is a pattern like this one,
This pattern states that when a pixel with value 0 and two neighbours are both 0. It will change to a value 1 in the next step. The row in the rules array is
{0,0,0,1}
Each of the 8 rules are an entry in the rules array.
Here are some of the examples generated by one single pixel in the middle of the 1st row. Each row of pixels is one generation.
We use the following function findCol() to determine the colour of the pixel in the next row. It accepts a parameter with an one dimensional array of 3 elements.
- _c[0] is the left neighbour
- _c[1] is the current pixel
- _c[2] is the right neighbour
color findCol(int [] _c) { int out = 0; for (int i=0;i<rules.length;i++) { if (_c[0]==rules[i][0] && _c[1]==rules[i][1] && _c[2]==rules[i][2]) { out = rules[i][3]; break; } } if (out==0) { return color(255); } else { return color(0); } } |
Write your copyRow() function to make use of the findCol() to fill up the pixels with appropriate colour. Try out different rules combinations.
void copyRow(int _r) { int r1 = _r*width; int r2 = (_r+1)*width; for (int c=0;c<width;c++) { int [] col = new int[3];
int left_idx = ?
int mid_idx = c;
int right_idx = ?
col[0] = convertCol(?);
col[1] = convertCol(?);
col[2] = convertCol(?);
pixels[r2+mid_idx] = findCol(col); } updatePixels(); } |
You may also need to simplify the work by using another function convertCol(). Given a color variable, it changes back to either 0 or 1 according to whether it is black or otherwise.
int convertCol(color _c) { if (_c==color(0)) { return 0; } else { return 1; } } |
|
| Random pixels as initial condition |
Note that the rules are relatively simple but the pattern created can be very complex. Some of them look random but none is actually random.
|
| 2D Cellular Automata |
If we extend the previous examples into the 2D world, we can think of a rule as
In an image (2D pixel array), every single one will change in time between 0 to 1. Similar to the 1D case, it depends on its neighbours' values to calculate its next state. Since it has 9 cells including itself, the number of rules will be 2 to the power 9, i.e. 512.
We are not going to implement a general 2D cellular automata but a special case.
|
| The Game of Life |
It is developed by John Conway. The Game of Life is a 2D cellular automata with the following rules to generate the new state.
- Any live cell with fewer than two live neighbours dies.
- Any live cell with more than three live neighbours dies.
- Any live cell with two or three live neighbours lives, unchanged, to the next generation.
- Any dead cell with exactly three live neighbours comes to life.
It does not care which neighbours are live or dead as long as the number of living neighbours meet the above conditions.
We are going to write a CA class to implement this version of 2D cellular automata.
class CA {
int [][][] buffer;
int row, col;
int curr, next;
int w, h;
boolean run;
CA(int _r, int _c) {
row = _r;
col = _c;
buffer = new int[2][_r][_c];
curr = 0;
next = 1;
w = width/col;
h = height/row;
run = true;
init();
}
void init() {
for (int r=0;r<row;r++) {
for (int c=0;c<col;c++) {
buffer[curr][r][c] = int(random(2));
buffer[next][r][c] = buffer[curr][r][c];
}
}
}
void update() {
if (run) {
int temp = curr;
curr = next;
next = temp;
for (int r=0;r<row;r++) {
for (int c=0;c<col;c++) {
buffer[next][r][c] = rules(r,c);
}
}
}
render();
}
void render() {
for (int r=0;r<row;r++) {
for (int c=0;c<col;c++) {
if (buffer[next][r][c]==1) {
fill(255,240,0);
} else {
fill(0);
}
rect(c*w,r*h,w-1,h-1);
}
}
}
int rules(int _r, int _c) {
int res = buffer[curr][_r][_c];
int up = (_r-1+row) % row;
int left = (_c-1+col) % col;
int down = (_r+1) % row;
int right = (_c+1) % col;
int num = 0;
if (buffer[curr][up][left]==1) num++;
if (buffer[curr][up][_c]==1) num++;
if (buffer[curr][up][right]==1) num++;
if (buffer[curr][_r][left]==1) num++;
if (buffer[curr][_r][right]==1) num++;
if (buffer[curr][down][left]==1) num++;
if (buffer[curr][down][_c]==1) num++;
if (buffer[curr][down][right]==1) num++;
if (buffer[curr][_r][_c]==1 && num<2) {
res = 0;
}
else if (buffer[curr][_r][_c]==1 && num>3) {
res = 0;
}
else if (buffer[curr][_r][_c]==1 && (num==2 || num==3)) {
res = 1;
}
else if (buffer[curr][_r][_c]==0 && num==3) {
res = 1;
}
return res;
}
void toggle() {
run = !run;
}
} |
The 2D cellular automata will be initialized by giving it the size of each dimension. It is a double buffer mechanism. For each frame, the current buffer matrix will be updated by the rules and put into the next buffer. And it will be shown on the canvas window. We use the index variables, instead of copying the whole array from one to another, curr and next.
init()
will put a random value 0 or 1 into each cell of the matrix.
update()
will copy the current matrix into the next matrix according to the rules.
render()
will display the next buffer on the screen.
toggle()
will change a boolean variable to indicate if we need to update the CA or not.
rules()
will return a value 0 or 1 according to the game of life rules, given a coordinate of a cell in the matrix.
Try to figure out the main program to use this CA class. If you have done, modify the CA class to display different types of graphics for the value of 0 and 1.
|
| Variations |
Different animated patterns can be created by just replacing the graphics for 0 and 1.
|
| Reference |
Paul Brown
Cellular Automata and Art |