Skip to content

Commit

Permalink
Merge pull request #6276 from RandomGamingDev/instancing
Browse files Browse the repository at this point in the history
Add support for webGL instancing
  • Loading branch information
davepagurek authored Aug 18, 2023
2 parents 725862a + cf20cd6 commit 226f2fe
Show file tree
Hide file tree
Showing 3 changed files with 199 additions and 11 deletions.
98 changes: 95 additions & 3 deletions src/core/shape/vertex.js
Original file line number Diff line number Diff line change
Expand Up @@ -592,11 +592,19 @@ p5.prototype.endContour = function() {
* The <a href="#/p5/endShape">endShape()</a> function is the companion to <a href="#/p5/beginShape">beginShape()</a> and may only be
* called after <a href="#/p5/beginShape">beginShape()</a>. When <a href="#/p5/endshape">endShape()</a> is called, all of the image
* data defined since the previous call to <a href="#/p5/beginShape">beginShape()</a> is written into the image
* buffer. The constant CLOSE as the value for the `mode` parameter to close
* buffer. The constant CLOSE is the value for the `mode` parameter to close
* the shape (to connect the beginning and the end).
* When using instancing with <a href="#/p5/endShape">endShape()</a> the instancing will not apply to the strokes.
* When the count parameter is used with a value greater than 1, it enables instancing for shapes built when in WEBGL mode. Instancing
* is a feature that allows the GPU to efficiently draw multiples of the same shape. It's often used for particle effects or other
* times when you need a lot of repetition. In order to take advantage of instancing, you will also need to write your own custom
* shader using the gl_InstanceID keyword. You can read more about instancing
* <a href="https://webglfundamentals.org/webgl/lessons/webgl-instanced-drawing.html">here</a> or by working from the example on this
* page.
*
* @method endShape
* @param {Constant} [mode] use CLOSE to close the shape
* @param {Integer} [count] number of times you want to draw/instance the shape (for WebGL mode).
* @chainable
* @example
* <div>
Expand All @@ -617,21 +625,105 @@ p5.prototype.endContour = function() {
* </code>
* </div>
*
* @example
* <div>
* <code>
* let fx;
* let vs = `#version 300 es
*
* precision mediump float;
*
* in vec3 aPosition;
* flat out int instanceID;
*
* uniform mat4 uModelViewMatrix;
* uniform mat4 uProjectionMatrix;
*
* void main() {
*
* // copy the instance ID to the fragment shader
* instanceID = gl_InstanceID;
* vec4 positionVec4 = vec4(aPosition, 1.0);
*
* // gl_InstanceID represents a numeric value for each instance
* // using gl_InstanceID allows us to move each instance separately
* // here we move each instance horizontally by id * 100
* float xOffset = float(gl_InstanceID) * 100.0;
*
* // apply the offset to the final position
* gl_Position = uProjectionMatrix * uModelViewMatrix * positionVec4 -
* vec4(xOffset, 0.0, 0.0, 0.0);
* }
* `;
* let fs = `#version 300 es
*
* precision mediump float;
*
* out vec4 outColor;
* flat in int instanceID;
* uniform float numInstances;
*
* void main() {
* vec4 red = vec4(1.0, 0.0, 0.0, 1.0);
* vec4 blue = vec4(0.0, 0.0, 1.0, 1.0);
*
* // Normalize the instance id
* float normId = float(instanceID) / numInstances;
*
* // Mix between two colors using the normalized instance id
* outColor = mix(red, blue, normId);
* }
* `;
*
* function setup() {
* createCanvas(400, 400, WEBGL);
* fx = createShader(vs, fs);
* }
*
* function draw() {
* background(220);
*
* // strokes aren't instanced, and are rather used for debug purposes
* shader(fx);
* fx.setUniform('numInstances', 4);
*
* beginShape();
* vertex(30, 20);
* vertex(85, 20);
* vertex(85, 75);
* vertex(30, 75);
* vertex(30, 20);
* endShape(CLOSE, 4);
*
* resetShader();
* }
* </code>
* </div>
*
* @alt
* Triangle line shape with smallest interior angle on bottom and upside-down L.
*/
p5.prototype.endShape = function(mode) {
p5.prototype.endShape = function(mode, count = 1) {
p5._validateParameters('endShape', arguments);
if (count < 1) {
console.log('🌸 p5.js says: You can not have less than one instance');
count = 1;
}

if (this._renderer.isP3D) {
this._renderer.endShape(
mode,
isCurve,
isBezier,
isQuadratic,
isContour,
shapeKind
shapeKind,
count
);
} else {
if (count !== 1) {
console.log('🌸 p5.js says: Instancing is only supported in WebGL2 mode');
}
if (vertices.length === 0) {
return this;
}
Expand Down
32 changes: 24 additions & 8 deletions src/webgl/p5.RendererGL.Immediate.js
Original file line number Diff line number Diff line change
Expand Up @@ -192,7 +192,8 @@ p5.RendererGL.prototype.endShape = function(
isBezier,
isQuadratic,
isContour,
shapeKind
shapeKind,
count = 1
) {
if (this.immediateMode.shapeMode === constants.POINTS) {
this._drawPoints(
Expand Down Expand Up @@ -228,7 +229,7 @@ p5.RendererGL.prototype.endShape = function(
!this.geometryBuilder &&
this.immediateMode.geometry.vertices.length >= 3
) {
this._drawImmediateFill();
this._drawImmediateFill(count);
}
}
if (this._doStroke) {
Expand Down Expand Up @@ -489,7 +490,7 @@ p5.RendererGL.prototype._tesselateShape = function() {
* enabling all appropriate buffers, applying color blend, and drawing the fill geometry.
* @private
*/
p5.RendererGL.prototype._drawImmediateFill = function() {
p5.RendererGL.prototype._drawImmediateFill = function(count = 1) {
const gl = this.GL;
this._useVertexColor = (this.immediateMode.geometry.vertexColors.length > 0);

Expand All @@ -505,11 +506,26 @@ p5.RendererGL.prototype._drawImmediateFill = function() {

this._applyColorBlend(this.curFillColor);

gl.drawArrays(
this.immediateMode.shapeMode,
0,
this.immediateMode.geometry.vertices.length
);
if (count === 1) {
gl.drawArrays(
this.immediateMode.shapeMode,
0,
this.immediateMode.geometry.vertices.length
);
}
else {
try {
gl.drawArraysInstanced(
this.immediateMode.shapeMode,
0,
this.immediateMode.geometry.vertices.length,
count
);
}
catch (e) {
console.log('🌸 p5.js says: Instancing is only supported in WebGL2 mode');
}
}
shader.unbindShader();
};

Expand Down
80 changes: 80 additions & 0 deletions test/unit/webgl/p5.RendererGL.js
Original file line number Diff line number Diff line change
Expand Up @@ -1870,6 +1870,86 @@ suite('p5.RendererGL', function() {
});
});

suite('instancing', function() {
test('instanced', function() {
let defShader;

const vertShader = `#version 300 es
in vec3 aPosition;
in vec2 aTexCoord;
uniform mat4 uModelViewMatrix;
uniform mat4 uProjectionMatrix;
out vec2 vTexCoord;
void main() {
vTexCoord = aTexCoord;
vec4 pos = vec4(aPosition, 1.0);
pos.x += float(gl_InstanceID);
vec4 wPos = uProjectionMatrix * uModelViewMatrix * pos;
gl_Position = wPos;
}`;

const fragShader = `#version 300 es
#ifdef GL_ES
precision mediump float;
#endif
in vec2 vTexCoord;
out vec4 fragColor;
void main() {
fragColor = vec4(1.0, 0.0, 0.0, 1.0);
}
`;

myp5.createCanvas(2, 1, myp5.WEBGL);
myp5.noStroke();
myp5.pixelDensity(1);

defShader = myp5.createShader(vertShader, fragShader);

myp5.background(0);
myp5.shader(defShader);
{
// Check to make sure that pixels are empty first
assert.deepEqual(
myp5.get(0, 0),
[0, 0, 0, 255]
);
assert.deepEqual(
myp5.get(1, 0),
[0, 0, 0, 255]
);

const siz = 1;
myp5.translate(-myp5.width / 2, -myp5.height / 2);
myp5.beginShape();
myp5.vertex(0, 0);
myp5.vertex(0, siz);
myp5.vertex(siz, siz);
myp5.vertex(siz, 0);
myp5.endShape(myp5.CLOSE, 2);

// check the pixels after instancing to make sure that they're the correct color
assert.deepEqual(
myp5.get(0, 0),
[255, 0, 0, 255]
);
assert.deepEqual(
myp5.get(1, 0),
[255, 0, 0, 255]
);
}
myp5.resetShader();
});
});

suite('clip()', function() {
//let myp5;
function getClippedPixels(mode, mask) {
Expand Down

0 comments on commit 226f2fe

Please sign in to comment.