Advice to improve performance of a 3D model composed of many blocks

Hi !

I’m trying to display several layers like the one depicted below.

it’s basically a grid of 64 x 64 box() elements each having a different thickness (i read those values from a .mat file at the start).
It occured to me that if I try to make several layers like this, it’s gets really slow. I’m wondering whether there is a method to make a layer a single 3d object so that p5.js doesn’t need to render 5 x 64 x 64 boxes :laughing::sweat_smile:

The most efficient way to render it would be to use a single box, pass in the heights as a texture, and use a fragment shader to ray trace the image. The ray tracer would be tricky to write, but would likely be extremely fast.

Alternatively, don’t think of it as an array of boxes, but as a single mesh. You can easily make triangle strips running in either the x- or y-directions that have the box tops (I’m thinking of z as the height) alternating with the box sides. You then need another set of strips that run along the sides facing the other direction which would alternate between the sides of the boxes and degenerate triangles that would run along the corners of the boxes (vertically into your height-field). Put all of these strips together into a single PShape and it should render much faster than separate boxes would. If you zig-zag up and down the grid alternating between rows of tops/sides and sides/degenerates you could put it all in a single triangle strip.

Ah, it seems Processing doesn’t have a flat shading mode which means you can’t use triangle strips, so you’ll have to give 6 triangles per cube (2 each for the front, bottom, and right sides) with an extra pair at the left of each end and a row of pairs for the top faces.

EDIT: I just realized you want this for p5.js which, oddly, doesn’t appear to have an equivalent to PShape for storing geometry. Unless someone has a work-around for p5, you might be better off trying three.js instead.

Original response:

On my 7 year old machine, I can set N as high as 450 and still get 60 fps.

EDIT: skip to below for QUAD-based code.

int N = 64;
PShape s;

void setup() {
  size( 800, 800, P3D );

  float [][] data = new float[N+1][N+1];  // leave a strip of 0s on the right and bottom
  for( int j=0; j<N; j++ )
    for( int i=0; i<N; i++ ) {
      float x = 2.*i/N-1, y = 2.*j/N-1, 
        a = atan2( y, x ), r = sqrt(x*x+y*y);
      data[j][i] = 1.1+cos(TAU*2*r+3*a) + random(.5);
    }
  s = createShape();
  s.beginShape(TRIANGLES);
  s.noStroke();
  for( int i=0; i<N; i++ ) {
    // -Y side
    float z2 = data[0][i];
    s.vertex( i+1, 0, 0 );
    s.vertex( i,   0, 0 );
    s.vertex( i+1, 0, z2 );

    s.vertex( i+1, 0, z2 );
    s.vertex( i,   0, 0 );
    s.vertex( i,   0, z2 );
  }
  for( int j=0; j<N; j++ ) {
    float z0, z1, z2;
    // -X side
    z1 = data[j][0];
    s.vertex( 0, j,   0 );
    s.vertex( 0, j+1, 0 );
    s.vertex( 0, j,   z1 );

    s.vertex( 0, j,   z1 );
    s.vertex( 0, j+1, 0 );
    s.vertex( 0, j+1, z1 );
    
    for( int i=0; i<N; i++ ) {
      z0 = z1;
      z1 = data[j][i+1];
      z2 = data[j+1][i];

      // +Z top
      s.vertex( i,   j,   z0 );
      s.vertex( i,   j+1, z0 );
      s.vertex( i+1, j,   z0 );

      s.vertex( i+1, j,   z0 );
      s.vertex( i,   j+1, z0 );
      s.vertex( i+1, j+1, z0 );

      // +X side
      s.vertex( i+1, j,   z0 );
      s.vertex( i+1, j+1, z0 );
      s.vertex( i+1, j,   z1 );

      s.vertex( i+1, j,   z1 );
      s.vertex( i+1, j+1, z0 );
      s.vertex( i+1, j+1, z1 );

      // +Y side
      s.vertex( i+1, j+1, z0 );
      s.vertex( i,   j+1, z0 );
      s.vertex( i+1, j+1, z2 );

      s.vertex( i+1, j+1, z2 );
      s.vertex( i,   j+1, z0 );
      s.vertex( i,   j+1, z2 );
    }
  }
  s.endShape();
}

void draw() {
  background( 0, 32, 64 );
  camera( 0, 0, N, 0, 0, 0, 0, 1, 0 );
  perspective( PI/3, 1, N/4, 2*N );
  rotateX( TAU/6. );
  lights();
  rotateZ( frameCount/115. );
  shape( s, -N/2, -N/2 );
  fill( 255 );
  camera();
  perspective();
  text( nf( frameRate, 0, 2 ), 10, 20 );
}

EDIT because discourse won’t let me post again:

Not sure why I didn’t use QUADS instead of TRIANGLES. With QUADS I can go up to N=550 with a single grid and the code’s simpler. Here’s a version with multiple grids.

int N = 64;
PShape s1, s2;

