# Sketch runs slow in P5.js WEBGL, (SOLVED by using P5.Geometry)

Hi.
I am translating a sketch from P5.java to P5.js. In java, it runs nice and smooth at a frameRate near 60. In p5.js the frameRate drops to around 4 f/s and a lot of details are missing.
How can I improve this, or is WEBGL really that bad?
Thanks.

P5.java:

``````int num;
float pt[];
int style[];
float sin_ar[];
float cos_ar[];
float PRECISION = 1.0;
int AR_LENGTH = 360;

void setup() {
size(600, 600, P3D);
background(255);
smooth();
sin_ar = new float[AR_LENGTH];
cos_ar = new float[AR_LENGTH];
for (int i = 0; i < AR_LENGTH; i++) {
}
num = 150;
pt = new float[6*num]; // rotx, roty, deg, rad, w, speed
style = new int[2*num]; // color, render style
// Set up arc shapes
int index = 0;
float prob;
for (int i = 0; i < num; i++) {
pt[index++] = random(PI*2); // Random X axis rotation
pt[index++] = random(PI*2); // Random Y axis rotation
pt[index++] = random(60, 80); // Short to quarter-circle arcs
if (random(100)>90) pt[index]=(int)random(8, 27)*10;
pt[index++] = int(random(2, 50)*5); // Radius. Space them out nicely
pt[index++] = random(4, 32); // Width of band
if (random(100)>90) pt[index]=random(40, 60); // Width of band
pt[index++] = radians(random(5, 30))/2; // Speed of rotation
// get colors
prob = random(100);
if (prob < 30) style[i*2]=color_blend(random(1), 255, 0, 100, 255, 0, 0, 210);
else if (prob < 70) style[i*2]=color_blend(random(1), 0, 153, 255, 170, 225, 255, 210);
else if (prob < 90) style[i*2]=color_blend(random(1), 200, 255, 0, 150, 255, 0, 210);
else style[i*2]=color(255, 255, 255, 220);
if (prob < 50) style[i*2]=color_blend(random(1), 200, 255, 0, 50, 120, 0, 210);
else if (prob < 90) style[i*2]=color_blend(random(1), 255, 100, 0, 255, 255, 0, 210);
else style[i*2] = color(255, 255, 255, 220);
style[i*2+1]=(int)(random(100))%3;
}
}

void draw() {
background(0);
println(frameRate);
int index=0;
translate(width/2, height/2, 0);
rotateX(PI/6);
rotateY(PI/6);
for (int i=0; i<num; i++) {
pushMatrix();
rotateX(pt[index++]);
rotateY(pt[index++]);
if (style[i*2+1]==0) {
stroke(style[i*2]);
noFill();
strokeWeight(1);
arc_line(0, 0, pt[index++], pt[index++], pt[index++]);
} else if (style[i*2+1]==1) {
fill(style[i*2]);
noStroke();
arc_line_bars(0, 0, pt[index++], pt[index++], pt[index++]);
} else {
fill(style[i*2]);
noStroke();
arc_b(0, 0, pt[index++], pt[index++], pt[index++]);
}
// increase rotation
pt[index-5]+=pt[index]/10;
pt[index-4]+=pt[index++]/20;
popMatrix();
}
}

// Get blend of two colors
public int color_blend(float fract,
float r, float g, float b,
float r2, float g2, float b2, float a) {
r2 = (r2 - r);
g2 = (g2 - g);
b2 = (b2 - b);
return color(r + r2 * fract, g + g2 * fract, b + b2 * fract, a);
}

// Draw arc line
public void arc_line(float x, float y, float deg, float rad, float w) {
int a=(int)(min (deg/PRECISION, AR_LENGTH-1));
int numlines=(int)(w/2);
for (int j=0; j<numlines; j++) {
beginShape();
for (int i=0; i<a; i++) {
}
endShape();
}
}

// Draw arc line with bars
public void arc_line_bars(float x, float y, float deg, float rad, float w) {
int a = int((min (deg/PRECISION, AR_LENGTH-1)));
a /= 4;
for (int i=0; i<a; i+=4) {
}
endShape();
}

