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