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) {