// Draw solid arc
public void arc_b(float x, float y, float deg, float rad, float w) {
int a = int(min (deg/PRECISION, AR_LENGTH-1));
for (int i = 0; i < a; i++) {
}
endShape();
}
``````

P5.js:

``````let num;
let pt = [];
let style = [];
let sin_ar = [];
let cos_ar = [];
const PRECISION = 1.0;
let AR_LENGTH = 360;

function setup() {
createCanvas(600, 600, WEBGL);
background(255);
for (let i = 0; i < AR_LENGTH; i++) {
}
num = 150;
// Set up arc shapes
let index = 0;
let prob;
for (let i = 0; i < num; i++) {
pt[index++] = random(PI * 2); // Random X axis rotation
pt[index++] = random(PI * 2); // Random Y axis rotation
pt[index++] = random(60, 80); // Short to quarter-circle arcs
if (random(100) > 90) pt[index] = random(8, 27) * 10;
pt[index++] = int(random(2, 50) * 5); // Radius. Space them out nicely
pt[index++] = random(4, 32); // Width of band
if (random(100) > 90) pt[index] = random(40, 60); // Width of band
pt[index++] = radians(random(5, 30)) ; // Speed of rotation
// get colors
prob = random(100);
if (prob < 30)
style[i * 2] = color_blend(random(1), 255, 0, 100, 255, 0, 0, 210);
else if (prob < 70)
style[i * 2] = color_blend(random(1), 0, 153, 255, 170, 225, 255, 210);
else if (prob < 90)
style[i * 2] = color_blend(random(1), 200, 255, 0, 150, 255, 0, 210);
else style[i * 2] = color(255, 255, 255, 220);
if (prob < 50)
style[i * 2] = color_blend(random(1), 200, 255, 0, 50, 120, 0, 210);
else if (prob < 90)
style[i * 2] = color_blend(random(1), 255, 100, 0, 255, 255, 0, 210);
else style[i * 2] = color(255, 255, 255, 220);
style[i * 2 + 1] = int(random(100)) % 3;
}
//ortho([left],   [right],  [bottom], [top],    [near],     [far])
//ortho(-width/2, width/2, -height/2, height/2, -height / 1, height / 2 );
}

function draw() {
background(0);
translate(-width/2, height/2);
let index = 0;
translate(width / 2, -height / 2, 0);
rotateX(PI / 6);
rotateY(PI / 6);
print(frameRate());
for (let i = 0; i < num; i++) {
push();
rotateX(pt[index++]);
rotateY(pt[index++]);
if (style[i * 2 + 1] == 0) {
stroke(style[i * 2]);
noFill();
strokeWeight(1.5);
arc_line(0, 0, pt[index++], pt[index++], pt[index++]);
} else if (style[i * 2 + 1] == 1) {
fill(style[i * 2]);
noStroke();
arc_line_bars(0, 0, pt[index++], pt[index++], pt[index++]);
} else {
fill(style[i * 2]);
noStroke();
arc_b(0, 0, pt[index++], pt[index++], pt[index++]);
}
// increase rotation
pt[index - 5] += pt[index] / 10;
pt[index - 4] += pt[index++] / 20;
pop();
}
}

// Get blend of two colors
function color_blend(frac, r, g, b, r2, g2, b2, a) {
r2 = r2 - r;
g2 = g2 - g;
b2 = b2 - b;
return color(r + r2 * frac, g + g2 * frac, b + b2 * frac, a);
}

// Draw arc line
function arc_line(x, y, deg, rad, w) {
let a = min(deg / PRECISION, AR_LENGTH - 1);
let numlines = w / 2;
for (let j = 0; j < numlines; j++) {
beginShape();
for (let i = 0; i < a; i++) {
}
endShape();
}
}

// Draw arc line with bars
function arc_line_bars(x, y, deg, rad, w) {
let a = int(min(deg / PRECISION, AR_LENGTH - 1));
a /= 4;
for (let i = 0; i < a; i += 4) {
vertex(cos_ar[i] * (rad + w) + x, sin_ar[i] * (rad + w) + y);
vertex(cos_ar[i + 2] * (rad + w) + x, sin_ar[i + 2] * (rad + w) + y);
vertex(cos_ar[i + 2] * rad + x, sin_ar[i + 2] * rad + y);
}
endShape();
}

