In the last post we look at how very simple rules for modelling how artificial life evolves can create some really interesting patterns and behaviours.
This time we'll take the idea and take it in a different direction - we'll turn the evolving landscape into a texture that maps onto the surface of a 3-d object.
3-D Objects in P5JS
Normally we use p5js to create 2-d shapes like lines, rectangles and circles, painted onto a canvas. We can also use p5js to create 3-d objects and move them in 3-d space.
To do that p5js uses webgl, a technology designed for efficiently rendering 3-d objects using a GPU in your computer if there is one. Lots of computer games use the same approach, taking advantage of the specially designed chips inside your computer or laptop.
In the past, using your GPU to accelerate graphics was really complex and hard - but today it has been made much easier. P5js also makes it easy.
Look at the following really simple and short code:
function setup() { createCanvas(800, 600, WEBGL); background('white'); } function draw() { box(200, 200, 200); }
In the setup() function the first thing we notice is that the canvas is created with an extra WEBGL parameter - that tells p5js to create a special canvas the can render webgl objects. We then create a white background - easy.
In the draw function we have only one command and the name box() gives us a clue as to what it does - it creates a 3-dimensional box. Our box(200, 200, 200) creates a box with width, height and depth 200, a cube in other words.
Let's see the results:
That doesn't look like a 3-d cube, it looks like a flat square. The reason is that we're looking at the cube straight on. To see more than one face we need to rotate the cube. Let's do that:
function draw() { rotateX(1.0); box(200, 200, 200); }
All we've done is add a command to rotate the box by 1 radian (about 57 degrees). Actually that's not quite right. That rotateX() command rotates the entire world around the x-axis. That means we should see the cube roll forwards or backwards.
Let's see the results:
That's much more like a 3-dimensional object. Let's see what happens if we combine a rotate about the x-axis as well as a rotation about the y-axis.
function draw() { rotateX(1.0); rotateY(1.0); box(200, 200, 200); }
And the result:
Nice! Now let's animate the cube by drawing frames with the cube at different rotations. We looked at animation previously.
Have a look at the code which has only grown a small bit:
// counter used to determine how much we rotate the 3-d world var counter = 0; function setup() { createCanvas(800, 600, WEBGL); } function draw() { // blank canvas for animation background('white'); // rotate world rotateX(counter); rotateY(counter); // draw box box(200, 200, 200); // increase counter counter += 0.01; }
We're blanking the canvas before we draw a frame. We're using the same rotations about the x and y axes as before but this time the angle is inside the variable counter, which we set to zero at the start of the code. We draw the box, and finally we increase counter by a small amount. That means the next frame will have the box drawn at a slightly different angle.
Let's see if this works:
That's looking good.
With very simple code we've rendered an animated 3-d object using p5js, and even taken advantage of the special hardware inside our computers to help accelerate the rendering.
We want to apply our Game of Life patterns onto the surface of our 3-d object. To do that we need to work out how to turn an array of numbers into a texture. There isn't a direct way to do that - that I know of - we need to first turn the numbers into an "image" before it can be applied as a texture.
Let's do this with a simpler set of numbers first. Look at the following code, most of which should be familiar.
var img; var counter = 0; function setup() { // create a WEBGL canvas createCanvas(800, 600, WEBGL); // create empty image and fill pixel by pixel img = createImage(400, 100); img.loadPixels(); for(var x = 0; x < img.width; x++) { for(var y = 0; y < img.height; y++) { // use noise() values 0-255 to create greyscale pixels img.set(x, y, noise(x/40,y/10)*255); } } img.updatePixels(); } function draw() { background('white'); // animate a 3-d box push(); rotateX(counter); rotateY(counter); texture(img); box(200, 200, 200); pop(); // increase counter which determines rotation of box counter += 0.01; }
The second half of the code is the same as before - we rotate the world and draw a cube, and increase the counter which determines the rotation of the world at the next frame. There is a new instruction texture(img) which we can correctly guess as applying a texture to the surface of the 3-d object. We can even guess that img is the image that's applied as the texture.
The new code near the top creates that image img. Let's look at it more closely. We first create an empty image of size 400 by 100 using createImage(400, 100). To access the actual pixels, we need to use loadPixels() to expose them. We then use a loop with in a loop to visit every pixel and set it's value, which we do using the noise() function, which creates a less random randomness. Finally we need to use updatePixels() to apply those changes and commit them to the image.
We've done all the hard work .. all we need to do is apply these ideas to the array that holds the population of the creatures that we modelled using Conway's Game of Life rules.
While we're at it, let's change the shape from a box() to a torus().
To make sure the texture changes as the Game of Life patterns change, we need to update the texture image every frame.
The code is a little longer so we'll just point to it. It doesn't do anything more than what we've already discussed.
One of the grandest ideas for an algorithm is to simulate life - to create artificial life itself!
That would be cool - but we don't really know how life works to a very deep level. Instead, we can work with a simplified model to see where that takes us.
In 1970 a British mathematician, John Conway, came up with such a simple model of life, which is now called Conway's Game of Life.
Conway's Artificial Life
The living beings in Conway's game are very simple creatures that live according to really simple rules. Let's imagine them some character:
These creatures actually live in a landscape made of little cells. The little cells in which they sit are square, and so the landscape looks like grid of squares.
These little creatures are quite social and like company, just like most living things. But the company has to be just right - too crowded and they die, too lonely and they die too.
Conway designed four simple clear rules that decide whether these creatures love or die.
Rule 1 - Loneliness
If a creature has less than 2 neighbours, it dies of loneliness.
That is, if a creature has 0 or 1 neighbours, it will die and not appear in the next generation of the population.
Rule 2 - Just Right
If a creature has 2 or 3 neighbours, it is happy and continues to live into the next generation.
Rule 3 - Overcrowding
If a creature has more than 3 neighbours, then it is overcrowded and sadly it dies.
That means, if a creature has 4, 5, 6, 7 or 8 neighbours, it dies of overcrowding.
Rule 4 - Birth
If there is an empty cell surrounded by exactly 3 neighbours, then a new creature is born into this empty cell.
This is a different kind of rule, because it works on empty cells, not already occupied cells.
Simple Worked Example 1
Let's look at some examples to make sure we understand how populations of these creatures evolve.
Have a look at the following landscape of creatures. There are only 2 creatures sat next to each other.
What does the next generation of this population look like? We must apply the rules to find out:
Looking at each of the creatures, we can see they are lonely, Each one has only 1 neighbour. That means they'll die of loneliness, and won't appear in the next generation.
The other rules don't apply, there is no overcrowding, and there aren't enough neighbours to be "just right".
No creatures are born in an empty cell, because no empty cell has exactly 3 neighbours.
So the next generation is empty - there are no surviving creatures.
Simple Worked Example 2
Let's look at a different, but still simple, example. There are now 4 creatures arranged in a square. How does this population evolve?
Let's see how those rules apply to these creatures:
Each one of the creatures has 3 neighbours. That's the "just right" amount, so the creatures will carry on living into the next generation. No creatures will die.
There is no overcrowding, and there is no loneliness.
No new creatures will be born as there are no empty cells with exactly 3 filled neighbours.
This means that the next generation will be exactly like this generation - there will be no change!
Simple Worked Example 3
Let's look at one more example. This landscape is similar to the previous one, but has one creature missing.
What happens to these creatures? Lets apply each of the rules:
Each of the creatures has 3 neighbours. That means it is happy and will live into the next generation.
There are no lonely or overcrowded creatures, so no creatures will die.
This time, a new creature will be born in that cell that had a creature in the previous example but doesn't here. This is because the empty cell has exactly 3 neighbours.
So the population will have grown by one creature for the next generation.
In these examples, we've seen creatures die, creatures live and creatures be born.
Let's Code It
Now that we've understood the rules let's write some code to actually simulate a population as it evolves. For now we'll keep the code simple so that we can stay focussed on the key ideas.
Here are the main design decisions:
The grid of cells is rectangular. That suggests a 2-dimensional array for storing the creatures. In many programming languages - eg Python - that's very easy to do. However, with Javascript, which underpins p5js, the language itself doesn't provide 2-dimensional arrays, only 1-dimensional lists. This means we just need to "unwrap" our 2-d array to a 1-d list, with each row following the next. The calculation for translating a position (i,j) in the 2d array is i + (rowsize * j) in the list. The following picture makes this clear.
We create array (albeit unwrapped as a 1-d list) with 50 rows and 50 columns, and give it an initial random population. That means we visit each cell and use randomness to either put a creature in it, or leave it empty. We use a value of 1 to mean that there is a creature, and 0 for an empty cell. Actually, we'll be a bit biased towards emptiness, so we'll only fill a cell with a probability of 30%. In the code, we can do this using if (random() < 0.3) which is only true 30% of the time.
We use the fact that the draw() function is called repeatedly to draw the grid and, then work out the next generation. This should give us an animation if the contents of the grid change. We saw how a repeatedly called draw() can be used to create animation in a previous post. We actually slow down the rate at which draw() is called to 5 frames per second using frameRate(5). The default frame rate is much higher, with is great for smooth animation, but not for watching how our creature populations evolve.
We use the familiar loop within a loop to count rows j and columns i, as we visit each cell at (i,j) of the grid and consider how the rules apply. To work out the number of neighbours, we need to also visit the neighbours of this cell at (i,j). We use yet another loop within a loop to count the rows and columns immediately around this cell.
We apply the rules and decide whether the creature lives on into the next generation. If it does, we don't write it into the current array grid_array, because that would make the next neighbour counts wrong. We need to put it into a temporary new array grid_array_2 Once all the cells have been visited, and a new temporary array completed, we point the name name grid_array at the new data, and leave javascript to free up the memory used for the now out of date data. If you're interested, this discussion on stackexchange confirms how/why this works.
This works well. Here's an example of a 50x50 grid:
Cool!
We can see that at the initial random population starts to settle down into shapes that are static, and there are some oscillator in there too. Watching closely I can even see some nice cross patterns emerging before being obliterated by neighbouring creatures!
Before we move on, did you notice how there isn't much going on at the edges of that grid? The cells all around the edge become vacant very quickly. The reason is that we cheated a little by not considering the creatures in those edge cells to make the logic for counting neighbours easier. How do we count the neighbours for a cell on the edge of the grid? We could just count the ones that are on the grid - which makes perfect sense - but the code to make this a special case is a bit messy. So we've kept the code simple and just ignored the edge cells. For me at least, the resulting image is rather pleasing as the empty edges create a kind of frame for the patterns.
Another approach is to "wrap the grid around the edges". That is, if we walk off the edge of the grid, instead of falling off, we just re-appear on the other side. This means that when we count the neighbours for a cell on the edge of the grid, we include the cells that are on the other side of the grid. The following picture makes this clear.
We can keep the code simple, avoiding special cases for the edges, by using the javascript remainder function. The following code illustrates how coordinates (i,j) are modified to wrap around the grid:
var wrapped_i = (i + x + number_of_rows) % number_of_rows; var wrapped_j = (j + y + number_of_columns) % number_of_columns;
The fancy term for this wrapping around is making the grid periodic. Here's the result:
If we watch carefully we can see populations on the edge move onto the opposite edge, and we also don't see the empty edges that we had before.
We've explored the very simple rules that Conway designed to model artificial life. It is striking that the rules are so simple - and yet the resulting patterns and their behaviours are quite interesting.
In Part 2 we'll look at extending this basic idea of simple artificial life.
Processing is great for drawing shapes - lines, dots, circles, rectangles - and, when combined with colour, we can build up quite sophisticated works of art using just these tools.
Many artists want to explore movement and animation, not just static objects that don't move once they've been placed.
This blog post will introduce the key ideas we need to understand about how to create motion with Processing.
Children's Flip Book
As a child, you may remember making flip books to create animation. Each page of the flip book had a drawing which was similar to the one on the pages before and after it. Similar - but not exactly the same.
When the pages were flipped so we saw each page for a very short time, the small differences in the drawings gave the appearance of smooth movement.
Here's a video reminding us how flip books are made.
The main idea that makes flip books work is simple - and luckily, that simple idea is the same one that makes digital animation work too.
Digital Flip Book
Before we jump into writing any code, let's think a bit more about that idea of a flip book, and how it might work digitally.
Let's start with three pages of our flip book.
We can see that the first page of the flip book has a red ball near the top. The next page has the red ball moved slightly down. The third page has the red ball further down.
If we flicked the book we'd see the ball appear to move from the top towards the bottom.
In digital animation, we have the same idea of a page, but we call it a frame. That's why the picture above has the three pages labelled as frame 1, frame 2 and frame 3. The word frame is familiar to anyone who did film photography or recorded movies using film. Each frame was a snapshot taken by the film.
What's the equivalent of a frame in Processing? Where do we paint our red ball?
The answer may be surprising - it's the canvas we've been using all along to paint our non-moving shapes. We can think of each of the frames in the picture above as a Processing canvas.
If the frames are just a canvas, we already know how to set the canvas size, set the background colour, and draw shapes on it. In the picture above, the frame's background is set to a light yellow with the familiar background() instruction, and the red ball is drawn with the familiar ellipse() instruction.
This last point is important. Anything and everything we could paint on a canvas - like circles, lines, and even more complex patterns and textures - we can use on our animation frames.
Making Movement
Ok - we now know that an animation frame is just a canvas, and we already know how to draw shapes on a canvas.
How do we take the next step to make movement?
Looking back at that last picture, we remember that we need to draw several frames, each one slightly different to the one before it.
In our example, the position of the red ball changes between each frame. We already know how to draw a red circle at different locations. Here's we would be drawing the red ball with a vertical position that grew larger with each frame - remembering that in Processing the vertical coordinate starts at the top of canvas and grows downwards.
The next picture shows the vertical y-coordinate of the ball getting larger as the ball moves down the canvas.
So far so good. But we still don't know how we're going to flip the frames, like we flip the pages of a children's flip book.
We could draw the red ball, pause for a very short time, and then blank the canvas and draw the ball again at a slightly different position, .. and repeat this for as long as we needed. That would work.
Luckily, Processing helps us with flipping the pages.
Remember how every Processing sketch has a setup() function and a draw() function?
We used setup() to set up our drawing environment - setting things up like the size of the canvas and the background colour. And we kept draw() separate for the instructions that actually did the drawing - for things like circles and rectangles.
What we might not have realised is that setup() is called once, at the beginning .. and draw() is called repeatedly, again and again, forever.
Why didn't we notice that draw() is called again and again? If our code simply draws a circle in the middle of the canvas, for example, and draw() repeatedly draws it again and again, we won't see any difference.
So - if Processing is calling draw() repeatedly, we can take advantage of that and consider each call to draw() as an opportunity to draw a single frame. The next call to draw() will be our opportunity to draw the next frame, and so on...
Processing will take care of pausing and calling draw() repeatedly. All we have to worry about is the more creative job of deciding what to animate.
First Animaion Code
We've learned how Processing enables us to animate objects - by repeatedly calling draw() which allows us to draw each frame of an animation.
Let's try to write some simple code to draw a little red ball, and move it down the canvas, just like the drawings above did.
Have a look at the following simple code:
function setup() { createCanvas(800, 600); background('white'); noStroke(); fill('red'); } function draw() { ellipse(400, 100, 50); }
The setup() function is simple - we're creating a canvas of width 800 and height 600, and giving it a background colour of white. We also set outline stroke to be invisible and fill colour to be red.
In the draw() function we have a very simple ellipse() instruction to draw a circle with diameter 50, centred at (400,100). The result shouldn't surprise it at all:
We know that ball is being redrawn again and again at the exact same location, because we now know the draw() function is being called again and again.
Now let's introduce some movement. At the first frame the ball will be drawn at (400, 100). At the next frame we want the ball to be drawn slightly further down the canvas, perhaps at (400, 101). That's a very tiny difference, but it is a difference. You may remember from the handmade flip books that if the difference between pages was big, then the animation wasn't smooth.
How we do draw the ball at (400, 101) at the second frame? In fact, how do we draw the ball at (400,102) at the third frame .. and at (400, 103) at the fourth frame, .. and so on?
If we think about it, we need to keep track of the ball's position, so that we can draw it at the next position during the next frame. If we don't keep track, we can't know what the current position is, and that means we can't decide what the new position of the ball should be.
This means we need a variable to keep track of the ball's vertical position. Have a look at the following code:
// variable to keep track of ball's vertical position var y = 100; function setup() { createCanvas(800, 600); background('white'); noStroke(); fill('red'); } function draw() { // draw ball ellipse(400, y, 50); //update vertical position y = y + 1; }
At the top of the code, we've created a new variable called y, and given it an initial value of 100. Inside the draw() function we've changed the ellipse() instruction to use y as the vertical position of the ball. When draw() is first called, the ball will be drawn at (400,100) just like before, because the value of yis100.
After drawing that ball, we increase the value of y by 1, using y = y + 1. That leaves y with a value of 101.
The next time draw() is called, the ball is drawn at (400,101) because y was left at 101. This is the second frame of our animation, and the position of the ball has been shifted down the canvas a tiny bit - just as we wanted! After the ball is drawn, y is creased to 102.
The next time draw() is called, the ball is drawn at ... you guessed it, (400,102). And y is increased to 103.
You can see how the ball is being drawn further and further down the canvas for each frame. Let's see the result:
Hmmm. That's not quite what we expected. What happened?
We can see the red circle being drawn further and further down the canvas. But at each frame, the canvas isn't being blanked. Blanking a canvas is just like starting a new drawing on a new blank page of our paper flip book.
Let's blank the canvas at the beginning of the draw() function, to start a fresh canvas for each frame.
// variable to keep track of ball's vertical position var y = 100; function setup() { createCanvas(800, 600); noStroke(); fill('red'); } function draw() { // blank canvas for each frame background('white'); // draw ball ellipse(400, y, 50); //update vertical position y = y + 1; // border stroke(0); noFill(); rect(0,0, width-1, height-1);noStroke(); fill('red'); }
Let's see the results now:
We now have animation!
Although this is a simple example, the simplicity makes clear the key points:
draw() is called repeatedly by Processing - this allows us to draw frames of an animation
by changing the image slightly between each frame, we achieve the effect of a smooth animation
variables can be very helpful in keeping track of where were are in the animation
we mustn't forget to blank the canvas before drawing a frame
If we were to improve that simple example, we'd probably fix the fact that the variable y will keep getting bigger and bigger. The ball will move off the bottom of the canvas. Even after that, y will keep getting bigger .. until the code crashes. We could easily write code to check that y hadn't reached an upper limit before increasing it every frame.
//update vertical position if (y <500) { y = y + 1; }
This time the ball moves down the canvas from y=100 until it reaches y=500 and then stays there because y is no longer increased every frame:
The p5js sketch for the code we've developed so far is online at:
How do we control the speed of objects as they move? That's a very good question.
In the simple example above, the ball moved slowly but surely down the canvas. With every frame, the y-coordinate increased by 1. So after 100 frames, the y-coordinate would have increased by 100.
We can start to see how we might alter the speed of the ball. If after 100 frames, we want the y-coordinate to have increased by 500, we would have had to increase the y-coordinate by 5 every frame.
That makes intuitive sense. A faster ball moves further than a slow ball in any given period of time.
Let's create a sketch with two balls, one whose y-coordinate increases by 1 every frame just like before, and the other whose y-coordinate increases by 5 every frame.
// variable to keep track of ball's vertical position var y1 = 100; var y2 = 100; function setup() { createCanvas(800, 600); noStroke(); fill('red'); } function draw() { // blank canvas for each frame background('white'); // draw ball ellipse(300, y1, 50); ellipse(500, y2, 50); //update vertical position if (y1 <500) { y1 = y1 + 1; } if (y2 <500) { y2 = y2 + 5; } }
This time we create two variables, y1 and y2, one for each ball. You can see that y1 is increased by 1 every frame, and y2 is increased by 5 every frame.
We can see clearly that second ball moves faster.
The p5js sketch illustrating how we can control the speed of animated objects is online at:
What we've covered so far are the key ideas behind creating digital animation. Next we'll just have a bit of fun!
More Fun
Let's use a mathematical function to control the position of a ball, based on a parameter t which we start at 0 and increase by 1 every frame:
x = 200 * sin(2 * t / 100)
y = 200 * cos(3 * t / 100)
Here's some simple code that does this:
// variable to keep track of "time-like" parameter var t = 0; function setup() { createCanvas(800, 600); noStroke(); } function draw() { // blank canvas for each frame background('white'); // calculate coordinates of ball var x = 200 * cos(2 * t/100); var y = 200 * sin(3 * t/100); // draw ball fill(255, 0, 0); ellipse(400+x, 300-y, 20); // update parameter t = t + 1; }
Notice that the multipliers inside the cos() and sin() are different. If they were the same, the ball would trace out a circle of radius 200 - try it. The following shows the path traced out as a result of these different multipliers:
That's an interesting looping movement. Let's make it more interesting by having several balls, each one trailing the next, and coloured with a lighter shade of red to emphasise the sequence.
How cool is that!
A mesmerising animation made of simple circles, following an interesting path determined by simple mathematical functions.
We won't dig into how this works in detail as this introductory blog post, but the idea is the same as the single ball. All we've done is used a loop to create 10 balls, and the parameter t is adjusted slightly based on the ball - in effect moving it along the path followed by all the balls. The translucency of the red fill colour is also linked to the ball number.