PShape in P5 or work around

Hello

I'm creating a Tile object and then I have a function to draw things with this tiles. Problem is that I need at least 140 instances of those tiles and my frame rate is being killed

My Tile object is made of 2 arcs() and 2 lines(). Sometimes I have a third arc() colouring the tile. My Tile object changes its size according to how the function that draws with it calls it.

Looking for the solution I found Daniel's pshape tutotal which says:

Try to draw around 500 stars and your sketch may run rather slowly, about 10 FPS. This is because this style of drawing, often referred to as "immediate" mode, requires the renderer to compute the geometry each time through draw for each and every star. But is this necessary? After all, it's the same star over and over and over again. Using a PShape allows Processing to essentially "memorize" the geometry of that star. Drawing the memorized geometry (called a "Vertex Buffer Object" in lower-level OpenGL syntax) is called "retained" mode and is a great deal faster. Those 500 stars render easily at 60 FPS using a PShape instead. This can be achieved by including a PShape variable as part of the Star class.

So I looked for such PShape in P5 and only found that where the topic of PShape seems to be in the design phase.

So here I am, asking you guy for a work around : ]

(things I have tried: - hide my tiles as much as possible (I get 5 FPS that way) - render my tile with 2 arcs with big strokes (I get 10 FPS that way [ I need 30]) - render my tile with a bezier... I get lost trying to make my shape parametric - move everything, as is, over to Processing (I get almost 30 FPS !!! :) but my webpage doesn't get better :( ) - I didn't even try to implement the PShape thing in Processing since the Frame Rate is fine )

cheers

Tagged:

