diff --git a/src/core/shape/vertex.js b/src/core/shape/vertex.js
index a51cd97b5c..acf96ffcc7 100644
--- a/src/core/shape/vertex.js
+++ b/src/core/shape/vertex.js
@@ -592,11 +592,19 @@ p5.prototype.endContour = function() {
* The endShape() function is the companion to beginShape() and may only be
* called after beginShape(). When endShape() is called, all of the image
* data defined since the previous call to beginShape() 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 endShape() 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
+ * here 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
*
@@ -617,11 +625,91 @@ p5.prototype.endContour = function() {
*
*
*
+ * @example
+ *
+ *
+ * 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();
+ * }
+ *
+ *
+ *
* @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,
@@ -629,9 +717,13 @@ p5.prototype.endShape = function(mode) {
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;
}
diff --git a/src/webgl/p5.RendererGL.Immediate.js b/src/webgl/p5.RendererGL.Immediate.js
index 240fa5807e..30f1bf8801 100644
--- a/src/webgl/p5.RendererGL.Immediate.js
+++ b/src/webgl/p5.RendererGL.Immediate.js
@@ -192,7 +192,8 @@ p5.RendererGL.prototype.endShape = function(
isBezier,
isQuadratic,
isContour,
- shapeKind
+ shapeKind,
+ count = 1
) {
if (this.immediateMode.shapeMode === constants.POINTS) {
this._drawPoints(
@@ -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) {
@@ -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);
@@ -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();
};
diff --git a/test/unit/webgl/p5.RendererGL.js b/test/unit/webgl/p5.RendererGL.js
index 7c78cedfd4..4ff0648141 100644
--- a/test/unit/webgl/p5.RendererGL.js
+++ b/test/unit/webgl/p5.RendererGL.js
@@ -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) {