How can I improve the performance for the following 3d Animation?

I am a beginner with p5js and graphics in general. I’ve done a few 2D sketches, and while they were becoming more complex, I read about createGraphics() and how I can use this as a buffer to improve performance.

A few days ago, I tried sketching my first 3d animation, one of a complex sinusoid.
The link is here: p5.js Web Editor

The code looks like this:

const r = 60;
let vC, vR, mP;
let angl;
let reset;
let f;

let points = [];

function setup() {
  createCanvas(800, 500, WEBGL);
  frameRate(20);
  push();
  translate(0,0,0);
  f = 0.03;
  angl = PI/2;
  vC = createVector(0, 0);
  vR = createVector(0, 0);
  mp = createVector(0, 0, 0);
  pop();
}

function draw() {
  background(255);

  orbitControl();
  
  translate(vC.x, vC.y, 0);
  // rotateX(frameCount * 0.01);
  // rotateY(angl);
  
  // orbitControl(1, 1, 0.1, vC.x, vC.y, 0);

  // Draw the first plane
  push();
  smooth();
  translate(vC.x, vC.y, 0);
  fill(255, 255, 255);
  vR.x = sin(angl) * r;
  vR.y = cos(angl) * r;
  stroke('red');
  line(-width, 0, 0, width, 0, 0); // x
  stroke('blue');
  line(0, -height, 0, 0, height, 0); // y
  stroke('green');
  line(0, 0, -2000, 0, 0, 2000); // z
  
  push();
  stroke('black');
  translate(0, 0, PI/2 * r);
  circle(0, 0, 3);
  translate(0, 0, PI/2 * r);
  circle(0, 0, 3);
  translate(0, 0, PI/2 * r);
  circle(0, 0, 3);
  translate(0, 0, PI/2 * r);
  circle(0, 0, 3);
  translate(0, 0, PI/2 * r);
  circle(0, 0, 3);
  translate(0, 0, PI/2 * r);
  circle(0, 0, 3);
  translate(0, 0, PI/2 * r);
  circle(0, 0, 3);
  pop();
  
  push();
  translate(0, 0, TWO_PI * r);
  stroke('black')
  circle(0, 0, 3);
  pop();
  
  stroke('black');
  line(0, 0, 0, vR.x, vR.y, 0);
  circle(vR.x, vR.y, 3);
  mp.x = vR.x;
  mp.y = vR.y;
  mp.z += f*r;
  points.push([vR.x, mp.y, mp.z]);
  stroke('gray');
  for(let i = 1; i < points.length; i++) {
    // main moving point
    line(points[i-1][0], points[i-1][1], points[i-1][2], points[i][0], points[i][1], points[i][2]);
    
    // cosinus moving point
    push();
    translate(2*r, 0, 0);
    rotateY(PI/2);
    line(-points[i][2], points[i][1], -points[i-1][2], points[i-1][1]);
    // point(-points[i][2], points[i][1]);
    pop();
    
    // sinus moving point
    push();
    translate(0, 2*r, 0);
    rotateX(PI/2);
    line(points[i][0], points[i][2], points[i-1][0], points[i-1][2]);
    pop();
  }
  line(vR.x, vR.y, 0, mp.x, mp.y, mp.z);
  fill('black');
  point(mp.x, mp.y, mp.z);
  noFill()
  circle(0, 0, 2*r);
  pop();
  
  push();
  translate(2*r, 0, 0);
  rotateY(PI/2);
  fill('lightgray');
  plane(r * 3 * TWO_PI, 2*r);
  pop();
  
  push();
  translate(0, 2*r, 0);
  rotateX(PI/2);
  fill('lightgray');
  plane(2*r, r * 3 * TWO_PI);
  pop();
  
  angl+=f;
  if (angl>2*TWO_PI) {
    angl=PI/2;
    mp = createVector(0,0,0);
    console.log(points.length);
    points = [];
  }
}

function drawPoint(x, y, z) {
  push();
  translate(x, y, z);
  pop();
}

The animation works well, but computers struggle a little because of that for loop inside the drawing function. Is there a way I could incrementally update the helix (spiral), without adding all points repeatedly? Something similar to a createGraphics() in 2d, but this time in 3d?

In short, how can I draw the complex sinusoid incrementally without having to redraw all existing points?

What you are looking for is the p5 equivalent to a PShape which is a structure that holds arrays of geometry that gets passed to the GPU in one batch rather than making hundreds of separate graphics calls. The p5 version is called p5.Geometry, which, as far as I know, has not yet been documented anywhere. It stores arrays of of vertexes, face indexes, normals, and uvs (texture coordinates). Oh, and each one must have a unique string called “gid”.

Here’s an example I wrote a while ago:

let nGrid = 64;
let grid1;
let cam;
let palette;

function setup() {
  createCanvas( 800, 800, WEBGL );
  colorMode( HSB, 1, 1, 1 );
  cam = createCamera();
  palette = createColors();
  let data = [];
  for( let j=0; j<nGrid; j++ ) {
    let row = [];
    for( let i=0; i<nGrid; i++ ) {
      //let x = 2.0*i/nGrid-1, y = 2.0*j/nGrid-1,
      //  a = Math.atan2( y, x ), r = Math.sqrt( x*x+y*y );
      //row.push( (1.1+cos(TAU*2*r+3*a) + random(0.5)) * nGrid/32.0 );
      row.push( 2.6* ((noise(7*i/nGrid+17.3, 7*j/nGrid+3)-0.5)*1.5+0.75) * nGrid/32.0 );
    }
    data.push( row );
  }
  let max_height = (1.1+1.5) * nGrid/32.0;
  grid1 = createGrid( `grid1`, nGrid, data, max_height );
  noStroke();
}

