P5.JS 3D Dot Diagram rendering extremely slow

I am trying to convert a normal 2D image(a simple JPEG) into a 3D Dot Diagram that the user can move around with. But upon trying to render that dot diagram, the program becomes extremely slow. Can anyone point where I am going wrong?

var x = [];
var y = [];
var z = [];
var colors = [];
var a = 0;
var counter = 0;

let img;

function preload() {
    img = loadImage('assets/image.jpeg');
}

function setup() {
  createCanvas(720, 400, WEBGL);
  background(0);

  img.resize(width/3, height/2);

  for(let col = 0; col < img.width; col+=3){
    for(let row = 0; row < img.height; row+=3){
        let c = img.get(col,row);
        let rgb_val = c[0] + c[1] + c[2]
        colors[a] = c 
        x[a] = map(col,0,255,-125,125)
        y[a] = map(row,0,255,-125,125)
        z[a] = map(rgb_val,0,765,-50,0)
        stroke(c)
        push();
        a++
    }
  }

}

function draw() {
   translate(0,0,-50);
   rotateY(frameCount * 0.1);
  background(0);

  for (var i = 0; i < a; i++) {
    stroke(colors[i])
    push();
    translate(x[i], y[i], z[i]);
    sphere(1);
    pop();
  }

  orbitControl();

}
1 Like
 for(let col = 0; col < img.width; col+=3){
    for(let row = 0; row < img.height; row+=3){

the img.width and img.hight make your render possibly larger than your canvas. how big is your jpeg in pixels?

Hello liogra,

The image I am trying on it right now is around 197x256.

maybe use sphereDetail() and use less than the default 30 to make is run smoother?

This is a cross post from StackOverflow: javascript - P5.JS 3D Dot Diagram rendering extremely slow - Stack Overflow

3 Likes

Answered on StackOverflow. This took some doing but I got it up to ~60 FPS using p5.Geometry :exploding_head:

const dotRadius = 1;
const detail = 4;

let img;

let fpsDisplay;
let lastTime = 0;

let geom;

function preload() {
  img = loadImage('https://www.paulwheeler.us/files/clooney.jpeg');
}

function setup() {
  console.log('Initializing');
  createCanvas(720, 400, WEBGL);
  // Because the spheres are so small, the default stroke makes them all black.
  // You could also use strokeWeight(0.1);
  noStroke();

  img.resize(width / 3, height / 2);

  const dotGrid = function() {
    const sliceCount = this.detailX + 1;
    let dotNumber = 0;
    for (let col = 0; col < img.width; col += 3) {
      for (let row = 0; row < img.height; row += 3) {
        let c = img.get(col, row);
        let rgb_val = c[0] + c[1] + c[2];
        let xOff = map(col, 0, img.width, -125, 125);
        let yOff = map(row, 0, img.height, -125, 125);
        let zOff = map(rgb_val, 0, 765, -50, 0);
        for (let i = 0; i <= this.detailY; i++) {
          const v = i / this.detailY;
          const phi = PI * v - PI / 2;
          const cosPhi = cos(phi);
          const sinPhi = sin(phi);

          for (let j = 0; j <= this.detailX; j++) {
            const u = j / this.detailX;
            const theta = 2 * PI * u;
            const cosTheta = cos(theta);
            const sinTheta = sin(theta);
            const p = createVector(
              xOff + dotRadius * cosPhi * sinTheta,
              yOff + dotRadius * sinPhi,
              zOff + dotRadius * cosPhi * cosTheta
            );
            this.vertices.push(p);
            this.vertexNormals.push(p);
            // All vertices in each dot get the same UV coordinates
            this.uvs.push(map(col, 0, img.width, 0, 1), map(row, 0, img.height, 0, 1));
          }
        }

        // Generate faces for the current dot

        // offset = number of vertices for previous dots.
        let offset = dotNumber * (this.detailX + 1) * (this.detailY + 1);
        let v1, v2, v3, v4;
        for (let i = 0; i < this.detailY; i++) {
          for (let j = 0; j < this.detailX; j++) {
            v1 = i * sliceCount + j + offset;
            v2 = i * sliceCount + j + 1 + offset;
            v3 = (i + 1) * sliceCount + j + 1 + offset;
            v4 = (i + 1) * sliceCount + j + offset;
            this.faces.push([v1, v2, v4]);
            this.faces.push([v4, v2, v3]);
          }
        }

        dotNumber++;
      }
    }
    console.log(`Dots: ${dotNumber}`);

    console.log(`Vertices: ${this.vertices.length}`);
    console.log(`Faces: ${this.faces.length}`);
  };

  geom = new p5.Geometry(detail, detail, dotGrid);
  geom.gid = 'dot-grid';

  fpsDisplay = createInput('0');
  fpsDisplay.position(10, 10);
}

function draw() {
  background(0);
  orbitControl(2, 1, 0.1);

  texture(img);
  model(geom);

  let t = millis();
  fpsDisplay.value(`${1000 / (t - lastTime)}`);
  lastTime = t;
}
2 Likes

I’ve tried and it seems to have no effect on the performance.

Yes, I did post it on Stack Overflow but no one seemed to answer even after a day, so then I decided to post the question here.

Whatttt, I can’t believe you did it :exploding_head:, I gave up thinking it was impossible to build. But I don’t know how to thank you on this. Thank you so so much Paul!!!

That’s fine, I just recommend that when you cross post between this forum and StackOverflow you add comments in both places mentioning the other post. That way somebody doesn’t invest time answering your question in one place without realizing it has already been answered elsewhere.

Cheers.

2 Likes

Okay, good point, I didn’t realize that but I will do that from now on. By the way, I’ve asked if you could explain the code a bit on SO, I’ll ask that here too in case you may not have not seen my comment in SO.

I’m working on a detailed article about how p5.Geometry works, but here’s some quick notes:

The p5.Geometry constructor takes three arguments: detailX, detailY, and a callback function. Both detailX and detailY are expected to be numbers, in this case I’m using them to represent the number of dots. The built in p5.Geometry types (like sphere and plane) use these differently, and they are necessary for p5.Geometry.computeFaces to work properly because it uses them to determine how many vertices there are per strip of triangles. So computeFaces is not going to work with this geometry hence I generate the faces in dotGrid.

The callback function (which is dotGrid in this case) is where the magic happens. This function is responsible for generating the list of vertices and uvs (and in this case vertexNormals and faces as well). For some geometry you can use computeNormals but I’m not sure it works in this particular case, and because we already know the normal for each vertex, specifying it manually is easier.

The callback function is invoked as a member of the p5.Geometry instance, so in the dotGrid function this is the p5.Geometry instance.

Much of the code in my dotGrid function was shamelessly stolen from the ellipsoid function from p5.js itself, which uses trig to generate a set of vertices for a sphere. However I offset these positions depending on where the dot is in the grid, and then I also manually generate the faces for each sphere.

The vertices array is a list of p5.Vector objects that represent the corners of the triangles that make up the geometry in 3d space. The faces array contains arrays of three integers which are the indices in the vertices array for the three corners of each triangle.

1 Like