We got some questions about how we made the pumpkin-to-wave animation we posted on Halloween, so we asked Dexter to give us a rundown.

Just like last month, we won’t need any special libraries to make this sketch. All you need is a text editor, a webserver to host the model, and a browser to display the final animation. If this is your first time with p5, make sure to check out this post to get everything set up.

First things first, we need our standard p5 boilerplate. A setup function to write our ‘one time only’ set up code, and a draw function to handle our animation.

function setup() { } function draw() { } 1 2 3 4 5 6 7 function setup ( ) { } function draw ( ) { }

Next we can create our canvas. 500×500 should be plenty, and becasue we are using 3d we need to add the third render mode argument WEBGL .

function setup() { createCanvas(500, 500, WEBGL); } 1 2 3 function setup ( ) { createCanvas ( 500 , 500 , WEBGL ) ; }

Now we need to import a model. We’ll be using this pumpkin by Soumyadeep Sur but feel free to use any .obj file you like.

In p5js, asset loading is done in a special function called preload . The preload function runs before setup and makes sure everything gets loaded in the correct order. Let’s create a variable called pumpkin to store our model, and then use the loadModel function in preload to assign pumpkin to our pumpkin.obj model.

var pumpkin; function preload() { pumpkin = loadModel('pumpkin.obj'); } function setup() { // ... 1 2 3 4 5 6 7 8 var pumpkin ; function preload ( ) { pumpkin = loadModel ( 'pumpkin.obj' ) ; } function setup ( ) { // ...

Now we can use the model function in draw to draw the pumpkin to the canvas. When working with 3d models, it’s common for the scale of a model and the scale of your sketch to not line up very well. We can use the scale and translate functions to fix this—these numbers are only estimates. We can also add some lighting to the scene with the ambientLight function.

function draw() { background(0); ambientLight(200, 200, 255); translate(0, 50, 0); scale(90); model(pumpkin); } 1 2 3 4 5 6 7 function draw ( ) { background ( 0 ) ; ambientLight ( 200 , 200 , 255 ) ; translate ( 0 , 50 , 0 ) ; scale ( 90 ) ; model ( pumpkin ) ; }

Now that we know the model is correctly imported, let’s find an interesting way to display it. We can remove our model , translate , and scale calls for now, but let’s leave the ambientLight .

If we print the pumpkin to the developer console, we can see that it’s an instance of p5.Geometry , and that it has an array of p5.Vector objects called vertices . Essentially, this array holds the positions of all the corners in the model. We can use these vectors to draw the model in any way we want.

Unfortunately, we probably won’t be able to display every vertex in the model and still keep a reasonable frame rate, so lets create a numPoints to keep track of our detail.

var pumpkin, numPoints; function setup() { createCanvas(500, 500, WEBGL); numPoints = 2000; } 1 2 3 4 5 6 var pumpkin , numPoints ; function setup ( ) { createCanvas ( 500 , 500 , WEBGL ) ; numPoints = 2000 ; }

In draw , we loop from 0 from numPoints and use the map function to ensure we always get equally spaced vertices in model.

function draw() { for(var i = 0; i < numPoints; i++) { var index = floor(map(i, 0, numPoints, 0, pumpkin.vertices.length)); var vert = pumpkin.vertices[index]; } } 1 2 3 4 5 6 function draw ( ) { for ( var i = 0 ; i < numPoints ; i ++ ) { var index = floor ( map ( i , 0 , numPoints , 0 , pumpkin . vertices . length ) ) ; var vert = pumpkin . vertices [ index ] ; } }

Now that we have our vertex loop in place, we need to display the vertices somehow. p5 has many 3d drawing functions, but let’s just use box for now. Unlike the 2d drawing functions, box only takes a size argument, meaning we need to use translate to position it in 3d space. We also need to use the push and pop functions to keep our translations from stacking on top of each other. We can also control our model size by multiplying out vert vector by a size vector.

var pumpkin, numPoints, modelSize; function setup() { // ... modelSize = createVector(100, 100); } function draw() { for(var i = 0; i < numPoints; i++) { var index = floor(map(i, 0, numPoints, 0, pumpkin.vertices.length)); var vert = pumpkin.vertices[index]; push(); translate(vert.x * modelSize, vert.y * modelSize, vert.z * modelSize); box(2); pop(); } } 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 var pumpkin , numPoints , modelSize ; function setup ( ) { // ... modelSize = createVector ( 100 , 100 ) ; } function draw ( ) { for ( var i = 0 ; i < numPoints ; i ++ ) { var index = floor ( map ( i , 0 , numPoints , 0 , pumpkin . vertices . length ) ) ; var vert = pumpkin . vertices [ index ] ; push ( ) ; translate ( vert . x * modelSize , vert . y * modelSize , vert . z * modelSize ) ; box ( 2 ) ; pop ( ) ; } }

Let’s make it spin with the rotateY function. First we can declare a rotationPhase variable and set it equal to 0 in the setup function. Then update rotationPhase in the draw function by setting it equal to frameCount * 0.005 .

function draw() { rotateY(rotationPhase) for(var i = 0; i < numPoints; i++) { var index = floor(map(i, 0, numPoints, 0, pumpkin.vertices.length)); var vert = pumpkin.vertices[index]; push(); translate(vert.x * 100, vert.y * 100, vert.z * 100); box(2); pop(); } rotationPhase = frameCount * 0.005; } 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 function draw ( ) { rotateY ( rotationPhase ) for ( var i = 0 ; i < numPoints ; i ++ ) { var index = floor ( map ( i , 0 , numPoints , 0 , pumpkin . vertices . length ) ) ; var vert = pumpkin . vertices [ index ] ; push ( ) ; translate ( vert . x * 100 , vert . y * 100 , vert . z * 100 ) ; box ( 2 ) ; pop ( ) ; } rotationPhase = frameCount * 0.005 ; }

If your pumpkin is off-center or rotated, add some more translations and rotations to line it up nicely in the canvas. By now our code should look something like this:

Now that we have our rotating pumpkin, let's focus on the wave effect next.

First, comment out the index and vert lines in draw; we will come back to them later when we combine the 2 animations.

for(var i = 0; i < numPoints; i++) { // var index = floor(map(i, 0, numPoints, 0, pumpkin.vertices.length)); // var vert = pumpkin.vertices[index]; push(); translate(vert.x * modelSize, vert.y * modelSize, vert.z * modelSize); box(2); pop(); } 1 2 3 4 5 6 7 8 9 for ( var i = 0 ; i < numPoints ; i ++ ) { // var index = floor(map(i, 0, numPoints, 0, pumpkin.vertices.length)); // var vert = pumpkin.vertices[index]; push ( ) ; translate ( vert . x * modelSize , vert . y * modelSize , vert . z * modelSize ) ; box ( 2 ) ; pop ( ) ; }

Now we need an array of vertices to store our wave positions. Create a variable called waveVertices and use a for loop to fill it with numPoints vectors in setup. Each vector should be at a random x position between -5 and 5, a y position of 0, and a random z position between -5 and 5.

function setup() { // ... waveVertices = []; for(let i = 0; i < numPoints; i++) { waveVertices[i] = createVector(random(-5, 5), 0, random(-5, 5)); } } 1 2 3 4 5 6 7 8 function setup ( ) { // ... waveVertices = [ ] ; for ( let i = 0 ; i < numPoints ; i ++ ) { waveVertices [ i ] = createVector ( random ( - 5 , 5 ) , 0 , random ( - 5 , 5 ) ) ; } }

In draw we can replace vert with waveVertices[i] to quickly see our random field of points.

for(var i = 0; i < numPoints; i++) { // var index = floor(map(i, 0, numPoints, 0, pumpkin.vertices.length)); // var vert = pumpkin.vertices[index]; var vert = waveVertices[i]; push(); translate(vert.x * modelSize, vert.y * modelSize, vert.z * modelSize); box(2); pop(); } 1 2 3 4 5 6 7 8 9 10 11 for ( var i = 0 ; i < numPoints ; i ++ ) { // var index = floor(map(i, 0, numPoints, 0, pumpkin.vertices.length)); // var vert = pumpkin.vertices[index]; var vert = waveVertices [ i ] ; push ( ) ; translate ( vert . x * modelSize , vert . y * modelSize , vert . z * modelSize ) ; box ( 2 ) ; pop ( ) ; }

To create the wave effect, we can set vert.y equal to some sin or cos function of vert.x and vert.z . To create a static wave, we could use vert.y = sin(vert.x)

for(var i = 0; i < numPoints; i++) { // var index = floor(map(i, 0, numPoints, 0, pumpkin.vertices.length)); // var vert = pumpkin.vertices[index]; var vert = waveVertices[i]; vert.y = sin(vert.x); push(); translate(vert.x * modelSize, vert.y * modelSize, vert.z * modelSize); box(2); pop(); } 1 2 3 4 5 6 7 8 9 10 11 12 for ( var i = 0 ; i < numPoints ; i ++ ) { // var index = floor(map(i, 0, numPoints, 0, pumpkin.vertices.length)); // var vert = pumpkin.vertices[index]; var vert = waveVertices [ i ] ; vert . y = sin ( vert . x ) ; push ( ) ; translate ( vert . x * modelSize , vert . y * modelSize , vert . z * modelSize ) ; box ( 2 ) ; pop ( ) ; }

We can make the wave ripple up and down by creating a wavePhase variable, setting it equal to 0 in setup and incrementing it in draw the same way we did with rotationPhase . Then we add wavePhase to vert.x in the sin function.

function draw() { // ... for(var i = 0; i < numPoints; i++) { // var index = floor(map(i, 0, numPoints, 0, pumpkin.vertices.length)); // var vert = pumpkin.vertices[index]; var vert = waveVertices[i]; vert.y = sin(vert.x + wavePhase); push(); translate(vert.x * modelSize, vert.y * modelSize, vert.z * modelSize); box(2); pop(); } // change 0.02 to other numbers to speed up or slow down the wave wavePhase = frameCount * 0.02; } 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 function draw ( ) { // ... for ( var i = 0 ; i < numPoints ; i ++ ) { // var index = floor(map(i, 0, numPoints, 0, pumpkin.vertices.length)); // var vert = pumpkin.vertices[index]; var vert = waveVertices [ i ] ; vert . y = sin ( vert . x + wavePhase ) ; push ( ) ; translate ( vert . x * modelSize , vert . y * modelSize , vert . z * modelSize ) ; box ( 2 ) ; pop ( ) ; } // change 0.02 to other numbers to speed up or slow down the wave wavePhase = frameCount * 0.02 ; }

To replicate the exact wave from the gif, set vert.y equal to sin(vert.x + wavePhase) * cos(vert.z + wavePhase) * 0.4

Now that the wave code works, all we need to do is smoothly transition between the pumpkin and the wave. Let's create an animationPosition variable, set it equal to 0 in setup , and update it with frameCount * 0.05 at the end of draw . We can use this variable to drive a sin function that swings each point between a wave position and a pumpkin position.

Inside the draw loop, create one variable called pumpkinVertex and another called waveVertex . Set pumpkinVertex equal to pumpkin.vertices[index] (replacing the commented out vert line), and waveVertex equal to waveVertices[i] (replacing the vert variable we had before). Your draw loop should look something like this:

function draw() { background(0); // base lighting color ambientLight(200, 200, 255); // center model on canvas and corrent rotation rotateZ(radians(180)); translate(0, -100, 0); //rotate model rotateY(rotationPhase); for(var i = 0; i < numPoints; i++) { var index = floor(map(i, 0, numPoints, 0, pumpkin.vertices.length)); var pumpkinVertex = pumpkin.vertices[index]; var waveVertex = waveVertices[i]; waveVertex.y = sin(waveVertex.x + wavePhase) * cos(waveVertex.y + wavePhase) * 0.4; push(); translate(vert.x * modelSize, vert.y * modelSize, vert.z * modelSize); box(2); pop(); } rotationPhase = frameCount * 0.05; wavePhase = frameCount * 0.02; animationPosition = frameCount * 0.05; } 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 function draw ( ) { background ( 0 ) ; // base lighting color ambientLight ( 200 , 200 , 255 ) ; // center model on canvas and corrent rotation rotateZ ( radians ( 180 ) ) ; translate ( 0 , - 100 , 0 ) ; //rotate model rotateY ( rotationPhase ) ; for ( var i = 0 ; i < numPoints ; i ++ ) { var index = floor ( map ( i , 0 , numPoints , 0 , pumpkin . vertices . length ) ) ; var pumpkinVertex = pumpkin . vertices [ index ] ; var waveVertex = waveVertices [ i ] ; waveVertex . y = sin ( waveVertex . x + wavePhase ) * cos ( waveVertex . y + wavePhase ) * 0.4 ; push ( ) ; translate ( vert . x * modelSize , vert . y * modelSize , vert . z * modelSize ) ; box ( 2 ) ; pop ( ) ; } rotationPhase = frameCount * 0.05 ; wavePhase = frameCount * 0.02 ; animationPosition = frameCount * 0.05 ; }

Now we can use the map function to interpolate between the waveVertex and pumpkinVertex with our animationPosition variable and the sin function.

var vert = createVector( map(sin(animationPosition), -1, 1, waveVertex.x, pumpkinVertex.x), map(sin(animationPosition), -1, 1, waveVertex.y, pumpkinVertex.y), map(sin(animationPosition), -1, 1, waveVertex.z, pumpkinVertex.z) ); 1 2 3 4 5 var vert = createVector ( map ( sin ( animationPosition ) , - 1 , 1 , waveVertex . x , pumpkinVertex . x ) , map ( sin ( animationPosition ) , - 1 , 1 , waveVertex . y , pumpkinVertex . y ) , map ( sin ( animationPosition ) , - 1 , 1 , waveVertex . z , pumpkinVertex . z ) ) ;

Now the draw function should look like this:

function draw() { background(0); // base lighting color ambientLight(200, 200, 255); // center model on canvas and corrent rotation rotateZ(radians(180)); translate(0, -100, 0); //rotate model rotateY(rotationPhase); for(var i = 0; i < numPoints; i++) { var index = floor(map(i, 0, numPoints, 0, pumpkin.vertices.length)); var pumpkinVertex = pumpkin.vertices[index]; var waveVertex = waveVertices[i]; waveVertex.y = sin(waveVertex.x + wavePhase) * cos(waveVertex.z + wavePhase) * 0.4; var vert = createVector( map(sin(animationPosition), -1, 1, waveVertex.x, pumpkinVertex.x), map(sin(animationPosition), -1, 1, waveVertex.y, pumpkinVertex.y), map(sin(animationPosition), -1, 1, waveVertex.z, pumpkinVertex.z) ); push(); translate(vert.x * modelSize, vert.y * modelSize, vert.z * modelSize); box(2); pop(); } rotationPhase = frameCount * 0.05; wavePhase = frameCount * 0.02; animationPosition = frameCount * 0.05; } 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 function draw ( ) { background ( 0 ) ; // base lighting color ambientLight ( 200 , 200 , 255 ) ; // center model on canvas and corrent rotation rotateZ ( radians ( 180 ) ) ; translate ( 0 , - 100 , 0 ) ; //rotate model rotateY ( rotationPhase ) ; for ( var i = 0 ; i < numPoints ; i ++ ) { var index = floor ( map ( i , 0 , numPoints , 0 , pumpkin . vertices . length ) ) ; var pumpkinVertex = pumpkin . vertices [ index ] ; var waveVertex = waveVertices [ i ] ; waveVertex . y = sin ( waveVertex . x + wavePhase ) * cos ( waveVertex . z + wavePhase ) * 0.4 ; var vert = createVector ( map ( sin ( animationPosition ) , - 1 , 1 , waveVertex . x , pumpkinVertex . x ) , map ( sin ( animationPosition ) , - 1 , 1 , waveVertex . y , pumpkinVertex . y ) , map ( sin ( animationPosition ) , - 1 , 1 , waveVertex . z , pumpkinVertex . z ) ) ; push ( ) ; translate ( vert . x * modelSize , vert . y * modelSize , vert . z * modelSize ) ; box ( 2 ) ; pop ( ) ; } rotationPhase = frameCount * 0.05 ; wavePhase = frameCount * 0.02 ; animationPosition = frameCount * 0.05 ; }

Now thatthe basic animation is working, we can tune the feel of the animation by changing the sin(animationPosition) call. Let's store the the sin equation in a variable and map it between 0 and 1 to clean things up a bit.

var animationLoopPosition = map(sin(animationPosition), -1, 1, 0, 1); 1 var animationLoopPosition = map ( sin ( animationPosition ) , - 1 , 1 , 0 , 1 ) ;

And our vert declaration becomes:

var vert = createVector( map(animationLoopPosition, 0, 1, waveVertex.x, pumpkinVertex.x), map(animationLoopPosition, 0, 1, waveVertex.y, pumpkinVertex.y), map(animationLoopPosition, 0, 1, waveVertex.z, pumpkinVertex.z) ); 1 2 3 4 5 var vert = createVector ( map ( animationLoopPosition , 0 , 1 , waveVertex . x , pumpkinVertex . x ) , map ( animationLoopPosition , 0 , 1 , waveVertex . y , pumpkinVertex . y ) , map ( animationLoopPosition , 0 , 1 , waveVertex . z , pumpkinVertex . z ) ) ;

Since our animationPosition is bouncing between 0 and 1 , we can write a simple easing function to change the animation so we speed up through the middle, and slow down on the pumpkin model and the wave shape. You can find some great easing functions written in pure JS here. I will be using the code from easeInOutQuart code.

function easeInOutQuart(t) { return t<.5 ? 8*t*t*t*t : 1-8*(--t)*t*t*t } 1 function easeInOutQuart ( t ) { return t < . 5 ? 8 * t* t* t* t : 1 - 8 * ( -- t ) * t* t* t }

and then change animationLoopPosition in draw :

var animationLoopPosition = easeInOutQuart(map(sin(animationPosition), -1, 1, 0, 1)); 1 var animationLoopPosition = easeInOutQuart ( map ( sin ( animationPosition ) , - 1 , 1 , 0 , 1 ) ) ;

and our final code should look like this.

Once you're comfortable with what we've used above, these free online courses will help bring you to the next level:

Introduction to p5.js

University of California, Los Angeles

LEARN MORE

The Nature of Code

Processing Foundation

LEARN MORE