function draw() {
  background( 0.6, 1, 0.25 );
  orbitControl( 2, 1, 0.05 );
  scale( 0.75*width/nGrid );
  rotateX( TAU/8 );
  rotateZ( frameCount/115.0 );
  translate( -nGrid/2, -nGrid/2 );
  ambientLight( 0, 0, 0.4 );
  directionalLight( 0, 0, 0.3, 0, 0, 1 );
  specularMaterial( 1 );
  pointLight( 0, 0, 0.4, cam.centerX, cam.centerY, cam.centerZ );
  texture( palette );
  model( grid1 );
}

function createColors() {
  let img = createImage( 256, 1 );
  img.loadPixels();
  for( let i=0; i<img.width; i++ ) {
    img.set( i, 0, color( 1-i/img.width, 1-pow(i/img.width,16), 1 ) );
  }
  img.updatePixels();
  return img;
}

function createGrid( name, N, data, max_height ) {
  console.log( data.length );
  let m = new p5.Geometry( 1, 1 );
  m.gid = name;
  let iv = 0;
  for( let i=0; i<N; i++ ) {
    // -Y side
    let z2 = data[0][i];
    m.vertices.push(
      new p5.Vector( i+1, 0, 0 ),
      new p5.Vector( i,   0, 0 ),
      new p5.Vector( i,   0, z2 ),
      new p5.Vector( i+1, 0, z2 ));
    m.faces.push( [ iv, iv+1, iv+2 ], [ iv+2, iv+3, iv ] );
    let zt = z2 / max_height;
    m.uvs.push([ zt, 0.5 ]);
    m.uvs.push([ zt, 0.5 ]);
    m.uvs.push([ zt, 0.5 ]);
    m.uvs.push([ zt, 0.5 ]);
    iv += 4;
  }
  for( let j=0; j<N; j++ ) {
    // -X side
    let z1 = data[j][0];
    m.vertices.push(
      new p5.Vector( 0, j, 0 ),
      new p5.Vector( 0, j+1, 0 ),
      new p5.Vector( 0, j+1, z1 ),
      new p5.Vector( 0, j, z1 ));
    m.faces.push( [ iv, iv+1, iv+2 ], [ iv+2, iv+3, iv ] );
    let zt = z1 / max_height;
    m.uvs.push([ zt, 0.5 ]);
    m.uvs.push([ zt, 0.5 ]);
    m.uvs.push([ zt, 0.5 ]);
    m.uvs.push([ zt, 0.5 ]);
    iv += 4;
  }
  for( let j=0; j<N; j++ ) {
    for( let i=0; i<N; i++ ) {
      // +Z  top
      let z0 = data[j][i];
      m.vertices.push(
        new p5.Vector( i, j, z0 ),
        new p5.Vector( i, j+1, z0 ),
        new p5.Vector( i+1, j+1, z0 ),
        new p5.Vector( i+1, j, z0 ));
      m.faces.push( [ iv, iv+1, iv+2 ], [ iv+2, iv+3, iv ] );
      let zt = z0 / max_height;
      m.uvs.push([ zt, 0.5 ]);
      m.uvs.push([ zt, 0.5 ]);
      m.uvs.push([ zt, 0.5 ]);
      m.uvs.push([ zt, 0.5 ]);
      iv += 4;

      // +X  side
      let z1 = i<N-1 ? data[j][i+1] : 0;
      m.vertices.push(
        new p5.Vector( i+1, j, z0 ),
        new p5.Vector( i+1, j+1, z0 ),
        new p5.Vector( i+1, j+1, z1 ),
        new p5.Vector( i+1, j, z1 ));
      m.faces.push( [ iv, iv+1, iv+2 ], [ iv+2, iv+3, iv ] );
      zt = max(z0,z1) / max_height;
      m.uvs.push([ zt, 0.5 ]);
      m.uvs.push([ zt, 0.5 ]);
      m.uvs.push([ zt, 0.5 ]);
      m.uvs.push([ zt, 0.5 ]);
      iv += 4;

      // +Y  side
      let z2 = j<N-1 ? data[j+1][i] : 0;
      m.vertices.push(
        new p5.Vector( i+1, j+1, z0 ),
        new p5.Vector( i  , j+1, z0 ),
        new p5.Vector( i  , j+1, z2 ),
        new p5.Vector( i+1, j+1, z2 ));
      m.faces.push( [ iv, iv+1, iv+2 ], [ iv+2, iv+3, iv ] );
       zt = max(z0,z2) / max_height;
      m.uvs.push([ zt, 0.5 ]);
      m.uvs.push([ zt, 0.5 ]);
      m.uvs.push([ zt, 0.5 ]);
      m.uvs.push([ zt, 0.5 ]);
     iv += 4;

    }
  }
  m.computeNormals();
  return m;
}

Let me add, here, however, that p5.Geometry is best for large, rigid geometries. It’s not great for dynamic geometry or something where you are adding to it incrementally unless you can figure out how to use shaders to move or selectively hide the elements. And it’s designed strictly for triangles, though a line is really just two long, skinny triangles.

4 Likes