contributor_docs/webgl_contribution_guide.md
If you're reading this page, you're probably interested in helping work on WebGL mode. Thank you, we're grateful for your help! This page exists to help explain how we structure WebGL contributions and to offer some tips for making changes.
We organize open issues in a GitHub Project, where we divide them up into a few types:
Everything related to WebGL is in the src/webgl subdirectory. Within that directory, top-level p5.js functions are split into files based on subject area: commands to set light go in lighting.js; commands to set materials go in materials.js.
When implementing user-facing classes, we generally try to have one file per class. These files may occasionally have a few other internal utility classes. For example, p5.Framebuffer.js includes the class p5.Framebuffer, and also additionally consists of a few framebuffer-specific subclasses of other main classes. Further framebuffer-specific subclasses can go in this file, too.
p5.RendererGL is a large class that handles a lot of behavior. For this reason, rather than having one large class file, its functionality is split into many files based on what subject area it is. Here is a description of the files we split p5.RendererGL across, and what to put in each one:
p5.RendererGL.jsInitialization and core functionality.
p5.RendererGL.Immediate.jsFunctionality related to immediate mode drawing (shapes that will not get stored and reused, such as beginShape() and endShape())
p5.RendererGL.Retained.jsFunctionality related to retained mode drawing (shapes that have been stored for reuse, such as sphere())
material.jsManagement of blend modes
3d_primitives.jsUser-facing functions that draw shapes, such as triangle(). These define the geometry of the shapes. The rendering of those shapes then happens in p5.RendererGL.Retained.js or p5.RendererGL.Immediate.js, treating the geometry input as a generic shape.
Text.jsFunctionality and utility classes for text rendering.
There are a lot of ways one can use the functions in p5.js. It's hard to manually verify all of it, so we add unit tests where we can. That way, when we make new changes, we can be more confident that we didn't break anything if all the unit tests still pass.
When adding a new test, if the feature is something that also works in 2D mode, one of the best ways to check for consistency is to check that the resulting pixels are the same in both modes. Here's one example of that in a unit test:
test('coplanar strokes match 2D', function() {
const getColors = function(mode) {
myp5.createCanvas(20, 20, mode);
myp5.pixelDensity(1);
myp5.background(255);
myp5.strokeCap(myp5.SQUARE);
myp5.strokeJoin(myp5.MITER);
if (mode === myp5.WEBGL) {
myp5.translate(-myp5.width/2, -myp5.height/2);
}
myp5.stroke('black');
myp5.strokeWeight(4);
myp5.fill('red');
myp5.rect(10, 10, 15, 15);
myp5.fill('blue');
myp5.rect(0, 0, 15, 15);
myp5.loadPixels();
return [...myp5.pixels];
};
assert.deepEqual(getColors(myp5.P2D), getColors(myp5.WEBGL));
});
This doesn't always work because you can't turn off antialiasing in 2D mode, and antialiasing in WebGL mode will often be slightly different. It can work for straight lines in the x and y axes, though!
If a feature is WebGL-only, rather than comparing pixels to 2D mode, we often check a few pixels to ensure their color is what we expect. One day, we might turn this into a more robust system that compares against full image snapshots of our expected results rather than a few pixels, but for now, here is an example of a pixel color check:
test('color interpolation', function() {
const renderer = myp5.createCanvas(256, 256, myp5.WEBGL);
// upper color: (200, 0, 0, 255);
// lower color: (0, 0, 200, 255);
// expected center color: (100, 0, 100, 255);
myp5.beginShape();
myp5.fill(200, 0, 0);
myp5.vertex(-128, -128);
myp5.fill(200, 0, 0);
myp5.vertex(128, -128);
myp5.fill(0, 0, 200);
myp5.vertex(128, 128);
myp5.fill(0, 0, 200);
myp5.vertex(-128, 128);
myp5.endShape(myp5.CLOSE);
assert.equal(renderer._useVertexColor, true);
assert.deepEqual(myp5.get(128, 128), [100, 0, 100, 255]);
});
While not the #1 concern of p5.js, we try to make sure changes don't cause a large hit to performance. Typically, this is done by creating two test sketches: one with your change and one without the change. We then compare the frame rates of both.
Some advice on how to measure performance:
p5.disableFriendlyErrors = true at the top of your sketch (or just test p5.min.js, which does not include the friendly error system)let frameRateP;
let avgFrameRates = [];
let frameRateSum = 0;
const numSamples = 30;
function setup() {
// ...
frameRateP = createP();
frameRateP.position(0, 0);
}
function draw() {
// ...
const rate = frameRate() / numSamples;
avgFrameRates.push(rate);
frameRateSum += rate;
if (avgFrameRates.length > numSamples) {
frameRateSum -= avgFrameRates.shift();
}
frameRateP.html(round(frameRateSum) + ' avg fps');
}
Here are cases we try to test since they stress different parts of the rendering pipeline:
line() called many times in a for loop)