Drawing and orbiting a shape with many vertices

Hello! I’m drawing a shape with thousands of vertices in my draw() function; it’s just a line drawing connecting many vertices. I’d like to then orbit around this shape with e.g. the easycam library. I’m able to do this, but framerate drops to ~0.3 fps. Is there a smarter way to do this? The shape won’t change during runtime, so I was thinking that something like model() would be useful, but I’m unsure of how to do this with a custom shape. I spent a good while googlinig this, but couldn’t seem to come up with the right search terms, so any advice or or resources pointing me in the right direction are greatly appreciated!

Thank you!

Hi,

Welcome to the forum! :wink:

How many vertices do you need to draw?

If the shape doesn’t change, it might not be faster because it depends on how much time the computer takes to compute the curve points you are drawing. If you are doing an expensive computation to compute the line then yes but if you have a lot of vertices to draw it’s not the same issue.

Also it’s not entirely related to your question but you might want to read this previous thread about rendering on the GPU :

2 Likes

Rendering p5.Geometry objects is indeed much more efficient than either drawing triangles with beginShape/endShape, or drawing lots of individual primitives (like box and plane). The reason for this is that p5.js takes p5.Geometry objects and turns them into vertex buffers which it caches and uses to render the model each time it is referenced.

One option is to create a 3d model file (such as a Wavefront .obj file, which you can create with Blender for example), load it with loadModel and render it with model. However if you want to create your model dynamically this is possible too. You just need to create a p5.Geometry instances and populate it’s vertices and faces arrays. Here’s an example of a sketch that demonstrates rendering an Icosahedron both with beginShape/endShape and with p5.Geometry (runnable version here):

const PHI = (1 + Math.sqrt(5)) / 2;

let vertices = [
  [0, 1, PHI],

  [PHI, 0, 1],
  [0, -1, PHI],
  [-PHI, 0, 1],
  [-1, PHI, 0],
  [1, PHI, 0],

  [PHI, 0, -1],
  [1, -PHI, 0],
  [-1, -PHI, 0],
  [-PHI, 0, -1],
  [0, 1, -PHI],

  [0, -1, -PHI],
];

let useModel;
let geom;

function setup() {
  createCanvas(windowWidth, windowHeight, WEBGL);
  useModel = createCheckbox("Use Model?");
  useModel.position(10, 10);
  useModel.style('color', 'white');
  
  geom = new p5.Geometry(1, 1, constructIcosahedron);
  // This is used as the key for caching vertex buffers. Make sure it is unique.
  geom.gid = 'icosahedron';
  // Automatically calculate normals based on faces.
  geom.computeNormals();
}

// This callback is invoked by the p5.Geometry constructor. It must populated
// the vertices and faces arrays. It can also populate the uvs array and
// calculate normals.
function constructIcosahedron() {
  for (let v of vertices) {
    this.vertices.push(createVector(...v));
  }
  for (let i = 0; i < 5; i++) {
    let n = (i + 1) % 5;
    this.faces.push([
      i + 1,
      i + 6,
      n + 6
    ]);
    
    this.faces.push([
      i + 1,
      n + 1,
      0
    ]);
  }
  for (let i = 0; i < 5; i++) {
    let n = (i - 1);
    if (n < 0) {
      n = 4;
    }
    this.faces.push([
      i + 6,
      i + 1,
      n + 1
    ]);

    this.faces.push([
      i + 6,
      n + 6,
      11
    ]);
  }
}

function draw() {
  background(0);
  orbitControl(2, 2, 0.01);
  directionalLight(200, 200, 200, 0.5, -0.5, -1);
  directionalLight(200, 50, 50, 1, -0.5, 0.5);
  directionalLight(50, 50, 200, -0.5, 1, -0.5);
  

  scale(100);

  if (useModel.checked()) {
    model(geom);
  } else {
    beginShape(TRIANGLES);
    for (let i = 0; i < 5; i++) {
      vertex(...vertices[i + 1]);
      vertex(...vertices[i + 6]);
      let n = (i + 1) % 5;
      vertex(...vertices[n + 6]);

      vertex(...vertices[i + 1]);
      vertex(...vertices[n + 1]);
      vertex(...vertices[0]);
    }
    for (let i = 0; i < 5; i++) {
      vertex(...vertices[i + 6]);
      vertex(...vertices[i + 1]);
      let n = (i - 1);
      if (n < 0) {
        n = 4;
      }
      vertex(...vertices[n + 1]);

      vertex(...vertices[i + 6]);
      vertex(...vertices[n + 6]);
      vertex(...vertices[11]);
    }
    endShape();

    push();
    strokeWeight(8);
    for (let vert of vertices) {
      stroke(...vert.map(c => map(c, -PHI, PHI, 0, 255)));
      point(vert[0], vert[1], vert[2]);
    }
    pop();
  }
}

2 Likes

Oh wow, this is terrifically helpful! I was able to get something up and running with your helpful example, at 60 fps no less! There wasn’t much documentation in the p5 reference, so this is greatly appreciated.

A brief follow-up question (and I can start a new question if that’s better): is there an easy way to determine the value for detailX and detailY when declaring the new p5.geometry? I’ve found that they should multiply to be the number of vertices (i.e. detailX +1 * detailY +1 = geom.vertices.length), and the order of the two matters (a tall object should have detailY > detailX, and vice versa). But determining detailX and detailY in this way feels overly complicated.

Thanks again for your help!

In the “constructGeometry” callback you can use this.detailX and this.detailY, and these will be the values you passed as the first to parameters to new p5.Geometry(). These parameters are mostly intended for procedurally generated geometry such as cylinders and spheres where you can produce different numbers of faces to more accurately represent the shape. Or in cases like plane they allow you to add arbitrary numbers of subdivisions so that you have additional vertices that might be transformed with a vertex shader. They are also used by the computeFaces and averageNormals functions on p5.Geometry but again these are mostly intended for use with the built in procedural primitives. Unless you have a good reason for supporting different levels of detail with your geometry generation code I would recommend just leaving these values as 1.

I really should write a tutorial on this topic. You are right that it is poorly documented. I figured it out by reading the source code for p5.Geometry.js and 3d_primitives

Thanks! Interesting- after generating my geometry, using values of 1 seem to not display anything on the screen whereas using bigger numbers (in my case, (100, 140)) shows the object correctly… I’ll keep playing around with this!