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(', ');