// Draw solid arc
function arc_b(x, y, deg, rad, w) {
let a = int(min(deg / PRECISION, AR_LENGTH - 1));
for (let i = 0; i < a; i++) {
vertex(cos_ar[i] * (rad + w) + x, sin_ar[i] * (rad + w) + y);
}
endShape();
}
``````

i just attempted to answer the reverse question, P5.js goes to Processing abnormally slow！ - #4 by jeremydouglass in which I contended that p5.js was superior to Processing. As soon as I get to a computer I will take a look at this code and see if my contention holds water or not.

2 Likes

So it looks like 1) QUADS and QUAD_STRIP don’t work for some reason (use TRIANGLES and TRIANGLE_STRIP instead), and 2) the performance of numerous long lines with stroke is terrible, you can simulate this with a thin triangle strip though.

https://editor.p5js.org/Kumu-Paul/sketches/IxXW_MXVj

2 Likes

I must say however that I’m frustrated with P5.js’s drawing power.
Below is a very small sketch where P5.js struggles to get 8 frames per second, while P5.java easily goes to 60 f/s. When you draw lines with the line() function it even drops to just 1 frame per second, while P5.java stays within 60 f/s. Why I would need to use a TRIANGLE_STRIP?

P5.java

``````void setup() {
size(600, 600, P3D);
}

void draw() {
background(0);
translate(width/2, height/2, 0);
for (int i = 0; i < 100; i++) {
stroke(random(80, 255));
rotateX(random(PI * 2));
rotateY(random(PI * 2));
lines();
}
print(frameRate);
}

void lines() {
int y = 0;
beginShape();
for (int x = 0; x < 100; x++) {
// line(x,  y, width - x, y);
vertex(x, y);
y++;
}
endShape();
}
``````

P5.js

``````function setup() {
createCanvas(600, 600, WEBGL);
background(255);
}

function draw() {
background(0);
for (let i = 0; i < 100; i++) {
stroke(random(80, 255));
rotateX(random(PI * 2));
rotateY(random(PI * 2));
lines();
}
print(frameRate());
}

function lines() {
let y = 0;
beginShape();
for (let x = 0; x < 100; x++) {
// line(x,  y, width - x, y);
vertex(x, y);
y++;
}
endShape();
}
``````

@KumuPaul
So it’s true that QUADS and QUAD_STRIP don’t work with the WEBGL renderer, and already on 22 Mar 2020 the issue was reported, but not solved until today.
I am trying to translate another sketch now with a more defined closed shape. A nut. And it’s actually not that difficult to transform a QUAD_STRIP shape into a simple vertex() shape by repeating the ‘end’ vectors and inverting them. But still the sketch is far from running as smooth as in P5.java. Also the pointLight()'s work very differently. What can I do to improve this?
P5.js

``````const NUTS = 25;
const MIN_HEIGHT = 2;
const MAX_HEIGHT = 6;
const MAX_ROT = 0.01;

var nuts = [];
let l0, l1, l2;
let rx = 0,
ry = 0;
let dx = 0,
dy = 0;
let rotX = 0,
rotY = 0;

function setup() {
createCanvas(800, 700, WEBGL);
for (let i = 0; i < NUTS; i++) {
nuts[i] = new Nut();
}
l0 = createVector(-100, -100, -100);
l1 = createVector(-100, -100, -100);
l2 = createVector(-100, -100, -100);
rx = random(0, TWO_PI);
dx = random(-MAX_ROT, MAX_ROT);
ry = random(0, TWO_PI);
dy = random(-MAX_ROT, MAX_ROT);
}

function draw() {
background(0);
scale(5);
rotateX(rotX);
rotateY(-rotY);
rotateX(1.5);
lights();
pointLight(64, 64, 64, l0.x, l0.y, l0.z);
pointLight(64, 64, 64, l1.x, l1.y, l1.z);
pointLight(64, 64, 64, l2.x, l2.y, l2.z);
rx += dx;
ry += dy;
rotateX(rx);
rotateY(ry);
for (let i = 0; i < NUTS; i++) {
nuts[i].draw();
}
}