PShape genGrid( int N, float [][] data ) {
  PShape s = createShape();
  s.beginShape(QUADS);
  s.noStroke();
  for( int i=0; i<N; i++ ) {
    // -Y side
    float z2 = data[0][i];
    s.vertex( i+1, 0, 0 );
    s.vertex( i,   0, 0 );
    s.vertex( i,   0, z2 );
    s.vertex( i+1, 0, z2 );
  }
  for( int j=0; j<N; j++ ) {
    float z0, z1, z2;
    // -X side
    z1 = data[j][0];
    s.vertex( 0, j,   0 );
    s.vertex( 0, j+1, 0 );
    s.vertex( 0, j+1, z1 );
    s.vertex( 0, j,   z1 );
    
    for( int i=0; i<N; i++ ) {
      z0 = z1;
      z1 = data[j][i+1];
      z2 = data[j+1][i];

      // +Z top
      s.vertex( i,   j,   z0 );
      s.vertex( i,   j+1, z0 );
      s.vertex( i+1, j+1, z0 );
      s.vertex( i+1, j,   z0 );

      // +X side
      s.vertex( i+1, j,   z0 );
      s.vertex( i+1, j+1, z0 );
      s.vertex( i+1, j+1, z1 );
      s.vertex( i+1, j,   z1 );

      // +Y side
      s.vertex( i+1, j+1, z0 );
      s.vertex( i,   j+1, z0 );
      s.vertex( i,   j+1, z2 );
      s.vertex( i+1, j+1, z2 );
    }
  }
  s.endShape();
  return s;
}

void setup() {
  size( 800, 600, P3D );
  // leave a strip of 0 on the right and bottom
  float [][] data = new float[N+1][N+1];
  for( int j=0; j<N; j++ )
    for( int i=0; i<N; i++ ) {
      float x = 2.*i/N-1, y = 2.*j/N-1,
        a = atan2( y, x ), r = sqrt(x*x+y*y);
      data[j][i] = (1.1+cos(TAU*2*r+3*a) + random(.5)) * N/64;
    }
  s1 = genGrid( N, data );
  for( int j=0; j<N; j++ )
    for( int i=0; i<N; i++ ) {
      float x = 2.*i/N-0.5, y = 2.*j/N-1.2,
        r = sqrt(x*x+y*y);
      data[j][i] = (1.1+cos(TAU*2*r) + random(.5)) * N/64;
    }
  s2 = genGrid( N, data );
}

void draw() {
  background( 0, 32, 64 );
  lights();
  camera( 0, 0, N*1.2, 0, 0, 0, 0, 1, 0 );
  perspective( PI/3, 1, N/4, N*4 );
  push();
  rotateY( TAU/5. );
  translate( 0, 0, -N*0.35 );
  rotateX( TAU/12. );
  rotateZ( frameCount/115. );
  shape( s1, -N/2, -N/2 );
  pop();
  push();
  rotateY( -TAU/5. );
  translate( 0, 0, -N*0.35 );
  rotateX( TAU/12. );
  rotateZ( frameCount/115. );
  shape( s2, -N/2, -N/2 );
  pop();
  fill( 255 );
  camera();
  perspective();
  text( nf( frameRate, 0, 2 ), 10, 20 );
}

Adding color doesn’t seem to slow it down any:

int N = 64;
PShape s1, s2;

PShape genGrid( int N, float [][] data ) {
  float c = 2.6*N/32;
  PShape s = createShape();
  s.beginShape(QUADS);
  s.noStroke();
  for( int i=0; i<N; i++ ) {
    // -Y side
    float z2 = data[0][i];
    s.fill( data[0][i]/c, 0.5, 1 );
    s.vertex( i+1, 0, 0 );
    s.vertex( i,   0, 0 );
    s.vertex( i,   0, z2 );
    s.vertex( i+1, 0, z2 );
  }
  for( int j=0; j<N; j++ ) {
    float z0, z1, z2;
    // -X side
    z1 = data[j][0];
    s.fill( data[j][0]/c, 0.5, 1 );
    s.vertex( 0, j,   0 );
    s.vertex( 0, j+1, 0 );
    s.vertex( 0, j+1, z1 );
    s.vertex( 0, j,   z1 );
    
    for( int i=0; i<N; i++ ) {
      z0 = z1;
      z1 = data[j][i+1];
      z2 = data[j+1][i];

      // +Z top
      s.fill( data[j][i]/c, 0.5, 1 );
      s.vertex( i,   j,   z0 );
      s.vertex( i,   j+1, z0 );
      s.vertex( i+1, j+1, z0 );
      s.vertex( i+1, j,   z0 );

      // +X side
      s.fill( data[j][i+(z0>z1?0:1)]/c, 0.5, 1 );
      s.vertex( i+1, j,   z0 );
      s.vertex( i+1, j+1, z0 );
      s.vertex( i+1, j+1, z1 );
      s.vertex( i+1, j,   z1 );

      // +Y side
      s.fill( data[j+(z0>z2?0:1)][i]/c, 0.5, 1 );
      s.vertex( i+1, j+1, z0 );
      s.vertex( i,   j+1, z0 );
      s.vertex( i,   j+1, z2 );
      s.vertex( i+1, j+1, z2 );
    }
  }
  s.endShape();
  return s;
}

