By Dave Pagurek, Adam Ferriss
WebGL mode in p5.js gives you access to your computer’s graphics hardware. This can allow you to produce 3D visuals even on lower-powered mobile devices. When doing more complex tasks, however, you can sometimes end up writing code that struggles to maintain a good framerate. We’ll go over some ways to measure and debug your sketch’s performance, and offer some tips for structuring your code for success.
How well does your sketch run?
When performance drops, you will notice your draw function doesn’t run as often as it should. This can cause slow or choppy animation. When you make a change to improve performance, you will want to check to see if you made the situation noticeably better. In addition to seeing how smooth the animation feels, we can display the frame rate.
The frame rate counts the number of times draw()
is called per second. The faster your sketch runs, the higher this number will be—at least, up to the point when your screen can’t refresh any quicker. On many displays, this limit is 60. Don’t worry if your sketch doesn’t quite reach 60 frames per second: 30 frames per second will also appear smooth—feature films typically run at 24 frames per second.
To measure your sketch’s frame rate, we can create a new <p>
element on the page and update it every frame:
let frameRateP;
function setup() {
createCanvas(200, 200, WEBGL);
frameRateP = createP();
}
function draw() {
frameRateP.html(round(frameRate()));
// Put the rest of your sketch here
}
It’s normal for this value to flicker a bit. Sometimes you will want to display a moving average of the frame rate to get a better estimate. Augmenting the previous code to show a moving average looks like this:
let frameRateP;
// Over how many frames should we average the frame rate?
let numSamples = 30;
// An array to store for all the individual samples
let frameRateSamples = [];
// A running count of the sum of the array
let frameRateSum = 0;
function setup() {
createCanvas(200, 200, WEBGL);
frameRateP = createP();
}
function draw() {
let newSample = frameRate();
frameRateSamples.push(newSample);
frameRateSum += newSample;
if (frameRateSamples.length > numSamples) {
frameRateSum -= frameRateSamples.shift();
}
frameRateP.html(
round(frameRateSum / frameRateSamples.length)
);
// Put the rest of your sketch here
}
Try this!
What does it take to get the frame rate to drop? Try drawing many shapes in a for
loop. How many shapes do you need to draw before you notice a change in frame rate? Is that number different when you look at your sketch on a phone instead of a desktop or laptop?
Getting the best performance out of WebGL
Here are some best practices for WebGL sketches. Following these will start you off on good footing for performance.
Prefer p5.Framebuffer
over p5.Graphics
in WebGL Mode
In 2D mode, if you need to draw something and then use it as a texture for something else, you would reach for a p5.Graphics
object. This approach works well because much of what happens in 2D mode happens on your computer’s CPU. In WebGL mode, it will be faster to use a p5.Framebuffer
, which lives on your computer’s graphics hardware along with your WebGL sketch, minimizing data transfer.
Here’s an example of some code that uses p5.Graphics
, and an improved example using p5.Framebuffer
:
❌ Using p5.Graphics
let layer;
function setup() {
createCanvas(200, 200, WEBGL);
layer = createGraphics(200, 200);
describe(
'a rotating red cube with yellow ' +
'dots on each face'
);
}
function draw() {
let t = millis() * 0.001;
layer.background('red');
layer.noStroke();
layer.fill('yellow');
for (let i = 0; i < 30; i += 1) {
layer.circle(
map(noise(i*10, 0, t), 0, 1, 0, width),
map(noise(i*10, 100, t), 0, 1, 0, height),
20
);
}
background(255);
lights();
noStroke();
texture(layer);
rotateX(t);
rotateY(t);
box(100);
}
✅ Using p5.Framebuffer
let layer;
function setup() {
createCanvas(200, 200, WEBGL);
layer = createFramebuffer();
describe(
'a rotating red cube with yellow ' +
'dots on each face'
);
}
function draw() {
let t = millis() * 0.001;
layer.begin();
background('red');
noStroke();
fill('yellow');
for (let i = 0; i < 30; i += 1) {
circle(
map(noise(i*10, 0, t), 0, 1, -width/2, width/2),
map(noise(i*10, 100, t), 0, 1, -height/2, height/2),
20
);
}
layer.end();
background(255);
lights();
noStroke();
texture(layer);
rotateX(t);
rotateY(t);
box(100);
}
Try this!
If you were using a 2D mode p5.Graphics
, switching to a p5.Framebuffer
means using WebGL mode instead. WebGL mode can draw 2D shapes too! Try adapting a 2D sketch you’ve made in the past to WebGL mode. Remember that the point (0,0) is in the top left of the canvas in 2D mode, but in the center in WebGL mode!
Replace static p5.Graphics
with p5.Image
Sometimes, you can’t use p5.Framebuffer
—maybe because you really need to draw something in 2D mode. If you are using p5.Graphics
, but you won’t be changing its contents every frame, you can turn it into a p5.Image
by calling .get()
on it. This lets p5.js know that it doesn’t need to send new image data to a texture every frame, and it can continue to reuse the initial texture instead.
❌ Using p5.Graphics
let layer;
function setup() {
createCanvas(200, 200, WEBGL);
layer = createGraphics(200, 200);
layer.background('red');
layer.noStroke();
layer.fill('yellow');
layer.textAlign(CENTER, CENTER);
layer.textSize(50);
layer.text('Hello!', width/2, height/2);
describe(
'a rotating red cube with the word ' +
'"Hello!" in yellow on each face'
);
}
function draw() {
let t = millis() * 0.001;
background(255);
lights();
noStroke();
texture(layer);
rotateX(t);
rotateY(t);
box(100);
}
✅ Using p5.Image
let layerImg;
function setup() {
createCanvas(200, 200, WEBGL);
let layer = createGraphics(200, 200);
layer.background('red');
layer.noStroke();
layer.fill('yellow');
layer.textAlign(CENTER, CENTER);
layer.textSize(50);
layer.text('Hello!', width/2, height/2);
// Save as a static image
layerImg = layer.get();
// We no longer need the original
layer.remove();
describe(
'a rotating red cube with the word ' +
'"Hello!" in yellow on each face'
);
}
function draw() {
let t = millis() * 0.001;
background(255);
lights();
noStroke();
texture(layerImg);
rotateX(t);
rotateY(t);
box(100);
}
Try this!
Try making a slideshow of generative art images, where you use .get()
on a p5.Graphics
to create each slide!
Use p5.Geometry
for shapes that don’t change
Using beginShape()
and endShape()
lets you create interesting geometry, but it spends time processing your shapes into a form that can be rendered. Similarly, you can make interesting shapes out of many simple pieces like sphere()
and box()
, but having too many at once can slow down a sketch.
If you have shapes (or pieces of shapes) that don’t change internally over time, even if their position, scales, and orientations change, you can collapse them into a single, reusable model using buildGeometry
. This means that p5.js can avoid recalculating positions again and again every frame.
❌ Redrawing the same shape
function setup() {
createCanvas(200, 200, WEBGL);
describe('a rotating corkscrew');
}
function draw() {
background(255);
noStroke();
lights();
rotateY(millis() * 0.01);
for (let y = -100; y <= 100; y += 0.5) {
const angle = y * 0.1;
const vec = createVector(50, 0)
.rotate(angle);
push();
translate(vec.x, y, vec.y);
sphere(5);
pop();
}
}
✅ Using buildGeometry()
let shape;
function setup() {
createCanvas(200, 200, WEBGL);
shape = buildGeometry(function() {
for (let y = -100; y <= 100; y += 0.5) {
let angle = y * 0.1;
let vec = createVector(50, 0)
.rotate(angle);
push();
translate(vec.x, y, vec.y);
sphere(5);
pop();
}
});
describe('a rotating corkscrew');
}
function draw() {
background(255);
noStroke();
lights();
rotateY(millis() * 0.01);
model(shape);
}
Try this!
Try making a swarm of bugs, flock of birds, or school of fish by making a p5.Geometry
for one animal, then drawing it many times!
Prefer shaders over modifying pixels
There are many effects one can achieve that involve looking at and modifying individual pixels on the canvas. Unfortunately, modifying pixels
in your sketch involves looping through every pixel, one at a time, in JavaScript. A shader, on the other hand, can run in parallel for every pixel at once, making it a great alternative for when speed is required.
Here is an example of some noise-based dithering using pixels
, and the equivalent filter using a shader. Be sure to check out our introduction to shaders for more information about shader programming.
❌ Using pixels
function setup() {
createCanvas(200, 200, WEBGL);
pixelDensity(1);
describe('a rotating cube with stippled shading');
}
function draw() {
background(255);
const t = millis() * 0.001;
push();
rotateX(t);
rotateY(t);
noStroke();
lights();
box(100);
pop();
loadPixels();
for (let x = 0; x < width; x += 1) {
for (let y = 0; y < height; y += 1) {
let idx = (y * height + x) * 4;
let newValue;
if (pixels[idx] + noise(x, y)*50 >= 220) {
newValue = 255;
} else {
newValue = 0;
}
pixels[idx] = newValue;
pixels[idx + 1] = newValue;
pixels[idx + 2] = newValue;
}
}
updatePixels();
}
✅ Using a shader
let dither;
function setup() {
createCanvas(200, 200, WEBGL);
dither = createFilterShader(`
precision highp float;
uniform sampler2D tex0;
varying vec2 vTexCoord;
float random(vec2 p) {
vec3 p3 = fract(vec3(p.xyx) * .1031);
p3 += dot(p3, p3.yzx + 33.33);
return fract((p3.x + p3.y) * p3.z);
}
void main() {
float sample = texture2D(tex0, vTexCoord).r;
sample += random(vTexCoord*100.0) * 0.2;
// 0 if sample < 0.98; 1 otherwise
float level = step(0.98, sample);
gl_FragColor = vec4(vec3(level), 1.);
}
`);
describe('a rotating cube with stippled shading');
}
function draw() {
background(255);
let t = millis() * 0.001;
push();
rotateX(t);
rotateY(t);
noStroke();
lights();
box(100);
pop();
filter(dither);
}
Try this!
Try adapting the dithering shader into a grain shader by making it add a small random amount of color to each pixel!
Other tips
Aside from the other structural changes we’ve listed so far, you may also be able to squeeze more performance out of your sketch by tweaking some minor settings that affect how the sketch gets rendered. These may provide varying levels of improvement on different browsers, screens, and computers, so try your changes out in a variety of spots.
Pixel density
Some devices have high-resolution screens. To ensure that everything isn’t tiny on your browser, those devices will render four pixels in a 2-by-2 grid for every pixel on a lower-density display. This makes images crisp, but it means there are four times the number of pixels to draw.
If your sketches are slower on these devices, consider adding pixelDensity(1)
to only draw the number of pixels you specify in the width and height of your canvas. Your sketch may appear a bit blurry, but that could be fine depending on your sketch.
function setup() {
createCanvas(200, 200, WEBGL);
pixelDensity(1);
}
Antialiasing
Antialiasing is the process of smoothing the colors of pixels when a shape intersects only part of the pixel. This can avoid jagged lines and visible pixel grids. However, like high pixel density displays, this is done by doing extra calculations per pixel. Sometimes, when you don’t need that extra smoothness, you can turn off antialiasing to run faster.
function setup() {
createCanvas(200, 200, WEBGL);
setAttributes({ antialias: false });
}
Curve Detail
When you aren’t drawing premade shapes with model()
, things generally run faster when fewer triangles and edges are involved. One way to tune how detailed your shapes are is with curveDetail()
: the larger the value you give, the more edges and triangles are used when breaking down curves. Try using a level of detail smaller than the default value of 20.
Try this!
What level of detail do you need for the curve above to appear smooth to you? Does it change the level you need if you draw the curve larger? What about smaller?
Debugging further
If your sketch is still running slowly and you don’t know what part is causing it, it can be helpful to use your browser’s profiler. This will vary based on your browser. For more information, you can get more detailed instructions for Chrome here or for Firefox here.
In general, you start by opening up the browser’s developer tools. An easy way to do that is to right-click somewhere on the page and click “Inspect.” In the new toolbar that opens, there will be a tab labeled “Performance” that you can click on to get to the profiler. Here is what that looks like in Chrome:
Start running your sketch, then click the record button in the profiler. After a few seconds, you can click again to stop the recording. It will now look something like this:
The horizontal axis of this graph is time, showing what code is executing. The longer a block in the graph, the longer that code took to run. The vertical axis shows nested function calls. High up in the stack should be your draw()
function, and below it will be any functions called within it, and below that will be any functions called by those functions. If you zoom in with your mouse wheel so that one draw()
call takes up most of the width of the window, you can see a breakdown of how it spends its time by looking at the level right below it and how long those blocks are. The longest blocks are most likely the ones you want to try to optimize further.
Conclusion
With these tips and tools, you should be able to hit the ground running with WebGL projects that run fast. Optimization can be complicated though, so don’t worry if you get stuck! Consider joining the p5.js Discord server where you will be able to ask for suggestions.
संबंधित संदर्भ
createFramebuffer
Creates and a new p5.Framebuffer object.
p5.Framebuffer
A class to describe a high-performance drawing surface for textures.
createFilterShader
filter() फ़ंक्शन के साथ उपयोग करने के लिए एक p5.Shader ऑब्जेक्ट बनाता है। createFilterShader() createShader() की तरह काम करता है, लेकिन एक डिफ़ॉल्ट वर्टेक्स शेडर शामिल होता है। createFilterShader() का इरादा filter() के साथ उपयोग करने के लिए है जिससे किसी कैनवास के कंटेंट्स को फ़िल्टर किया जा सके। एक फ़िल्टर शेडर पूरे कैनवास पर लागू होगा, न कि केवल p5.Geometry ऑब्जेक्ट्स पर। पैरामीटर, fragSrc, फ्रैग्मेंट शेडर को सेट करता है। यह एक स्ट्रिंग है जो GLSL में लिखे फ्रैग्मेंट शेडर प्रोग्राम को कंटेन करता है। बने हुए p5.Shader ऑब्जेक्ट में कुछ यूनिफॉर्म्स हैं जो सेट किए जा सकते हैं: sampler2D tex0, जो कैनवास कंटेंट्स को एक टेक्सचर के रूप में कंटेन करता है। vec2 canvasSize, जो कैनवास की चौड़ाई और ऊंचाई है, पिक्सल घनत्व को छोड़कर। vec2 texelSize, जो एक भौतिक पिक्सल का आकार है जिसमें पिक्सल घनत्व शामिल है। यह 1.0 / (width * density) के लिए पिक्सल चौड़ाई और 1.0 / (height * density) के लिए पिक्सल ऊंचाई के रूप में गणना की जाती है। बने हुए p5.Shader में varying vec2 vTexCoord भी प्रदान किया जाता है, जिसमें 0 और 1 के बीच के मूल्य होते हैं। vTexCoord बताता है कि पिक्सल कैनवास पर कहाँ खींचा जाएगा। फ़िल्टर और शेडर के बारे में अधिक जानकारी के लिए, Adam Ferriss के शेडर उदाहरण रिपो या शेडर का परिचय ट्यूटोरियल देखें। .
p5.Shader
एक शेडर प्रोग्राम का वर्णन करने वाली एक क्लास। प्रत्येक p5.Shader ऑब्जेक्ट में एक शेडर प्रोग्राम होता है जो ग्राफिक्स प्रोसेसिंग यूनिट (GPU) पर चलता है। शेडर एक साथ कई पिक्सल या वर्टेक्स को प्रोसेस कर सकते हैं, जिससे वे कई ग्राफिक्स कार्यों के लिए तेज हो जाते हैं। वे GLSL नामक भाषा में लिखे जाते हैं और स्केच में शेष कोड के साथ चलते हैं। एक शेडर प्रोग्राम दो फ़ाइलों से बना होता है, एक वर्टेक्स शेडर और एक फ्रैग्मेंट शेडर। वर्टेक्स शेडर 3D ज्यामिति को स्क्रीन पर कहां खींचता है और फ्रैग्मेंट शेडर रंग को प्रभावित करता है। एक बार p5.Shader ऑब्जेक्ट बना लिया जाता है, तो इसका उपयोग shader() फ़ंक्शन के साथ किया जा सकता है, जैसे shader(myShader)। ध्यान दें: createShader(), createFilterShader() और loadShader() इस क्लास के एक इंस्टेंस बनाने के लिए अनुशंसित तरीके हैं। .