class Nut {
constructor() {
this.x = random(0);
this.y = random(0);
this.z = random(-20, 20);
this.h = random(MIN_HEIGHT, MAX_HEIGHT);
this.rx = 0;
this.ry = 0;
this.rz = random(0, TWO_PI);
this.dx = 0;
this.dy = 0;
this.dz = random(-MAX_ROT, MAX_ROT);
this.r = int(random(64, 256));
this.g = int(random(64, 256));
this.b = int(random(64, 256));
}

draw() {
fill(this.r, this.g, this.b);
noStroke();
this.rx += this.dx;
this.ry += this.dy;
this.rz += this.dz;
this.z = 30 * cos(this.rz);
rotateX(this.rx);
rotateY(this.ry);
rotateZ(this.rz);
this.tube(this.r0);
this.tube(this.r1);
this.side(this.z + this.h);
this.side(this.z - this.h);
}

let a = 0;
let angle = TWO_PI / 6;
for (let i = 0; i < 6; i++) {
beginShape();
a += angle;
endShape();
}
}

side(ht) {
let a = 0;
let angle = TWO_PI / 6;
for (let i = 0; i < 6; i++) {
beginShape();
vertex(this.r0 * cos(a), this.r0 * sin(a), ht);
vertex(this.r1 * cos(a), this.r1 * sin(a), ht);
a += angle;
vertex(this.r1 * cos(a), this.r1 * sin(a), ht);
vertex(this.r0 * cos(a), this.r0 * sin(a), ht);
endShape();
}
}
}

function mouseDragged() {
if (mouseY < height - 20) {
rotY -= (mouseX - pmouseX) * 0.01;
rotX -= (mouseY - pmouseY) * 0.01;
}
}
``````

P5.java

``````float rotX, rotY;

int NUTS = 25;
float MIN_HEIGHT = 2;
float MAX_HEIGHT = 6;
float MAX_ROT = .01;

TheNut[] nuts = new TheNut[NUTS];
PVector l0, l1, l2;
float rx, ry;
float dx, dy;

void setup() {
size(1000, 750, P3D);
for (int i = 0; i < NUTS; i++) {
nuts[i] = new TheNut();
}
l0 = new PVector(random(-200, 200), random(-200, 200), random(-200, 200));
l1 = new PVector(random(-200, 200), random(-200, 200), random(-200, 200));
l2 = new PVector(random(-200, 200), random(-200, 200), random(-200, 200));
rx = random(0, TWO_PI);
dx = random(-MAX_ROT, MAX_ROT);
ry = random(0, TWO_PI);
dy = random(-MAX_ROT, MAX_ROT);
}

void draw() {
background(0);
translate(width/2, height/2);
scale(5);
rotateX(rotX);
rotateY(-rotY);
lights();
pointLight(64, 64, 64, l0.x, l0.y, l0.z);
pointLight(64, 64, 64, l1.x, l1.y, l1.z);
pointLight(64, 64, 64, l2.x, l2.y, l2.z);
rx += dx;
ry += dy;
rotateX(rx);
rotateY(ry);
for (int i = 0; i < NUTS; i++) {
nuts[i].draw();
}
}

class TheNut {
float x, y, z, h;
float rx, ry, rz;  // rotations
float dx, dy, dz;  // deltas
int r, g, b;

TheNut() {
x = random(0);
y = random(0);
z = random(-20, 20);
h = random(MIN_HEIGHT, MAX_HEIGHT);
rx = 0;
ry = 0;
rz = random(0, TWO_PI);
dx = 0;
dy = 0;
dz = random(-MAX_ROT, MAX_ROT);
r = int(random(64, 256));
g = int(random(64, 256));
b = int(random(64, 256));
}

void draw() {
fill(r, g, b);
noStroke();
rx += dx;
ry += dy;
rz += dz;
z = 30 * cos(rz);
rotateX(rx);
rotateY(ry);
rotateZ(rz);
float angle = TWO_PI / 5;

// inner
tube(r0);
// outer
tube(r1);
// top
side(z + h);
// bottom
side(z - h);
}

float angle = TWO_PI / 6;
float a = 0;
for (int i = 0; i < 8; i++) {
a += angle;
}
endShape();
}

