Optimizing p5.js project / sandbox/pixel simulator

Viewed 85

I've started on a pixel sim project and it doesn't run well. What can I do to optimize it? I'm fairly confident that the issue is in all the checks that are done involving the particles array. I'm looking for any solution, to do anything different, or any optimizations in general, not just about the particles array but throughout the whole code. My goal for this project is to create a pixel sim web app, and if it can't run well after a few seconds of spawning sand, it defeats the purpose of a web app. Any help is appreciated.

Main file:

let alter = true;

let particles = []

function setup() {
  let canvas = createCanvas(windowWidth, windowHeight);
  frameRate(120);
}

var a = ['rgb(244,220,148)', 'rgb(236,211,140)', 'rgb(252,228,156)', 'rgb(252,220,149)', 'rgb(244,212,148)', 'rgb(228,204,132)', 'rgb(240,220,156)']

function sandColor() {
  return color(a[Math.floor(Math.random() * a.length)]);
}

function drect(c, x, y, l, w) {
  noStroke();
  fill(c);
  rect(x, y, l, w);
}

class Particle {
  constructor(p, c, x, y, s) {
    this.p = p;
    this.c = c;
    this.x = x;
    this.y = y;
    this.s = s;
  }

  draw() {
    drect(this.c, this.x, this.y, this.s, this.s);
  }
}

function check(x, y) {
  return color(get(x, y));
}

function draw() {

  drect(color(37, 150, 190), 0, 0, windowWidth, windowHeight)

  tw = 4;
  th = 4;

  for (let i = 0; i < particles.length; i++) {
    particles[i].draw()
  }

  alter = !(alter)
  if (!alter) {

    for (let i = 0; i < particles.length; i++) {
      if (particles[i].p == 's') {
        let down = false
        if (JSON.stringify(check(particles[i].x, particles[i].y + 4).levels) == '[37,150,190,255]') {
          particles[i].y += 4;
          down = true;
        }
        if (!down) {
          let r = Math.floor(Math.random() * 2);
          if (r == 0) {
            if (JSON.stringify(check(particles[i].x - 4, particles[i].y + 4).levels) == '[37,150,190,255]') {
              particles[i].y += 4;
              particles[i].x -= 4;
            } else {
              if (JSON.stringify(check(particles[i].x + 4, particles[i].y + 4).levels) == '[37,150,190,255]') {
                particles[i].y += 4;
                particles[i].x += 4;
              }
            }
          }
        }
      }
    }

    if (mouseIsPressed) {
      for (let i = 0; i < 6; i++) {
        for (let j = 0; j < 6; j++) {
          let p = 's'
          let c = sandColor()
          let x = (Math.floor(mouseX / tw)) * tw + (i * 4) - 9;
          let y = (Math.floor(mouseY / th)) * th + (j * 4) - 9;
          let s = 4;

          let sand = new Particle(p, c, x, y, s)
          let d = true;
          for (let m = 0; m < particles.length; m++) {
            if (particles[m].x == x && particles[m].y == y && particles[m].p == "s") {
              d = false;
            }
          }
          if (d) {
            drect(c, x, y, s, s)
            particles.push(sand)
          }
        }
      }
    }
  }
}

function windowResized() {
  resizeCanvas(windowWidth, windowHeight);
}

document.addEventListener('contextmenu', event => event.preventDefault());
<script src="https://cdnjs.cloudflare.com/ajax/libs/p5.js/1.4.2/p5.min.js"></script>

Hosted example: https://pixsim.loganstottle202.repl.co/ and if that doesn't work the code is here: https://replit.com/@LoganStottle202/pixsim?v=1

2 Answers

Unfortunately I won't have the time to provide a nice detailed runnable example such as your fun sketch.

