Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add methods to construct p5.Geometry from other p5 drawing functions #6287

Merged
merged 12 commits into from
Aug 2, 2023
170 changes: 170 additions & 0 deletions src/webgl/3d_primitives.js
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,176 @@ import p5 from '../core/main';
import './p5.Geometry';
import * as constants from '../core/constants';

/**
* Starts creating a new p5.Geometry. Subsequent shapes drawn will be added
aferriss marked this conversation as resolved.
Show resolved Hide resolved
* to the geometry and then returned when
* <a href="#/p5/endGeometry">endGeometry()</a> is called. One can also use
* <a href="#/p5/buildGeometry">buildGeometry()</a> to pass a function that
* draws shapes.
*
* If you need to draw complex shapes every frame which don't change over time,
* combining them upfront with `beginGeometry()` and `endGeometry()` and then
* drawing that will run faster than repeatedly drawing the individual pieces.
*
* @method beginGeometry
*
* @example
* <div>
* <code>
* let shapes;
*
* function setup() {
* createCanvas(100, 100, WEBGL);
* makeShapes();
* }
*
* function makeShapes() {
* beginGeometry();
* scale(0.18);
*
* push();
* translate(100, -50);
* scale(0.5);
* rotateX(PI/4);
* cone();
* pop();
* cone();
*
* beginShape();
* vertex(-20, -50);
* quadraticVertex(
* -40, -70,
* 0, -60
* );
* endShape();
*
* beginShape(TRIANGLE_STRIP);
* for (let y = 20; y <= 60; y += 10) {
* for (let x of [20, 60]) {
* vertex(x, y);
* }
* }
* endShape();
*
* beginShape();
* vertex(-100, -120);
* vertex(-120, -110);
* vertex(-105, -100);
* endShape();
*
* shapes = endGeometry();
* }
*
* function draw() {
* background(255);
* lights();
* orbitControl();
* model(shapes);
* }
* </code>
* </div>
*
* @alt
* A series of different flat, curved, and 3D shapes floating in space.
*/
p5.prototype.beginGeometry = function() {
return this._renderer.beginGeometry();
};

/**
* Finishes creating a new <a href="#/p5.Geometry">p5.Geometry</a> that was
* started using <a href="#/p5/beginGeometry">beginGeometry()</a>. One can also
* use <a href="#/p5/buildGeometry">buildGeometry()</a> to pass a function that
* draws shapes.
*
* @method endGeometry
* @returns {p5.Geometry} The model that was built.
*/
p5.prototype.endGeometry = function() {
return this._renderer.endGeometry();
};

/**
* Creates a new <a href="#/p5.Geometry">p5.Geometry</a> that contains all
* the shapes drawn in a provided callback function. The returned combined shape
* can then be drawn all at once using <a href="#/p5/model">model()</a>.
*
* If you need to draw complex shapes every frame which don't change over time,
* combining them with `buildGeometry()` once and then drawing that will run
* faster than repeatedly drawing the individual pieces.
*
* One can also draw shapes directly between
* <a href="#/p5/beginGeometry">beginGeometry()</a> and
* <a href="#/p5/endGeometry">endGeometry()</a> instead of using a callback
* function.
*
* @method buildGeometry
* @param {Function} callback A function that draws shapes.
* @returns {p5.Geometry} The model that was built from the callback function.
*
* @example
* <div>
* <code>
* let particles;
* let button;
*
* function setup() {
* createCanvas(100, 100, WEBGL);
* button = createButton('New');
* button.mousePressed(makeParticles);
* makeParticles();
* }
*
* function makeParticles() {
* if (particles) freeGeometry(particles);
*
* particles = buildGeometry(() => {
* for (let i = 0; i < 60; i++) {
* push();
* translate(
* randomGaussian(0, 20),
* randomGaussian(0, 20),
* randomGaussian(0, 20)
* );
* sphere(5);
* pop();
* }
* });
* }
*
* function draw() {
* background(255);
* noStroke();
* lights();
* orbitControl();
* model(particles);
* }
* </code>
* </div>
*
* @alt
* A cluster of spheres.
*/
p5.prototype.buildGeometry = function(callback) {
return this._renderer.buildGeometry(callback);
};

/**
* Clears the resources of a model to free up browser memory. A model whose
* resources have been cleared can still be drawn, but the first time it is
* drawn again, it might take longer.
*
* This method works on models generated with
* <a href="#/p5/buildGeometry">buildGeometry()</a> as well as those loaded
* from <a href="#/p5/loadModel">loadModel()</a>.
*
* @method freeGeometry
davepagurek marked this conversation as resolved.
Show resolved Hide resolved
* @param {p5.Geometry} The geometry whose resources should be freed
*/
p5.prototype.freeGeometry = function(geometry) {
davepagurek marked this conversation as resolved.
Show resolved Hide resolved
this._renderer._freeBuffers(geometry.gid);
};

