Skip to content

Commit

Permalink
Merge pull request #6287 from davepagurek/build-geometry
Browse files Browse the repository at this point in the history
Add methods to construct p5.Geometry from other p5 drawing functions
  • Loading branch information
davepagurek committed Aug 2, 2023
2 parents b2b93c2 + a9f4eb3 commit b677a33
Show file tree
Hide file tree
Showing 9 changed files with 613 additions and 48 deletions.
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
* 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
* @param {p5.Geometry} The geometry whose resources should be freed
*/
p5.prototype.freeGeometry = function(geometry) {
this._renderer._freeBuffers(geometry.gid);
};

/**
* Draw a plane with given a width and height
* @method plane
Expand Down
154 changes: 154 additions & 0 deletions src/webgl/GeometryBuilder.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,154 @@
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++;
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();

// If all vertices are the same color (no per-vertex colors were
// supplied), remove the vertex color data so that one may override the
// fill when drawing the geometry with `model()`
let allVertexColorsSame = true;
for (let i = 4; i < this.geometry.vertexColors.length; i++) {
if (this.geometry.vertexColors[i] !== this.geometry.vertexColors[i % 4]) {
allVertexColorsSame = false;
break;
}
}
if (allVertexColorsSame) {
this.geometry.vertexColors = [];
}

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

0 comments on commit b677a33

Please sign in to comment.