Hopefully I can give you a few directions:

  • as the p5 get() reference mentions: "Getting the color of a single pixel with get(x, y) is easy, but not as fast as grabbing the data directly from pixels[]. ". You'd call loadPixels() once at the start of the frame, then use pixels[4 * (x * d + i) + ((y * d + j) * width * d + )] where d = pixelDensity().
  • JSON.stringify() and comparing strings can get computationally heavy for many particles. Consider marking a particle's state (colliding, moving, still/inactive, etc.) with as an integer value which you can compare directly (e.g. again const integer state with descriptive names). (You can then reuse this state to render the particle in different colours if you like). If you really really want to use the colour, you could simplify by using the alpha value for different states (e.g. 255, 254, 253, etc.) where perceptually it would be hard to notice and you can just compare against .levels[3] avoiding JSON.stringify() and string comparison. I do recommend using global or static Particle state const variables with descriptive name an a basic state property for the Particle instances.
  • you can separate your graphics into two "layers" using createGraphics(): one would be used to render the "active/alive" alive particles (it could even be the global p5 graphics buffer). The other layer could be used to render the static particles that have settled. You can then re-use/redraw this layer using image(yourStaticSandLayer, 0, 0);, getting rid of rect() calls for static particles (since they've been drawn/cached into the layer already).

(Additionally, you can simplify sandColor() to simply return random(a); since p5's random() can also pick a random item in an array for you. This won't speed up anything, just simplify code: makes it easier to read/maintain)

Update Here's basic demo based roughly on your Particle class and using p5.Graphics:

let particles = [];
let activeParticlesLayer;
let inactiveParticlesLayer;

function setup() {
  createCanvas(300, 150);
  
  activeParticlesLayer = createGraphics(width, height);
  inactiveParticlesLayer = createGraphics(width, height);
  
  for(let i = 0;  i < 10; i++){
    particles.push(new Particle(color(0, 192, 0), random(width), random(90), 10, 10));
  }
}

function draw() {
  // clear only active layer: don't clear inactive layer
  activeParticlesLayer.background(255);
  
  // for debugging only: count active particles
  let numActiveParticles = 0;
  for(let i = 0;  i < 10; i++){
    let p = particles[i];
    // only update particles if they're active
    if(p.isActive){
      // if the current particle collided (with stage bottom for now)
      // then make it inactive (change colour as a visual cue) and cache to inactive layer
      // replace this with pixels[] collision logic
      if(p.y > height - p.s){
        p.isActive = false;
        p.c = color(0, 128, 0);
        // particle is inactive: cache into inactive layer (which isn't cleared)
        p.draw(inactiveParticlesLayer); 
      }
      // otherwise our particle is active: update position and render to active layer
      else{
        p.y += 1;
        p.draw(activeParticlesLayer);
        numActiveParticles++;
      }  
    }
  }
  
  // display layers
  image(activeParticlesLayer, 0, 0);
  image(inactiveParticlesLayer, 0, 0);
  text("active particles: " + numActiveParticles, 10, 15);
}

class Particle {
  constructor(c, x, y, s) {
    this.isActive = true;
    this.c = c;
    this.x = x;
    this.y = y;
    this.s = s;
  }
  
  update(){
    if(this.y > height - this.s){
      this.isActive = false;
      this.c = color(0, 128, 0);
    }else{
      this.y += 1;  
    }
  }

  draw(buffer) {
    buffer.fill(this.c);
    buffer.rect(this.x, this.y, this.s, this.s);
  }
}
<script src="https://cdnjs.cloudflare.com/ajax/libs/p5.js/1.4.2/p5.min.js"></script>

Note: the above removes a of the functionalities in your original demo such as using colour to check collisions. For the sake of simplicity there's only a very small number of particles that only collide with the bottom of the canvas (and don't stack). Hopefully this is enough to illustrate the isActive state change and caching the rect() call only once, as the state changes from active to inactive.

Additionally I recommend looking at Developer Tools and looking into profiling the code to spot exactly what the slower bit of code are and focusing on those (instead of optimising bits of code that have little to no impact, but have the potential of making the code less readable / harder to maintian in the future)

I saw you had your GitHub link yesterday and created a PR to implement minor performance improvements.

They are mostly minor improvments. I think you need to change to a pixel based system for a performance overhaul, but if you are sticking with this approach, these improvements should help.

  1. Remove re-declarations.

You have constants like tw and th being declared right inside the draw() loop, unnecessarily being re-declared every cycle. I put them outside the draw() function.

  1. Change Loop logic

You had 2 for (i in particles) loops. Instead of looping it over twice, I put the logic inside a single loop, so you loop half as many times. Also, I changed for...in loop to a for loop for performance improvements.

  1. Remove dead particles

I saw that there is only code to add particles, but not to remove them. I added a check to see if the particles have fallen below windowHeight and removed them from particles array to prevent infinite growth of the array.

Hope this helps.

Overall, I think George provided great insights as to how you could further re-structure and improve your code. Hope that helps you create a fun project. I would love to see more!

Related