void side(float ht) {
float angle = TWO_PI / 6;
float a = 0;
for (int i = 0; i < 8; i++) {
vertex(r0 * cos(a), r0 * sin(a), ht);
vertex(r1 * cos(a), r1 * sin(a), ht);
a += angle;
}
endShape();
}
}

void mouseDragged() {
if (mouseY < height - 20) {
rotY -= (mouseX - pmouseX) * 0.01;
rotX -= (mouseY - pmouseY) * 0.01;
}
}
``````

@KumuPaul I know I’m being insistent, but it seems that you are one of few that can help me here.
I was searching for other options to improve the sketch above, and I stumbled - on your post - commenting about p5.Geometry objects being much more efficient than drawing multiple shapes but like you said there is almost no documentation about this, so one needs to study the source code to understand it. My question is, do you think that using geometry objects will eventually run the code above similar to the speed in P5.java? Because before spending more time I’m really about to give up on WEBGL, and starting to learn GLSL to write shaders.

I’ve got it running online but on Pjs (Processing Java Mode syntax):

Still slow and not as vibrant colors as running it on Processing’s PDE itself, but much faster than your current p5js version.

As a bonus I’ve also got a “Rotating Arcs” sketch:

@GoToLoop
Thank you so much for your effort and interest. The sketch runs very well in openProcessing, and I learned a few more things from your code. It’s a pity that openProcessing’s Pjs mode only lets the function pointLight() work on basic shapes like sphere() etc. My final goal however is coding in P5.js.
As I see you are a long-time collaborator, I would like to ask you the same question as I made to @KumuPaul , because only for his contention that P5.js is superior to P5.java I didn’t give up already.
“Do you think that using geometry objects will eventually run the code above similar to the speed in P5.java?” or would you advise learning GLSL to write shaders

Actually I dunno whether p5.Geometry can pull that out. Dunno much about it.

I’d also love to learn shaders. It’s a way to directly code for the GPU.
It’s as fast as we can get for 3D stuff for sure.

Thanks. Yes, I made up my mind and I am going to dive into this. The shadertoy website has incredible examples. But at first, I also found it quite complicated, a totally different concept. But then I found this tutorial which gave me the first grip. It gives a bit of work because for every tutorial you have to change a number, but it’s easy to understand. Now I’m reading this tutorial and this Book of Shaders. Still none of these touch 3D shapes, but at least I am confident that after reading these tutorials I will get a grip on that as well and when I finish my first simple rotating nut, I will post it here.

1 Like

Sorry, I haven’t been online much lately. I’ve been taking a vacation and dealing with a few other things. I have three quick thoughts, although they may not be entirely satisfactory:

1. Regarding `pointLight()` working with geometry drawn with `beginShape()`/`vertex()`/`endShape()`: In order for this to work you need to use the `normal()` function which I am quite proud to say I contributed to p5.js (although it was a very minor change). If you don’t use this function before you call `vertex()` then each vertex will get the default normal which is `(0, 0, 1)` or something.
2. Regarding `p5.Geometry` being potentially as fast as Java processing’s rendering of similar models I think it should be comparable.
3. Regarding GLSL shaders, I think that learning shaders is a great thing to learn, and it can definitely unlock some massive performance gains in certain circumstances. However, having a solid grasp of 3d geometry and rendering pipelines is a prerequisite. For many scenarios you’ll want to transition to using custom `p5.Geometry` regardless, even if you then use shaders as well.

Unfortunately learning how to utilize `p5.Geometry` is still a bit of a daunting task. I’m actively working on an in depth guide with examples which I will link to when I finish it, but I cannot promise how soon that will be. If you are interested in being able to use high performance 3d graphics with p5.js I would also suggest you support this github feature request that I supported via comment or reaction. This feature request covers making it possible to create `p5.Geometry` using the existing p5.js 3d primitive and drawShape/endShape functions such that the existing functions can be used to create geometry that can be rendered with high performance.

