diff --git a/src/core/p5.Renderer3D.js b/src/core/p5.Renderer3D.js
index 363773befc..cebf29a6bd 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);
@@ -530,7 +532,7 @@ export class Renderer3D extends Renderer {
}
endShape(mode, count) {
- this.drawShapeCount = count;
+ this.drawShapeCount = count ?? this._instanceCount ?? 1;
super.endShape(mode, count);
}
@@ -547,7 +549,8 @@ export class Renderer3D extends Renderer {
this.updateShapeVertexProperties();
}
- model(model, count = 1) {
+ model(model, count) {
+ count = count ?? this._instanceCount ?? 1;
if (model.vertices.length > 0) {
if (this.geometryBuilder) {
this.geometryBuilder.addRetained(model);
@@ -679,6 +682,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 +691,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);
@@ -2034,61 +2038,55 @@ function renderer3D(p5, fn) {
* item is accessed by index, and its properties are available by name.
*
* ```js example
- * let instanceData;
- * let instancesShader;
- * let instance;
- * let count = 5;
- *
- * async function setup() {
- * await createCanvas(200, 200, WEBGPU);
- *
- * let data = [];
- * for (let i = 0; i < count; i++) {
- * data.push({
- * position: createVector(
- * random(-1, 1) * width / 2,
- * random(-1, 1) * height / 2,
- * 0,
- * ),
- * color: color(
- * random(255),
- * random(255),
- * random(255)
- * )
- * });
- * }
- * instanceData = createStorage(data);
- * instance = buildGeometry(drawInstance);
- * instancesShader = buildMaterialShader(drawInstances);
- * describe('Five spheres at random positions, each a different random color.');
- * }
- *
- * function drawInstance() {
- * sphere(15);
- * }
- *
- * function drawInstances() {
- * let data = uniformStorage(instanceData);
- * let itemColor = sharedVec4();
- *
- * worldInputs.begin();
- * let item = data[instanceID()];
- * itemColor = item.color;
- * worldInputs.position += item.position;
- * worldInputs.end();
- *
- * finalColor.begin();
- * finalColor.set(itemColor);
- * finalColor.end();
- * }
- *
- * function draw() {
- * background(220);
- * lights();
- * noStroke();
- * shader(instancesShader);
- * model(instance, count);
- * }
+ * let instanceData;
+ * let instancesShader;
+ * let count = 5;
+ *
+ * async function setup() {
+ * await createCanvas(200, 200, WEBGPU);
+ *
+ * let data = [];
+ * for (let i = 0; i < count; i++) {
+ * data.push({
+ * position: createVector(
+ * random(-1, 1) * width / 2,
+ * random(-1, 1) * height / 2,
+ * 0,
+ * ),
+ * color: color(
+ * random(255),
+ * random(255),
+ * random(255)
+ * )
+ * });
+ * }
+ * instanceData = createStorage(data);
+ * instancesShader = buildMaterialShader(drawInstances);
+ * describe('Five spheres at random positions, each a different random color.');
+ * }
+ *
+ * function drawInstances() {
+ * let data = uniformStorage(instanceData);
+ * let itemColor = sharedVec4();
+ *
+ * worldInputs.begin();
+ * let item = data[instanceIndex];
+ * itemColor = item.color;
+ * worldInputs.position += item.position;
+ * worldInputs.end();
+ *
+ * finalColor.begin();
+ * finalColor.set(itemColor);
+ * finalColor.end();
+ * }
+ *
+ * function draw() {
+ * background(220);
+ * lights();
+ * noStroke();
+ * shader(instancesShader);
+ * instances(count).sphere(15);
+ * }
* ```
*
* You can also store a plain list of numbers by passing an array of numbers.
@@ -2242,15 +2240,14 @@ function renderer3D(p5, fn) {
* initial data. Connect your iteration function to the storage by passing the storage
* into `uniformStorage`.
*
- * Often, compute shaders are paired with `model(myGeometry, count)`
+ * Often, compute shaders are paired with `instances(count)`
* to draw one instance per object in the storage, and a shader that uses
- * `instanceID()` to position each instance.
+ * `instanceIndex` to position each instance.
*
* ```js example
* let particles;
* let computeShader;
* let displayShader;
- * let instance;
* const numParticles = 100;
*
* async function setup() {
@@ -2258,7 +2255,6 @@ function renderer3D(p5, fn) {
* particles = createStorage(makeParticles(width / 2, height / 2));
* computeShader = buildComputeShader(simulate);
* displayShader = buildMaterialShader(display);
- * instance = buildGeometry(drawParticle);
* describe('100 orange particles shooting outward.');
* }
*
@@ -2275,10 +2271,6 @@ function renderer3D(p5, fn) {
* return data;
* }
*
- * function drawParticle() {
- * sphere(2);
- * }
- *
* function simulate() {
* let data = uniformStorage(particles);
* let idx = index.x;
@@ -2288,7 +2280,7 @@ function renderer3D(p5, fn) {
* function display() {
* let data = uniformStorage(particles);
* worldInputs.begin();
- * let pos = data[instanceID()].position;
+ * let pos = data[instanceIndex].position;
* worldInputs.position.xy += pos - [width / 2, height / 2];
* worldInputs.end();
* }
@@ -2302,7 +2294,7 @@ function renderer3D(p5, fn) {
* noStroke();
* fill(255, 200, 50);
* shader(displayShader);
- * model(instance, numParticles);
+ * instances(numParticles).sphere(2);
* }
* ```
*
@@ -2310,7 +2302,6 @@ function renderer3D(p5, fn) {
* let particles;
* let computeShader;
* let displayShader;
- * let instance;
* const numParticles = 50;
*
* async function setup() {
@@ -2333,14 +2324,9 @@ function renderer3D(p5, fn) {
*
* computeShader = buildComputeShader(simulate);
* displayShader = buildMaterialShader(display);
- * instance = buildGeometry(drawParticle);
* describe('50 white spheres bouncing around the canvas.');
* }
*
- * function drawParticle() {
- * sphere(3);
- * }
- *
* function simulate() {
* let r = 3;
* let data = uniformStorage(particles);
@@ -2363,7 +2349,7 @@ function renderer3D(p5, fn) {
* function display() {
* let data = uniformStorage(particles);
* worldInputs.begin();
- * let pos = data[instanceID()].position;
+ * let pos = data[instanceIndex].position;
* worldInputs.position.xy += pos;
* worldInputs.end();
* }
@@ -2375,7 +2361,7 @@ function renderer3D(p5, fn) {
* fill(255);
* lights();
* shader(displayShader);
- * model(instance, numParticles);
+ * instances(numParticles).sphere(3);
* }
* ```
*
@@ -2412,7 +2398,6 @@ function renderer3D(p5, fn) {
* let particles;
* let computeShader;
* let displayShader;
- * let instance;
* const numParticles = 50;
*
* async function setup() {
@@ -2435,14 +2420,9 @@ function renderer3D(p5, fn) {
*
* computeShader = buildComputeShader(simulate);
* displayShader = buildMaterialShader(display);
- * instance = buildGeometry(drawParticle);
* describe('50 white spheres bouncing around the canvas.');
* }
*
- * function drawParticle() {
- * sphere(3);
- * }
- *
* function simulate() {
* let r = 3;
* let data = uniformStorage(particles);
@@ -2465,7 +2445,7 @@ function renderer3D(p5, fn) {
* function display() {
* let data = uniformStorage(particles);
* worldInputs.begin();
- * let pos = data[instanceID()].position;
+ * let pos = data[instanceIndex].position;
* worldInputs.position.xy += pos;
* worldInputs.end();
* }
@@ -2477,7 +2457,7 @@ function renderer3D(p5, fn) {
* fill(255);
* lights();
* shader(displayShader);
- * model(instance, numParticles);
+ * instances(numParticles).sphere(3);
* }
* ```
*
diff --git a/src/shape/vertex.js b/src/shape/vertex.js
index 0f41a6c043..7222370550 100644
--- a/src/shape/vertex.js
+++ b/src/shape/vertex.js
@@ -833,11 +833,20 @@ function vertex(p5, fn){
* describe('A row of four squares. Their colors transition from purple on the left to red on the right');
* }
*/
- fn.endShape = function(mode, count = 1) {
+ fn.endShape = function(mode, count) {
// p5._validateParameters('endShape', arguments);
- if (count < 1) {
- console.log('🌸 p5.js says: You can not have less than one instance');
- count = 1;
+ if (typeof mode === 'number') {
+ count = mode;
+ mode = undefined;
+ }
+
+ if (count !== undefined) {
+ if (count < 1) {
+ console.log('🌸 p5.js says: You can not have less than one instance');
+ count = 1;
+ } else {
+ count = Math.round(count);
+ }
}
this._renderer.endShape(mode, count);
diff --git a/src/webgl/3d_primitives.js b/src/webgl/3d_primitives.js
index 0d10bfdf4b..7531d9bd7f 100644
--- a/src/webgl/3d_primitives.js
+++ b/src/webgl/3d_primitives.js
@@ -2595,6 +2595,129 @@ function primitives3D(p5, fn){
}
return this._renderer.curveDetail(d);
};
+
+ /**
+ * @typedef {Object} InstancesWrapper
+ * @property {function(radius: Number=, detailX: Number=, detailY: Number=): undefined} sphere
+ * @property {function(width: Number=, height: Number=, depth: Number=, detailX: Number=, detailY: Number=): undefined} box
+ * @property {function(width: Number=, height: Number=, detailX: Number=, detailY: Number=): undefined} plane
+ * @property {function(radiusX: Number=, radiusY: Number=, radiusZ: Number=, detailX: Number=, detailY: Number=): undefined} ellipsoid
+ * @property {function(radius: Number=, height: Number=, detailX: Number=, detailY: Number=, bottomCap: Boolean=, topCap: Boolean=): undefined} cylinder
+ * @property {function(radius: Number=, height: Number=, detailX: Number=, detailY: Number=, cap: Boolean=): undefined} cone
+ * @property {function(radius: Number=, tubeRadius: Number=, detailX: Number=, detailY: Number=): undefined} torus
+ * @property {function(x1: Number, y1: Number, x2: Number, y2: Number, x3: Number, y3: Number): undefined} triangle
+ * @property {function(x: Number, y: Number, w: Number, h: Number=, detailX: Number=, detailY: Number=): undefined} rect
+ * @property {function(x1: Number, y1: Number, z1: Number=, x2: Number, y2: Number, z2: Number=, x3: Number, y3: Number, z3: Number=, x4: Number, y4: Number, z4: Number=, detailX: Number=, detailY: Number=): undefined} quad
+ * @property {function(x: Number, y: Number, w: Number, h: Number=, detail: Number=): undefined} ellipse
+ * @property {function(x: Number, y: Number, w: Number, h: Number, start: Number, stop: Number, mode: String=, detail: Number=): undefined} arc
+ * @property {function(model: p5.Geometry, count: Number=): undefined} model
+ * @property {function(x1: Number, y1: Number, z1: Number|Number, x2: Number=, y2: Number=, z2: Number=): undefined} line
+ * @property {function(x: Number, y: Number, z: Number=): undefined} point
+ * @property {function(x1: Number, y1: Number, z1: Number|Number, x2: Number=, y2: Number=, z2: Number=, x3: Number=, y3: Number=, z3: Number=, x4: Number=, y4: Number=, z4: Number=): undefined} bezier
+ * @property {function(x1: Number, y1: Number, z1: Number|Number, x2: Number=, y2: Number=, z2: Number=, x3: Number=, y3: Number=, z3: Number=, x4: Number=, y4: Number=, z4: Number=): undefined} spline
+ */
+
+ /**
+ * 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 {p5.InstancesWrapper} an object with methods `sphere`, `box`, `plane`,
+ * `ellipsoid`, `cylinder`, `cone`, `torus`, `triangle`, `rect`, `quad`,
+ * `ellipse`, `arc`, `model`, `line`, `point`, `bezier`, and `spline`. Call one of
+ * these to draw `count` instances of that primitive.
+ *
+ * @example
+ *
+ *
+ * let myShader;
+ * let count = 5;
+ *
+ * function setup() {
+ * createCanvas(200, 200, WEBGL);
+ * myShader = buildMaterialShader(drawSpaced);
+ * describe('Five red spheres arranged in a horizontal line.');
+ * }
+ *
+ * function drawSpaced() {
+ * worldInputs.begin();
+ * // Spread spheres evenly across the canvas based on their index
+ * let spacing = width / count;
+ * worldInputs.position.x +=
+ * (instanceIndex - (count - 1) / 2) * spacing;
+ * worldInputs.end();
+ * }
+ *
+ * function draw() {
+ * background(220);
+ * lights();
+ * noStroke();
+ * fill('red');
+ * shader(myShader);
+ * instances(count).sphere(15);
+ * }
+ *
+ *
+ */
+ fn.instances = function(count) {
+ this._assert3d('instances');
+
+ if (typeof count !== 'number' || !isFinite(count) || count < 1) {
+ p5._friendlyError(
+ 'instances() requires a positive integer count. Clamping to 1.',
+ 'instances'
+ );
+ count = 1;
+ } else {
+ count = Math.round(count);
+ }
+
+ const r = this._renderer;
+
+ // Each wrapped method: set _instanceCount, call the method with
+ // the correct context, clear _instanceCount in finally so it never leaks.
+ const wrap = (method, ctx = r) => function(...args) {
+ r._instanceCount = count;
+ try {
+ method.apply(ctx, args);
+ } finally {
+ r._instanceCount = undefined;
+ }
+ };
+
+ const result = {
+ 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),
+ triangle: wrap(this.triangle, this),
+ rect: wrap(this.rect, this),
+ quad: wrap(this.quad, this),
+ ellipse: wrap(this.ellipse, this),
+ arc: wrap(this.arc, this),
+ model: wrap(r.model),
+ line: wrap(this.line, this),
+ point: wrap(this.point, this),
+ bezier: wrap(this.bezier, this),
+ spline: wrap(this.spline, this)
+ };
+
+ if (typeof this.curve === 'function') {
+ result.curve = wrap(this.curve, this);
+ }
+
+ return result;
+ };
}
export default primitives3D;
diff --git a/src/webgl/loading.js b/src/webgl/loading.js
index 5edd0d0791..aafc095e80 100755
--- a/src/webgl/loading.js
+++ b/src/webgl/loading.js
@@ -1077,23 +1077,17 @@ function loading(p5, fn){
* }
* ```
*
- * Multiple instances can be drawn at once with `model(geometry, count)`. On its own,
+ * Multiple instances can be drawn at once with `instances(count)`. On its own,
* all the instances get drawn to the same spot, but you can use
- * `instanceID()` inside of a shader to handle each instance.
+ * `instanceIndex` inside of a shader to handle each instance.
* At large counts, this often runs faster than using a `for` loop.
*
* ```js example
* let instancesShader;
- * let instance;
* let count = 5;
*
- * function drawInstance() {
- * sphere(15);
- * }
- *
* function setup() {
* createCanvas(200, 200, WEBGL);
- * instance = buildGeometry(drawInstance);
* instancesShader = buildMaterialShader(drawSpaced);
* }
*
@@ -1102,7 +1096,7 @@ function loading(p5, fn){
* // Spread spheres evenly across the canvas based on their index
* let spacing = width / count;
* worldInputs.position.x +=
- * (instanceID() - (count - 1) / 2) * spacing;
+ * (instanceIndex - (count - 1) / 2) * spacing;
* worldInputs.end();
* }
*
@@ -1112,7 +1106,7 @@ function loading(p5, fn){
* noStroke();
* fill('red');
* shader(instancesShader);
- * model(instance, count);
+ * instances(count).sphere(15);
* }
* ```
*
diff --git a/src/webgpu/p5.RendererWebGPU.js b/src/webgpu/p5.RendererWebGPU.js
index 3b909230b7..87ec15f8b4 100644
--- a/src/webgpu/p5.RendererWebGPU.js
+++ b/src/webgpu/p5.RendererWebGPU.js
@@ -58,7 +58,6 @@ function rendererWebGPU(p5, fn) {
* let particles;
* let computeShader;
* let displayShader;
- * let instance;
* const numParticles = 100;
*
* async function setup() {
@@ -66,7 +65,6 @@ function rendererWebGPU(p5, fn) {
* particles = createStorage(makeParticles(width / 2, height / 2));
* computeShader = buildComputeShader(simulate);
* displayShader = buildMaterialShader(display);
- * instance = buildGeometry(drawParticle);
* describe('100 orange particles shooting outward.');
* }
*
@@ -83,10 +81,6 @@ function rendererWebGPU(p5, fn) {
* return data;
* }
*
- * function drawParticle() {
- * sphere(2);
- * }
- *
* function simulate() {
* let data = uniformStorage(particles);
* let idx = index.x;
@@ -96,7 +90,7 @@ function rendererWebGPU(p5, fn) {
* function display() {
* let data = uniformStorage(particles);
* worldInputs.begin();
- * let pos = data[instanceID()].position;
+ * let pos = data[instanceIndex].position;
* worldInputs.position.xy += pos - [width / 2, height / 2];
* worldInputs.end();
* }
@@ -110,7 +104,7 @@ function rendererWebGPU(p5, fn) {
* noStroke();
* fill(255, 200, 50);
* shader(displayShader);
- * model(instance, numParticles);
+ * instances(numParticles).sphere(2);
* }
* ```
*
diff --git a/test/unit/visual/cases/webgl.js b/test/unit/visual/cases/webgl.js
index 972d9bc5f0..56406f8d3f 100644
--- a/test/unit/visual/cases/webgl.js
+++ b/test/unit/visual/cases/webgl.js
@@ -1101,6 +1101,64 @@ visualSuite('WebGL', function() {
p5.model(obj, numInstances);
screenshot();
});
+
+ visualTest('instances() API draws multiple spaced primitives', (p5, screenshot) => {
+ p5.createCanvas(50, 50, p5.WEBGL);
+ const count = 5;
+ const shader = p5.baseMaterialShader().modify(() => {
+ p5.getWorldInputs((inputs) => {
+ let spacing = p5.width / count;
+ inputs.position.x += (p5.instanceIndex - (count - 1) / 2.0) * spacing;
+ return inputs;
+ });
+ }, { p5, count });
+ p5.background(220);
+ p5.lights();
+ p5.noStroke();
+ p5.fill('red');
+ p5.shader(shader);
+ p5.instances(count).sphere(7);
+ screenshot();
+ });
+
+ visualTest('instances() API draws multiple spaced 2D rects', (p5, screenshot) => {
+ p5.createCanvas(50, 50, p5.WEBGL);
+ const count = 3;
+ const shader = p5.baseMaterialShader().modify(() => {
+ p5.getWorldInputs(inputs => {
+ let spacing = p5.width / count;
+ inputs.position.x += (p5.instanceIndex - (count - 1) / 2.0) * spacing;
+ return inputs;
+ });
+ }, { p5, count });
+ p5.background(220);
+ p5.noStroke();
+ p5.fill('blue');
+ p5.shader(shader);
+ p5.instances(count).rect(-5, -5, 10, 10);
+ screenshot();
+ });
+
+ visualTest('instances() API draws immediate-mode shape primitives', (p5, screenshot) => {
+ p5.createCanvas(50, 50, p5.WEBGL);
+ const count = 4;
+ const shader = p5.baseMaterialShader().modify(() => {
+ p5.getWorldInputs(inputs => {
+ let spacing = p5.width / count;
+ inputs.position.x += (p5.instanceIndex - (count - 1) / 2.0) * spacing;
+ return inputs;
+ });
+ }, { p5, count });
+ p5.background(220);
+ p5.stroke(0);
+ p5.strokeWeight(2);
+ p5.shader(shader);
+ p5.instances(count).line(0, -15, 0, 0, 15, 0);
+ p5.instances(count).point(0, -10, 0);
+ p5.instances(count).bezier(-5, -5, 0, -2, 5, 0, 2, -5, 0, 5, 5, 0);
+ p5.instances(count).spline(-5, 5, 0, -2, -5, 0, 2, 5, 0, 5, -5, 0);
+ screenshot();
+ });
});
visualSuite('p5.strands', () => {
diff --git a/test/unit/visual/cases/webgpu.js b/test/unit/visual/cases/webgpu.js
index 4fc098850b..96972b766e 100644
--- a/test/unit/visual/cases/webgpu.js
+++ b/test/unit/visual/cases/webgpu.js
@@ -326,6 +326,64 @@ visualSuite("WebGPU", function () {
await screenshot();
});
+ visualTest('instances() API draws multiple spaced primitives (WebGPU)', async function(p5, screenshot) {
+ await p5.createCanvas(50, 50, p5.WEBGPU);
+ const count = 5;
+ const shader = p5.baseMaterialShader().modify(() => {
+ p5.getWorldInputs((inputs) => {
+ let spacing = p5.width / count;
+ inputs.position.x += (p5.instanceIndex - (count - 1) / 2.0) * spacing;
+ return inputs;
+ });
+ }, { p5, count });
+ p5.background(220);
+ p5.lights();
+ p5.noStroke();
+ p5.fill('red');
+ p5.shader(shader);
+ p5.instances(count).sphere(7);
+ await screenshot();
+ });
+
+ visualTest('instances() API draws multiple spaced 2D rects (WebGPU)', async function(p5, screenshot) {
+ await p5.createCanvas(50, 50, p5.WEBGPU);
+ const count = 3;
+ const shader = p5.baseMaterialShader().modify(() => {
+ p5.getWorldInputs(inputs => {
+ let spacing = p5.width / count;
+ inputs.position.x += (p5.instanceIndex - (count - 1) / 2.0) * spacing;
+ return inputs;
+ });
+ }, { p5, count });
+ p5.background(220);
+ p5.noStroke();
+ p5.fill('blue');
+ p5.shader(shader);
+ p5.instances(count).rect(-5, -5, 10, 10);
+ await screenshot();
+ });
+
+ visualTest('instances() API draws immediate-mode shape primitives (WebGPU)', async function(p5, screenshot) {
+ await p5.createCanvas(50, 50, p5.WEBGPU);
+ const count = 4;
+ const shader = p5.baseMaterialShader().modify(() => {
+ p5.getWorldInputs(inputs => {
+ let spacing = p5.width / count;
+ inputs.position.x += (p5.instanceIndex - (count - 1) / 2.0) * spacing;
+ return inputs;
+ });
+ }, { p5, count });
+ p5.background(220);
+ p5.stroke(0);
+ p5.strokeWeight(2);
+ p5.shader(shader);
+ p5.instances(count).line(0, -15, 0, 0, 15, 0);
+ p5.instances(count).point(0, -10, 0);
+ p5.instances(count).bezier(-5, -5, 0, -2, 5, 0, 2, -5, 0, 5, 5, 0);
+ p5.instances(count).spline(-5, 5, 0, -2, -5, 0, 2, 5, 0, 5, -5, 0);
+ await screenshot();
+ });
+
visualTest('random() colors a basic shader (WebGPU)', async function(p5, screenshot) {
await p5.createCanvas(50, 50, p5.WEBGPU);
const shader = p5.baseColorShader().modify(() => {
@@ -382,7 +440,7 @@ visualSuite("WebGPU", function () {
p5.shader(shader);
p5.plane(20, 20);
await screenshot();
- }, { focus: true });
+ });
});
visualTest('randomGaussian() colors a basic shader (WebGPU)', async function(p5, screenshot) {
diff --git a/test/unit/visual/screenshots/WebGL/instanced randering/instances() API draws immediate-mode shape primitives/000.png b/test/unit/visual/screenshots/WebGL/instanced randering/instances() API draws immediate-mode shape primitives/000.png
new file mode 100644
index 0000000000..8ec09c70f3
Binary files /dev/null and b/test/unit/visual/screenshots/WebGL/instanced randering/instances() API draws immediate-mode shape primitives/000.png differ
diff --git a/test/unit/visual/screenshots/WebGL/instanced randering/instances() API draws immediate-mode shape primitives/metadata.json b/test/unit/visual/screenshots/WebGL/instanced randering/instances() API draws immediate-mode shape primitives/metadata.json
new file mode 100644
index 0000000000..2d4bfe30da
--- /dev/null
+++ b/test/unit/visual/screenshots/WebGL/instanced randering/instances() API draws immediate-mode shape primitives/metadata.json
@@ -0,0 +1,3 @@
+{
+ "numScreenshots": 1
+}
\ No newline at end of file
diff --git a/test/unit/visual/screenshots/WebGL/instanced randering/instances() API draws multiple spaced 2D rects/000.png b/test/unit/visual/screenshots/WebGL/instanced randering/instances() API draws multiple spaced 2D rects/000.png
new file mode 100644
index 0000000000..58d3d20fff
Binary files /dev/null and b/test/unit/visual/screenshots/WebGL/instanced randering/instances() API draws multiple spaced 2D rects/000.png differ
diff --git a/test/unit/visual/screenshots/WebGL/instanced randering/instances() API draws multiple spaced 2D rects/metadata.json b/test/unit/visual/screenshots/WebGL/instanced randering/instances() API draws multiple spaced 2D rects/metadata.json
new file mode 100644
index 0000000000..2d4bfe30da
--- /dev/null
+++ b/test/unit/visual/screenshots/WebGL/instanced randering/instances() API draws multiple spaced 2D rects/metadata.json
@@ -0,0 +1,3 @@
+{
+ "numScreenshots": 1
+}
\ No newline at end of file
diff --git a/test/unit/visual/screenshots/WebGL/instanced randering/instances() API draws multiple spaced primitives/000.png b/test/unit/visual/screenshots/WebGL/instanced randering/instances() API draws multiple spaced primitives/000.png
new file mode 100644
index 0000000000..7404725fee
Binary files /dev/null and b/test/unit/visual/screenshots/WebGL/instanced randering/instances() API draws multiple spaced primitives/000.png differ
diff --git a/test/unit/visual/screenshots/WebGL/instanced randering/instances() API draws multiple spaced primitives/metadata.json b/test/unit/visual/screenshots/WebGL/instanced randering/instances() API draws multiple spaced primitives/metadata.json
new file mode 100644
index 0000000000..2d4bfe30da
--- /dev/null
+++ b/test/unit/visual/screenshots/WebGL/instanced randering/instances() API draws multiple spaced primitives/metadata.json
@@ -0,0 +1,3 @@
+{
+ "numScreenshots": 1
+}
\ No newline at end of file
diff --git a/test/unit/visual/screenshots/WebGPU/Shaders/instances() API draws immediate-mode shape primitives (WebGPU)/000.png b/test/unit/visual/screenshots/WebGPU/Shaders/instances() API draws immediate-mode shape primitives (WebGPU)/000.png
new file mode 100644
index 0000000000..751b893bba
Binary files /dev/null and b/test/unit/visual/screenshots/WebGPU/Shaders/instances() API draws immediate-mode shape primitives (WebGPU)/000.png differ
diff --git a/test/unit/visual/screenshots/WebGPU/Shaders/instances() API draws immediate-mode shape primitives (WebGPU)/metadata.json b/test/unit/visual/screenshots/WebGPU/Shaders/instances() API draws immediate-mode shape primitives (WebGPU)/metadata.json
new file mode 100644
index 0000000000..2d4bfe30da
--- /dev/null
+++ b/test/unit/visual/screenshots/WebGPU/Shaders/instances() API draws immediate-mode shape primitives (WebGPU)/metadata.json
@@ -0,0 +1,3 @@
+{
+ "numScreenshots": 1
+}
\ No newline at end of file
diff --git a/test/unit/visual/screenshots/WebGPU/Shaders/instances() API draws multiple spaced 2D rects (WebGPU)/000.png b/test/unit/visual/screenshots/WebGPU/Shaders/instances() API draws multiple spaced 2D rects (WebGPU)/000.png
new file mode 100644
index 0000000000..58d3d20fff
Binary files /dev/null and b/test/unit/visual/screenshots/WebGPU/Shaders/instances() API draws multiple spaced 2D rects (WebGPU)/000.png differ
diff --git a/test/unit/visual/screenshots/WebGPU/Shaders/instances() API draws multiple spaced 2D rects (WebGPU)/metadata.json b/test/unit/visual/screenshots/WebGPU/Shaders/instances() API draws multiple spaced 2D rects (WebGPU)/metadata.json
new file mode 100644
index 0000000000..2d4bfe30da
--- /dev/null
+++ b/test/unit/visual/screenshots/WebGPU/Shaders/instances() API draws multiple spaced 2D rects (WebGPU)/metadata.json
@@ -0,0 +1,3 @@
+{
+ "numScreenshots": 1
+}
\ No newline at end of file
diff --git a/test/unit/visual/screenshots/WebGPU/Shaders/instances() API draws multiple spaced primitives (WebGPU)/000.png b/test/unit/visual/screenshots/WebGPU/Shaders/instances() API draws multiple spaced primitives (WebGPU)/000.png
new file mode 100644
index 0000000000..71fe813353
Binary files /dev/null and b/test/unit/visual/screenshots/WebGPU/Shaders/instances() API draws multiple spaced primitives (WebGPU)/000.png differ
diff --git a/test/unit/visual/screenshots/WebGPU/Shaders/instances() API draws multiple spaced primitives (WebGPU)/metadata.json b/test/unit/visual/screenshots/WebGPU/Shaders/instances() API draws multiple spaced primitives (WebGPU)/metadata.json
new file mode 100644
index 0000000000..2d4bfe30da
--- /dev/null
+++ b/test/unit/visual/screenshots/WebGPU/Shaders/instances() API draws multiple spaced primitives (WebGPU)/metadata.json
@@ -0,0 +1,3 @@
+{
+ "numScreenshots": 1
+}
\ No newline at end of file
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) {
diff --git a/utils/typescript.mjs b/utils/typescript.mjs
index 751ff117f8..382c9e90fa 100644
--- a/utils/typescript.mjs
+++ b/utils/typescript.mjs
@@ -366,19 +366,27 @@ function convertFunctionTypeForInterface(typeNode, options) {
.map((param, i) => {
let typeObj;
let paramName;
- if (param.type === 'ParameterType') {
- typeObj = param.expression;
- paramName = param.name ?? `p${i}`;
- } else if (typeof param.type === 'object' && param.type !== null) {
- typeObj = param.type;
- paramName = param.name ?? `p${i}`;
+ let isOptional = false;
+
+ let currentParam = param;
+ if (currentParam && currentParam.type === 'OptionalType') {
+ isOptional = true;
+ currentParam = currentParam.expression;
+ }
+
+ if (currentParam && currentParam.type === 'ParameterType') {
+ typeObj = currentParam.expression;
+ paramName = currentParam.name ?? `p${i}`;
+ } else if (currentParam && typeof currentParam.type === 'object' && currentParam.type !== null) {
+ typeObj = currentParam.type;
+ paramName = currentParam.name ?? `p${i}`;
} else {
// param itself is a plain type node
- typeObj = param;
+ typeObj = currentParam;
paramName = `p${i}`;
}
const paramType = convertTypeToTypeScript(typeObj, options);
- return `${paramName}: ${paramType}`;
+ return `${paramName}${isOptional ? '?' : ''}: ${paramType}`;
})
.join(', ');