Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
10 changes: 9 additions & 1 deletion src/core/p5.Renderer3D.js
Original file line number Diff line number Diff line change
Expand Up @@ -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);

Expand Down Expand Up @@ -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);
Expand Down Expand Up @@ -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 {
Expand All @@ -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);
Expand Down
71 changes: 71 additions & 0 deletions src/webgl/3d_primitives.js
Original file line number Diff line number Diff line change
Expand Up @@ -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
* <div>
* <code>
* // 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);
* }
* </code>
* </div>
*/
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;
Expand Down
67 changes: 66 additions & 1 deletion test/unit/webgl/p5.RendererGL.js
Original file line number Diff line number Diff line change
@@ -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);
Expand Down Expand Up @@ -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) {
Expand Down