2 Likes

Other notable shape features p5js lacks compared to Processing’s Java Mode is loadShape() (SVG & OBJ files) & createShape() (reusable geometry):

For easily transitioning Processing flavors to p5js those 2 functions above are crucial methinks.

1 Like

Thanks, @KumuPaul. I am going to experiment with the normal() function although I don’t clearly understand how this would affect the pointLight() function which is I believe a camera function that shines light over the conjunction of all objects while as I understand the normal() will reflect light perpendicularly to the surface of each shape object. I clearly need to study the light functions for better understanding because they really seem to differ from java.

The normal for a vertex determines how brightly that vertex is illuminated. If the vector from the point light to the vertex is exactly opposite the normal it will get full illumination. If the normal points away from the light it will get no illumination and be black. Different types of lights have different formulas for how the vector of the light rays is calculated. You also need to consider distance falloff for point lights.

Here’s an example which uses pointLight:

2 Likes

Thanks for pointing those out. P5.js does have `loadModel()` for loading 3D OBJ files. However the createShape capability does not exist and is what I would like to see added. I didn’t realize it supported 3D geometry in Java processing. I’ll have to take a look at that, because using that same API in p5.js might be a good way to go.

2 Likes

@KumuPaul@GoToLoop
So I spent quite some time learning to write shaders, and I made progress, but at the same time, I realize that writing the ‘Nut sketch’ as a shader will be really difficult. So I came back to try the p5.Geometry and managed to design the ‘Nut’, and it seems really fast, but I can’t get any light function work on it decently. Without these functions working first I can’t proceed. Any hint?

Code here.

Drag with mouse.

You need to define vertexNormals to get directional lighting to work. Here’s an example with some modifications to clearly demonstrate the effect:

https://editor.p5js.org/Kumu-Paul/sketches/hFpJ_8MBi

One thing I had to tweak was the order in which the vertices were listed for each face. The `computeNormals()` function expects vertices to be listed in clockwise order (as you are facing the front of the triangle). I added some debugging display for the vertices and their normals which you can reveal by pressing the shift key.

(btw can you tell me how you get the embedded preview?)

2 Likes

If you want flat lighting instead of smooth/interpolated lighting (which is really better for smooth/rounded shapes where you want the triangle edges to be invisible), then you can use an alternate lighting shader like this.

Addendum: I should have also said that it is also possible to have flat-shaded faces if faces with different normals don’t share vertices. This would mean that your vertices list would contain the same vertex multiple times, but the corresponding vertexNormals would be different.

1 Like

Thank you @KumuPaul So nice, exactly what I needed!
Here’s the code I used to embed this sketch:
You have to include some code though that lets the sketch only run when the mouse is over or something; otherwise, the whole page will have a lag.
Place mouse over sketch to run.

``````<iframe src="https://editor.p5js.org/J_Silva/full/u4Bpjjf4L"
width="500" height="550" allowfullscreen frameborder="0"
marginwidth="0" marginheight="0"></iframe>
``````

Actually, I was working on a sketch that uses *.obj files because light functions work right out of the box on these objects. I got a link to a very nice sketch of @GreenCell which changes facial expression by blending/morphing several obj files. See here. The sketch I am translating uses at least 25 different ‘nut’ forms and sizes, so making that many different obj files is too much work. Though by using this concept, blending two files, you can draw all the different forms necessary.

code

However, using P5. Geometry is even better, so I started to make a ‘Nut’ class, but I’m having difficulties changing the ‘nut’ sizes for both the ‘blend obj’s’ and ‘the P5.Geometry’ sketches. The color parameter changes but the ‘nut’ forms are all the same. I guess it has to do because I’m using

canvas.createBuffers(gid, this.blendMesh);
this.canvas.drawBuffers(gid);

If you have some time please take a look. Here’s the code that runs so far.

2 Likes

Here’s a working example of “blending” two meshes using the formulas you had:

https://editor.p5js.org/Kumu-Paul/sketches/Tfmd9vwzA

3 Likes