From 8fe600ae644f07006b9f913e879bb3df9f57281a Mon Sep 17 00:00:00 2001 From: aashu2006 Date: Mon, 22 Jun 2026 06:27:48 +0530 Subject: [PATCH 1/2] Add instances() API for primitive instancing --- src/core/p5.Renderer3D.js | 10 +++++- src/webgl/3d_primitives.js | 71 ++++++++++++++++++++++++++++++++++++++ 2 files changed, 80 insertions(+), 1 deletion(-) diff --git a/src/core/p5.Renderer3D.js b/src/core/p5.Renderer3D.js index 363773befc..c4acc17b72 100644 --- a/src/core/p5.Renderer3D.js +++ b/src/core/p5.Renderer3D.js @@ -247,6 +247,8 @@ export class Renderer3D extends Renderer { this._curShader = undefined; this.drawShapeCount = 1; + // Set by the instances() wrapper; undefined means "no instancing". + this._instanceCount = undefined; this.scratchMat3 = new Matrix(3); @@ -548,6 +550,11 @@ export class Renderer3D extends Renderer { } model(model, count = 1) { + // Use _instanceCount only when count was NOT explicitly passed. + // arguments.length distinguishes model(geom) from model(geom, 1). + if (arguments.length < 2 && this._instanceCount !== undefined) { + count = this._instanceCount; + } if (model.vertices.length > 0) { if (this.geometryBuilder) { this.geometryBuilder.addRetained(model); @@ -679,6 +686,7 @@ export class Renderer3D extends Renderer { } _drawGeometryScaled(model, scaleX, scaleY, scaleZ) { + const count = this._instanceCount || 1; let originalModelMatrix = this.states.uModelMatrix; this.states.setValue("uModelMatrix", this.states.uModelMatrix.clone()); try { @@ -687,7 +695,7 @@ export class Renderer3D extends Renderer { if (this.geometryBuilder) { this.geometryBuilder.addRetained(model); } else { - this._drawGeometry(model); + this._drawGeometry(model, { count }); } } finally { this.states.setValue("uModelMatrix", originalModelMatrix); diff --git a/src/webgl/3d_primitives.js b/src/webgl/3d_primitives.js index 0d10bfdf4b..1cb4014534 100644 --- a/src/webgl/3d_primitives.js +++ b/src/webgl/3d_primitives.js @@ -2595,6 +2595,77 @@ function primitives3D(p5, fn){ } return this._renderer.curveDetail(d); }; + + /** + * Draws `count` instances of the next 3D primitive or model using WebGL + * instanced rendering. + * + * Call a draw method on the returned object to render that primitive + * `count` times in a single draw call. Instance-specific transforms and + * attributes (position, color, etc.) are supplied through a custom shader + * that reads per-instance data from an instanced attribute buffer. + * + * @method instances + * @param {Number} count number of instances to draw. Must be a positive + * integer. + * @returns {Object} an object with methods `sphere`, `box`, `plane`, + * `ellipsoid`, `cylinder`, `cone`, `torus`, and `model`. Call one of + * these to draw `count` instances of that primitive. + * + * @example + *
+ * + * // Draw 10 spheres in a single instanced draw call. + * // A custom shader reads gl_InstanceID to offset each sphere. + * function setup() { + * createCanvas(100, 100, WEBGL); + * } + * function draw() { + * background(200); + * instances(10).sphere(20); + * } + * + *
+ */ + fn.instances = function(count) { + this._assert3d('instances'); + + if (typeof count !== 'number' || !isFinite(count) || count < 1) { + console.log( + '🌸 p5.js says: instances() requires a positive integer count.' + + ' Clamping to 1.' + ); + count = Math.max(1, Math.round(count) || 1); + } else { + count = Math.round(count); + } + + const r = this._renderer; + const p = this; + + // Each wrapped method: set _instanceCount, call the renderer method with + // the correct `this`, clear _instanceCount in finally so it never leaks. + const wrap = method => function(...args) { + r._instanceCount = count; + try { + method.apply(r, args); + } finally { + r._instanceCount = undefined; + } + return p; + }; + + return { + sphere: wrap(r.sphere), + box: wrap(r.box), + plane: wrap(r.plane), + ellipsoid: wrap(r.ellipsoid), + cylinder: wrap(r.cylinder), + cone: wrap(r.cone), + torus: wrap(r.torus), + model: wrap(r.model) + }; + }; } export default primitives3D; From 30f95e2b5e8b2b2d6e753a8c4379a4626f821f1b Mon Sep 17 00:00:00 2001 From: aashu2006 Date: Mon, 22 Jun 2026 06:47:11 +0530 Subject: [PATCH 2/2] Add tests for instances() API --- test/unit/webgl/p5.RendererGL.js | 67 +++++++++++++++++++++++++++++++- 1 file changed, 66 insertions(+), 1 deletion(-) diff --git a/test/unit/webgl/p5.RendererGL.js b/test/unit/webgl/p5.RendererGL.js index 69ccf620da..90e12e329a 100644 --- a/test/unit/webgl/p5.RendererGL.js +++ b/test/unit/webgl/p5.RendererGL.js @@ -1,4 +1,4 @@ -import { suite } from 'vitest'; +import { suite, vi } from 'vitest'; import p5 from '../../../src/app.js'; import '../../js/chai_helpers'; const toArray = typedArray => Array.from(typedArray); @@ -2527,6 +2527,71 @@ void main() { }); }); + suite('instances() API', function() { + let drawSpy; + + beforeEach(function() { + myp5.createCanvas(10, 10, myp5.WEBGL); + drawSpy = vi.spyOn(myp5._renderer, '_drawGeometry'); + }); + + afterEach(function() { + vi.restoreAllMocks(); + }); + + test('instances(5).sphere() sets correct instanceCount on draw and clears it', function() { + myp5.instances(5).sphere(10); + + expect(drawSpy).toHaveBeenCalled(); + const lastCall = drawSpy.mock.calls[drawSpy.mock.calls.length - 1]; + assert.equal(lastCall[1].count, 5); + + assert.isUndefined(myp5._renderer._instanceCount); + }); + + test('instances(5).box() sets correct instanceCount on draw and clears it', function() { + myp5.instances(5).box(10); + + expect(drawSpy).toHaveBeenCalled(); + const lastCall = drawSpy.mock.calls[drawSpy.mock.calls.length - 1]; + assert.equal(lastCall[1].count, 5); + + assert.isUndefined(myp5._renderer._instanceCount); + }); + + test('instances(5).model(geom) uses instances count', function() { + const geom = new p5.Geometry(); + geom.gid = 'instances_model_test'; + geom.vertices.push(myp5.createVector(0, 0, 0)); + geom.vertices.push(myp5.createVector(1, 0, 0)); + geom.vertices.push(myp5.createVector(1, 1, 0)); + + myp5.instances(5).model(geom); + + expect(drawSpy).toHaveBeenCalled(); + const lastCall = drawSpy.mock.calls[drawSpy.mock.calls.length - 1]; + assert.equal(lastCall[1].count, 5); + + assert.isUndefined(myp5._renderer._instanceCount); + }); + + test('instances(10).model(geom, 1) has explicit-count precedence', function() { + const geom = new p5.Geometry(); + geom.gid = 'instances_precedence_test'; + geom.vertices.push(myp5.createVector(0, 0, 0)); + geom.vertices.push(myp5.createVector(1, 0, 0)); + geom.vertices.push(myp5.createVector(1, 1, 0)); + + myp5.instances(10).model(geom, 1); + + expect(drawSpy).toHaveBeenCalled(); + const lastCall = drawSpy.mock.calls[drawSpy.mock.calls.length - 1]; + assert.equal(lastCall[1].count, 1); + + assert.isUndefined(myp5._renderer._instanceCount); + }); + }); + suite('clip()', function() { //let myp5; function getClippedPixels(mode, mask) {