void setup() {
  size( 800, 600, P3D );
  colorMode( HSB, 1, 1, 1 );
  // leave a strip of 0 on the right and bottom
  float [][] data = new float[N+1][N+1];
  for( int j=0; j<N; j++ )
    for( int i=0; i<N; i++ ) {
      float x = 2.*i/N-1, y = 2.*j/N-1,
        a = atan2( y, x ), r = sqrt(x*x+y*y);
      data[j][i] = (1.1+cos(TAU*2*r+3*a) + random(.5)) * N/32;
    }
  s1 = genGrid( N, data );
  for( int j=0; j<N; j++ )
    for( int i=0; i<N; i++ ) {
      float x = 2.*i/N-0.5, y = 2.*j/N-1.2,
        r = sqrt(x*x+y*y);
      data[j][i] = (1.1+cos(TAU*2*r) + random(.5)) * N/32;
    }
  s2 = genGrid( N, data );
}

void draw() {
  background( 0.6, 1, 0.25 );
  lights();
  camera( 0, 0, N*1.2, 0, 0, 0, 0, 1, 0 );
  perspective( PI/3, 1.*width/height, N/4, N*4 );
  push();
  rotateY( TAU/5. );
  translate( 0, 0, -N*0.35 );
  rotateX( TAU/12. );
  rotateZ( frameCount/115. );
  shape( s1, -N/2, -N/2 );
  pop();
  push();
  rotateY( -TAU/5. );
  translate( 0, 0, -N*0.35 );
  rotateX( TAU/12. );
  rotateZ( frameCount/115. );
  shape( s2, -N/2, -N/2 );
  pop();
  fill( 0, 0, 1 );
  camera();
  perspective();
  text( nf( frameRate, 0, 2 ), 10, 20 );
}
1 Like

Wow !

Thank you so much for your detailed answer…

This was what I was stuck with previously (please check the rotate speed in the following video)
https://twitter.com/ramithuh/status/1561485870598946821

I’m hoping to study the directions you shared and improve! Thanks!:heart:

EDIT : wow… that colored example is so cool!

@scudly I was wondering whether this is what is analogous to PShape in p5.js :thinking:

Almost, but no. In both cases, you construct an array of triangles (quads, but they are really pairs of triangles under the covers) that get packed into a machine native data format buffer that is sent off to the OpenGL video driver to display. The HUGE difference, is that in Processing, I can create the PShape just one time in setup() which preserves that OpenGL data buffer and then draw it with a single (or very few) graphics call. In contrast, using just beginShape() in either Processing or p5, nothing is saved. You have to call it to re-generate the entire buffer from scratch every draw() frame. And doing that is painfully slow because every single vertex has to be put in that buffer with a function call every single frame. The only discussion I could find about adding a PShape equivalent for p5 in the github issues was all about adding SVG support with only a brief mention of 3D shapes and they just glossed over it. The p5 maintainers seem to have decided that serious 3D performance just isn’t of interest.

If you really want to stick with javascript, then I’d suggest you take a look at three.js instead. It easily has the performance capabilities, but, sadly, is rather more complicated to use. I’ve only looked at it, but never used it, so I wouldn’t know how to create the mesh there.

1 Like

There is an undocumented workaround for caching geometry: p5.geometry

This tutorial by Paul Wheeler is a great introduction to it: Custom 3D Geometry in P5.js

3 Likes

That works. Thanks, Raph.

But I absolutely cannot figure out how per-vertex colors work. The geometry seems to have a vertexColors array, but I don’t know what data format or value range it expects or if p5 is even using it.

So here’s a monochomatic cube mesh:

let nGrid = 64;
let grid1;
let cam;

function setup() {
  createCanvas( 800, 800, WEBGL );
  colorMode( HSB, 1, 1, 1 );
  cam = createCamera();
  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 );
    }
    data.push( row );
  }
  grid1 = createGrid( `grid1`, nGrid, data );
  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.2 );
  directionalLight( 0, 0, 0.3, 0, 0, 1 );
  specularMaterial( 1 );
  pointLight( 0.3, 0.0, 0.3, cam.centerX, cam.centerY, cam.centerZ );
  model(grid1);
}

function createGrid( name, N, data ) {
  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 ] );
    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 ] );
    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 ] );
      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 ] );
      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 ] );
      iv += 4;

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

Thank you !! @scudly & @sableRaph

Starting from @scudly 's code, I was able to extend it to my need without performance issues. Will explore more!

I actually didn’t have any idea about creating it using a single mesh, quite fascinating!

Edit : There was another discussion on p5.geometry I will add that link here, i think there is an example with texture in it

2 Likes

Here’s a version that uses a 256x1 image as the color palette mapped to block height

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.3, 0.0, 0.3, 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;
}
1 Like

Hi @scudly thank you so much again!

Finally I was able to build what I need, so grateful to all of you who shared codes, and new directions.
Here’s a video of the final thing I made.

2 Likes