Josiah Leach
Josiah Leach

Reputation: 58

Is there a way to draw hundreds of points faster (p5.js)

I am making a program to test out my attempt at a Perlin Noise generating algorithm. The Perlin noise itself seems fine, however I've found that drawing that noise on the canvas is very slow. This is probably because for every single point in the canvas I have to call the stroke() function to change the color of the next pixel, then draw that pixel. This is done over a 400*400 pixel canvas, so I am changing the color with stroke() 160,000 times and calling point() 160,000 times.

This takes some time to do. I was wondering if there is any way of making this faster. Perhaps if I could turn the Perlin noise into an image, then load that image instead of drawing all 160,000 points individually?

The code of my draw loop is below

function draw() {
  background(220);
  strokeWeight(1);
      
  for(var row = 0; row < height; row ++)
  {
    for(var column = 0; column < width; column ++)
    {
      //takes a noise value from the myNoise array whose elements have a range of [-1,1] and turns it into a value from [0,256], and makes that the color of the next point
      stroke((myNoise[row][column]+1)*128);
      
      point(column,row)
    }
  }
    
  noLoop();
}

Edit: I used the following code to create and load an image. Credit to Samathingamajig for the tip.

function draw() {
  background(220);
      
  img = createImage(width,height);
  img.loadPixels();
  
  for(var row = 0; row < height; row ++)
  {
    for(var column = 0; column < width; column ++)
    {
      //takes a noise value from the myNoise array whose elements have a range of [-1,1] and turns it into a value from [0,256], and makes that the color of the next pixel in the image
      img.set(row,column,color((myNoise[row][column]+1)*128))      
    }
  }
  img.updatePixels();
  
  image(img,0,0)
  
  noLoop();
  
}

Also Samathingamajig pointed out that 400*400 is 160,000, not 1,600, which I have changed above.

My original code took about 4 seconds to run the draw loop. This new version takes about 0.75 seconds.

I also tested using the createGraphics() method as suggested by rednoyz. This was not as fast as using the image methods because it still requires me to call stroke() 160,000 times.

Both of these solutions gave me an image that I could very quickly draw, however createImage() allowed me to create the image in much less time than createGraphics() did.

Upvotes: 3

Views: 1509

Answers (2)

George Profenza
George Profenza

Reputation: 51837

Just to add a bit of nuance to the existing suggestion:

use pixels[] instead of set(x, y, color): it's less intuitive to think of a 1D index that takes into account [r,g,b,a,...] pixels order, and (pixelDensity on retina displays), but it is faster.

The documentation mentions:

Setting the color of a single pixel with set(x, y) is easy, but not as fast as putting the data directly into pixels[]. Setting the pixels[] values directly may be complicated when working with a retina display, but will perform better when lots of pixels need to be set directly on every loop.

In your case that would roughly look like this:

img.loadPixels();
  
  let numPixels = width * height;
  for(let pixelIndex = 0; pixelIndex < numPixels; pixelIndex ++)
  {
    //takes a noise value from the myNoise array whose elements have a range of [-1,1] and turns it into a value from [0,256], and makes that the color of the next pixel in the image
    // index of red channel for the current pixel
    // pixels = [r0, g0, b0, a0, r1, g1, b1, a1, ...]
    let redIndex = pixelIndex * 4;
    // convert 1D array index to 2D array indices
    let column = pixelIndex % width;
    let row    = floor(pixelIndex / width);
    // get perlin noise value
    let grey = (myNoise[row][column]+1) * 128;
    // apply grey value to R,G,B channels (and 255 to alpha)
    img.pixels[redIndex]     = grey; // R
    img.pixels[redIndex + 1] = grey; // G
    img.pixels[redIndex + 2] = grey; // B
    img.pixels[redIndex + 3] = 255;  // A
  }
  
  img.updatePixels();

(also looping once instead of nested looping will help).

Regarding point(), it might use something like beginShape(POINTS);vertex(x,y);endShape(); behind the scenes which means something like this would be slightly more efficient:

let numPixels = width * height;
  beginShape(POINTS);
  for(let pixelIndex = 0; pixelIndex < numPixels; pixelIndex ++)
  {
    //takes a noise value from the myNoise array whose elements have a range of [-1,1] and turns it into a value from [0,256], and makes that the color of the next pixel in the image
    // convert 1D array index to 2D array indices
    let column = pixelIndex % width;
    let row    = floor(pixelIndex / width);
    // get perlin noise value
    stroke(color((myNoise[row][column]+1) * 128));
    // apply grey value to R,G,B channels (and 255 to alpha)
    vertex(column, row);
  }
  endShape();

That being said, it might not work as intended:

  1. AFAIK this won't work with createCanvas(400, 400, WEBGL) as currently you can't set an independent stroke to each vertex in a shape.
  2. With the typical Canvas 2D renderer this may still be very slow to render this many vertices using beginShape()/endShape()

Although a more advanced topic, another option that should be fast is shader(). (Might find some perlin noise inspiration on shadertoy.com btw).

p5.js is great to learn with but it's main goal is not to have the most performant canvas renderers. If shaders are a bit too complex at this stage, but you're comfortable with javascript in general, you can look at other libraries such as pixi.js (maybe pixi particle-emitter could be handy ?)

Upvotes: 3

KoderM
KoderM

Reputation: 380

I've tried a lot of millisecond tests, and the image approach is by FAR the best. The problem is really just the amount of pixels you're trying to process as @Samathinamajig pointed out.

testing: https://editor.p5js.org/KoderM/sketches/6XPirw_98s

Upvotes: 2

Related Questions