/**
* Draw a plane with given a width and height
* @method plane
Expand Down
139 changes: 139 additions & 0 deletions src/webgl/GeometryBuilder.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,139 @@
import p5 from '../core/main';
import * as constants from '../core/constants';

/**
* @private
* A class responsible for converting successive WebGL draw calls into a single
* `p5.Geometry` that can be reused and drawn with `model()`.
*/
class GeometryBuilder {
constructor(renderer) {
this.renderer = renderer;
renderer._pInst.push();
this.identityMatrix = new p5.Matrix();
renderer.uMVMatrix = new p5.Matrix();
this.geometry = new p5.Geometry();
this.geometry.gid = `_p5_GeometryBuilder_${GeometryBuilder.nextGeometryId}`;
GeometryBuilder.nextGeometryId++;
davepagurek marked this conversation as resolved.
Show resolved Hide resolved
this.hasTransform = false;
}

/**
* @private
* Applies the current transformation matrix to each vertex.
*/
transformVertices(vertices) {
if (!this.hasTransform) return vertices;

return vertices.map(v => this.renderer.uMVMatrix.multiplyPoint(v));
}

/**
* @private
* Applies the current normal matrix to each normal.
*/
transformNormals(normals) {
if (!this.hasTransform) return normals;

return normals.map(
v => this.renderer.uNMatrix.multiplyVec3(v)
);
}

/**
* @private
* Adds a p5.Geometry to the builder's combined geometry, flattening
* transformations.
*/
addGeometry(input) {
this.hasTransform = !this.renderer.uMVMatrix.mat4
.every((v, i) => v === this.identityMatrix.mat4[i]);

if (this.hasTransform) {
this.renderer.uNMatrix.inverseTranspose(this.renderer.uMVMatrix);
}

let startIdx = this.geometry.vertices.length;
this.geometry.vertices.push(...this.transformVertices(input.vertices));
this.geometry.vertexNormals.push(
...this.transformNormals(input.vertexNormals)
);
this.geometry.uvs.push(...input.uvs);

if (this.renderer._doFill) {
this.geometry.faces.push(
...input.faces.map(f => f.map(idx => idx + startIdx))
);
}
if (this.renderer._doStroke) {
this.geometry.edges.push(
...input.edges.map(edge => edge.map(idx => idx + startIdx))
);
}
const vertexColors = [...input.vertexColors];
while (vertexColors.length < input.vertices.length * 4) {
vertexColors.push(...this.renderer.curFillColor);
}
this.geometry.vertexColors.push(...vertexColors);
}

/**
* Adds geometry from the renderer's immediate mode into the builder's
* combined geometry.
*/
addImmediate() {
const geometry = this.renderer.immediateMode.geometry;
const shapeMode = this.renderer.immediateMode.shapeMode;
const faces = [];

if (this.renderer._doFill) {
if (
shapeMode === constants.TRIANGLE_STRIP ||
shapeMode === constants.QUAD_STRIP
) {
for (let i = 2; i < geometry.vertices.length; i++) {
if (i % 2 === 0) {
faces.push([i, i - 1, i - 2]);
} else {
faces.push([i, i - 2, i - 1]);
}
}
} else if (shapeMode === constants.TRIANGLE_FAN) {
for (let i = 2; i < geometry.vertices.length; i++) {
faces.push([0, i - 1, i]);
}
} else {
for (let i = 0; i < geometry.vertices.length; i += 3) {
faces.push([i, i + 1, i + 2]);
}
}
}
this.addGeometry(Object.assign({}, geometry, { faces }));
}

/**
* Adds geometry from the renderer's retained mode into the builder's
* combined geometry.
*/
addRetained(geometry) {
this.addGeometry(geometry.model);
}

/**
* Cleans up the state of the renderer and returns the combined geometry that
* was built.
* @returns p5.Geometry The flattened, combined geometry
*/
finish() {
this.renderer._pInst.pop();
return this.geometry;
}
}

/**
* Keeps track of how many custom geometry objects have been made so that each
* can be assigned a unique ID.
*/
GeometryBuilder.nextGeometryId = 0;

export default GeometryBuilder;
8 changes: 4 additions & 4 deletions src/webgl/p5.Camera.js
Original file line number Diff line number Diff line change
Expand Up @@ -1910,13 +1910,13 @@ p5.Camera = class Camera {
const ca = c * a;
const lerpedRotMat = new p5.Matrix('mat3', [
cosAngle + oneMinusCosAngle * a * a,
oneMinusCosAngle * ab - sinAngle * c,
oneMinusCosAngle * ca + sinAngle * b,
oneMinusCosAngle * ab + sinAngle * c,
cosAngle + oneMinusCosAngle * b * b,
oneMinusCosAngle * bc - sinAngle * a,
oneMinusCosAngle * ca - sinAngle * b,
oneMinusCosAngle * ab - sinAngle * c,
cosAngle + oneMinusCosAngle * b * b,
oneMinusCosAngle * bc + sinAngle * a,
oneMinusCosAngle * ca + sinAngle * b,
oneMinusCosAngle * bc - sinAngle * a,
cosAngle + oneMinusCosAngle * c * c
]);

Expand Down
16 changes: 8 additions & 8 deletions src/webgl/p5.Matrix.js
Original file line number Diff line number Diff line change
Expand Up @@ -22,7 +22,7 @@ if (typeof Float32Array !== 'undefined') {
* @class p5.Matrix
* @private
* @constructor
* @param {Array} [mat4] array literal of our 4×4 matrix
* @param {Array} [mat4] column-major array literal of our 4×4 matrix
*/
p5.Matrix = class {
constructor(...args){
Expand Down Expand Up @@ -877,9 +877,9 @@ p5.Matrix = class {
*/
column(columnIndex) {
return new p5.Vector(
this.mat3[columnIndex],
this.mat3[columnIndex + 3],
this.mat3[columnIndex + 6]
this.mat3[3 * columnIndex],
this.mat3[3 * columnIndex + 1],
this.mat3[3 * columnIndex + 2]
);
}

Expand All @@ -888,14 +888,14 @@ p5.Matrix = class {
* A function that returns a row vector of a 3x3 matrix.
*
* @method row
* @param {Number} columnIndex matrix row number
* @param {Number} rowIndex matrix row number
* @return {p5.Vector}
*/
row(rowIndex) {
return new p5.Vector(
this.mat3[3 * rowIndex],
this.mat3[3 * rowIndex + 1],
this.mat3[3 * rowIndex + 2]
this.mat3[rowIndex],
this.mat3[rowIndex + 3],
this.mat3[rowIndex + 6]
);
}

Expand Down