Answers

  • Thank you for your answer GoTo :)

    Maybe I'm not enough of an expert to fully appreciate it yet because it takes me to things I don't quite master yet.

    Do you mind if I post some code here to see how I should proceed to better it ?

      var tiles = [];
      var num = -1;
    
      function setup() {
        createCanvas(1000, 350);
        //
        for (x = 0; x < 1000; x += 50) {
          for (y = 0; y < 350; y += 50) {
            num++;
            var tileLoc = createVector(x, y);
            tiles[num] = new Tile(tileLoc, radians(1), 46);
          }
        }
      }
    
      function draw() {
        background(255);
        textSize(50);
        fill(128, 64);
        text(round(frameRate()), 0, 0, 80, 50);
        noFill();
        for (var i = 0; i < tiles.length; i++) {
          tiles[i].run();
        }
      }
    
      /////////////////////////////////////////////////////////////////////Tile Obj
      function Tile(tLoc, speed, radius) {
        this.loc = createVector(tLoc.x, tLoc.y);
        this.radius = radius;
        this.speed = speed;
        var angle = radians(dist(tLoc.x, tLoc.y, 0, 0) / 10);
        //
        var semiArc = (360 / 10) + tLoc.x / 10;
        var arcIntA = radians(-semiArc);
        var arcIntB = radians(semiArc)
        var arcExtA = radians(-semiArc);
        var arcExtB = radians(semiArc);
    
        this.run = function() {
          this.render();
          this.update();
        }
    
        this.update = function() {
          angle = angle + this.speed;
        }
    
        this.render = function() {
          stroke(250);
          rect(this.loc.x, this.loc.y, this.radius, this.radius);
          //
          stroke(0);
          push();
          translate(this.radius / 2, this.radius / 2);
          //
          arc(this.loc.x, this.loc.y, this.radius - 10, this.radius - 10, arcIntA + angle, arcIntB + angle);
          arc(this.loc.x, this.loc.y, this.radius, this.radius, arcExtA + angle, arcExtB + angle);
          //
          var semiAngle1 = radians(-semiArc) + angle;
          var ax = tLoc.x + cos(semiAngle1) * this.radius / 2;
          var ay = tLoc.y + sin(semiAngle1) * this.radius / 2;
          //
          var bx = tLoc.x + cos(semiAngle1) * (this.radius / 2 - 5);
          var by = tLoc.y + sin(semiAngle1) * (this.radius / 2 - 5);
          //
          line(ax, ay, bx, by);
          //
          var semiAngle2 = radians(semiArc) + angle;
          var dx = tLoc.x + cos(semiAngle2) * this.radius / 2;
          var dy = tLoc.y + sin(semiAngle2) * this.radius / 2;
          //
          var ex = tLoc.x + cos(semiAngle2) * (this.radius / 2 - 5);
          var ey = tLoc.y + sin(semiAngle2) * (this.radius / 2 - 5);
          //
          line(dx, dy, ex, ey);
          //
          pop();
        }
    
    
      }
    
  • edited February 2016

    Results of a very quick optimisation below. Main changes:

    • removed unnecessary function calls by condensing run(), update() and render() into a single render() method
    • removed duplicate variables: e.g. arcIntA == arcExtA
    • cached repeated calculations - especially important with cos() and sin(): caching these resulted in the most obvious improvement in framerate. Edit: note that I also moved all variable declarations to the top the render function. This is generally considered best practice in JS and also makes it easier to spot when calculations are duplicated ;)

    Framerate improvements were modest, but significant on my laptop: from ~30fps to ~35fps - I've just upgraded to a core i7 with 12GB RAM and am very happy :D

    Here's the code:

    var tiles = [];
    var num = -1;
    
    function setup() {
      createCanvas(1000, 350);
      //
      for (x = 0; x < 1000; x += 50) {
        for (y = 0; y < 350; y += 50) {
          num++;
          var tileLoc = createVector(x, y);
          tiles[num] = new Tile(tileLoc, radians(1), 46);
        }
      }
    }
    
    function draw() {
      background(255);
      textSize(50);
      fill(128, 64);
      text(round(frameRate()), 0, 0, 80, 50);
      noFill();
      for (var i = 0; i < tiles.length; i++) {
        tiles[i].render();
      }
    }
    
    //////// Tile Obj //////// 
    function Tile(tLoc, speed, radius) {
      this.loc = createVector(tLoc.x, tLoc.y);
      this.radius = radius;
      this.speed = speed;
    
      var angle = radians(dist(tLoc.x, tLoc.y, 0, 0) / 10);
      //
      var semiArc = (360 / 10) + tLoc.x / 10;    
      var arcA = radians(-semiArc);
      var arcB = radians(semiArc)
    
    
      this.render = function() {
    
        var arcAngleA = arcA + angle,
            arcAngleB = arcB + angle,
            cosSemiAngle1 = cos(semiAngle1),
            sinSemiAngle1 = sin(semiAngle1),  
            cosSemiAngle2 = cos(semiAngle2),
            sinSemiAngle2 = sin(semiAngle2),
            halfRadius = this.radius/2,
            halfRadiusOffset = this.radius/2 - 5,
    
            semiAngle1 = arcA + angle,
            ax = tLoc.x + cosSemiAngle1 * halfRadius,
            ay = tLoc.y + sinSemiAngle1 * halfRadius,
    
            bx = tLoc.x + cosSemiAngle1 * halfRadiusOffset,
            by = tLoc.y + sinSemiAngle1 * halfRadiusOffset,
    
            semiAngle2 = arcB + angle,
            dx = tLoc.x + cosSemiAngle2 * halfRadius,
            dy = tLoc.y + sinSemiAngle2 * halfRadius,
    
            ex = tLoc.x + cosSemiAngle2 * halfRadiusOffset,
            ey = tLoc.y + sinSemiAngle2 * halfRadiusOffset;
    
    
        stroke(250);
        rect(this.loc.x, this.loc.y, this.radius, this.radius);
        //
        stroke(0);
    
        push();
            translate(halfRadius, halfRadius);
            //
            arc(this.loc.x, this.loc.y, this.radius - 10, this.radius - 10, arcAngleA, arcAngleB);
            arc(this.loc.x, this.loc.y, this.radius, this.radius, arcAngleA, arcAngleB);
    
            line(ax, ay, bx, by);
            line(dx, dy, ex, ey);
            //
        pop();
    
        // update angle
        angle = angle + this.speed;
      }
    
    
    }
    

    Obviously caching the shape to SVG should result in far greater gains. I don't have time to look into that: meant to be teaching myself NodeJS and AngularJS at the moment...

  • What about drawing it on a PGraphics instead of PShape, could it improve the frameRate?

  • Yep - that had occurred to me too...

    For the sheer hell of it I just tried what I've seen recommended in the past for this type of sketch: namely using translate and rotate to achieve the position/rotation; instead of re-calculating everything for each object position. It does have the advantage of simplifying the code a little... I had also hoped that this would be more performant too; since it means you can pre-calculate all the drawing stuff; but the impression is that any gains are lost because of the additional calculations that must be performed by rotate()...

    Here's the code (haven't done much by way of tidying/testing/optimisation) for reference:

    var tiles = [];
    var num = -1;
    
    function setup() {
      createCanvas(1000, 350);
      //
      for (x = 0; x < 1000; x += 50) {
        for (y = 0; y < 350; y += 50) {
          num++;
    
            tiles[num] = new Tile(x, y, radians(1), 46);
        }
      }
    }
    
    function draw() {
      background(255);
      textSize(50);
      fill(128, 64);
      text(round(frameRate()), 0, 0, 80, 50);
      noFill();
      for (var i = 0, len = tiles.length; i < len; i++) {
        tiles[i].render();
      }
    }
    
    //////// Tile Obj //////// 
    function Tile(x, y, speed, radius) {
      this.x = x;
      this.y = y;
      this.radius = radius;
      this.speed = speed;
    
      var angle = radians(dist(x, y, 0, 0) / 10);
      var semiArc = (360 / 10) + x / 10;    
    
      this.arcA = radians(-semiArc);
      this.arcB = radians(semiArc);
    
        var cosSemiAngle1 = cos(this.arcA),
            sinSemiAngle1 = sin(this.arcA),  
            cosSemiAngle2 = cos(this.arcB),
            sinSemiAngle2 = sin(this.arcB),
            halfRadius = this.radius / 2,
            halfRadiusOffset = halfRadius - 5;
    
            this.ax = cosSemiAngle1 * halfRadius,
            this.ay = sinSemiAngle1 * halfRadius,
    
            this.bx = cosSemiAngle1 * halfRadiusOffset,
            this.by = sinSemiAngle1 * halfRadiusOffset,
    
            this.dx = cosSemiAngle2 * halfRadius,
            this.dy = sinSemiAngle2 * halfRadius,
    
            this.ex = cosSemiAngle2 * halfRadiusOffset,
            this.ey = sinSemiAngle2 * halfRadiusOffset;
    
      this.render = function() {
    
        var radius = this.radius,
            arcA = this.arcA,
            arcB = this.arcB;
    
        stroke(250);
        rect(0, 0, radius, radius);
        stroke(0);
    
        push();
            translate(this.x + radius/2, this.y + radius/2);
            rotate(angle);   
            arc(0, 0, radius - 10, radius - 10, arcA, arcB);
            arc(0, 0, radius, radius, arcA, arcB);
    
            line(this.ax, this.ay, this.bx, this.by);
            line(this.dx, this.dy, this.ex, this.ey);
    
            //
        pop();
    
        angle = angle + this.speed;
      } 
    
    }
    
  • Wow impressed how you guys are getting into it :D

    I have been testing your 2 attempts blindfish and the first one gives me more or less the same FPS as mine while the 2nd is almost 10 FPS slower on my machine.

    It looks like my last resort is to try the zenozeng's p5.js-svg option. I'll keep you posted

    thank you again !

  • edited February 2016

    ok, tested "as is" with zenozeng's svg renderer and FPS is between 3 and 4 FPS.

    here is how I did it:

    • create a new sketch and save it with the proper name ex "svg-test"

    • download tha lastest p5.js and the latest p5.svg.js

    • place both files inside the Librairies/ folder in the "svg-test" sketch folder

    • add in the of the index.html file

    • paste the above code inside the sktech

    • change createCanvas(1000, 350); to createCanvas(1000, 350, SVG);

    • run

    I guess the point is now to make a good use of the zenozeng library to turn the tile into some kind of svg parametric object ... I'm going to dig this but it doesn't feel good to me

  • It's running faster here using PGraphics.

    var tiles = [];
    var num = 0;
    
    function setup() {
      createCanvas(1000, 350);
      imageMode(CENTER);
    
      for (x = 0; x < 1000; x += 50) {
        for (y = 0; y < 350; y += 50) {
          var tileLoc = createVector(x, y);
          tiles[num] = new Tile(tileLoc, radians(1), 23);
          num++;
        }
      }
    }
    
    function draw() {
      background(255);
    
      textSize(50);
      fill(128, 64);
      text(round(frameRate()), 0, 0, 80, 50);
    
      for (var i = 0; i < tiles.length; i++) {
        tiles[i].run();
      }
    }
    
    /////////////////////////////////////////////////////////////////////Tile Obj
    function Tile(tLoc, speed, radius) {
      this.loc = createVector(tLoc.x, tLoc.y);
      this.radius = radius;
      this.speed = speed;
      this.angle = 0;
    
      this.render = function() {
        var semiArc = radians((360 / 10) + tLoc.x / 10);
        var pg = createGraphics(2*radius + 2, 2*radius + 2);
    
        pg.stroke(0);
        pg.noFill();
    
        pg.arc(radius, radius, 2*radius - 10, 2*radius - 10, -semiArc, semiArc);
        pg.arc(radius, radius, 2*radius, 2*radius, -semiArc, semiArc);
    
        var ax = radius + cos(-semiArc) * radius;
        var ay = radius + sin(-semiArc) * radius;
        var bx = radius + cos(-semiArc) * (radius  - 5);
        var by = radius + sin(-semiArc) * (radius  - 5);
        pg.line(ax, ay, bx, by);
    
        var dx = radius + cos(semiArc) * radius;
        var dy = radius + sin(semiArc) * radius;
        var ex = radius + cos(semiArc) * (radius - 5);
        var ey = radius + sin(semiArc) * (radius - 5);
        pg.line(dx, dy, ex, ey);
    
        return pg;
      };
      this.pg = this.render();
    
      this.run = function() {
        this.angle += this.speed;
    
        push();
          translate(this.loc.x + this.radius, this.loc.y + this.radius);
          rotate(this.angle);
          image(this.pg, 0, 0);
        pop();
    
        noFill();
        stroke(250);
        rect(this.loc.x, this.loc.y, 2*this.radius, 2*this.radius);
      };
    }
    

    I changed the meaning of radius while editing cause it was calling the diameter radius and it was confusing me.

  • wow it works really well ! :)

    thank you so much Barbara

    it is going to take me some time to get this createGraphics() thing to work on my code, but I have the feeling that this is it.

    I'll keep you posted

Sign In or Register to comment.