diff --git a/src/webgl/3d_primitives.js b/src/webgl/3d_primitives.js
index f92ef2f22f..39d7509461 100644
--- a/src/webgl/3d_primitives.js
+++ b/src/webgl/3d_primitives.js
@@ -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
+ * endGeometry() is called. One can also use
+ * buildGeometry() 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
+ *
+ *
+ * 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);
+ * }
+ *
+ *
+ *
+ * @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 p5.Geometry that was
+ * started using beginGeometry(). One can also
+ * use buildGeometry() 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 p5.Geometry that contains all
+ * the shapes drawn in a provided callback function. The returned combined shape
+ * can then be drawn all at once using model().
+ *
+ * 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
+ * beginGeometry() and
+ * endGeometry() 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
+ *
+ *
+ * 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);
+ * }
+ *
+ *
+ *
+ * @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
+ * buildGeometry() as well as those loaded
+ * from loadModel().
+ *
+ * @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
diff --git a/src/webgl/GeometryBuilder.js b/src/webgl/GeometryBuilder.js
new file mode 100644
index 0000000000..289786b651
--- /dev/null
+++ b/src/webgl/GeometryBuilder.js
@@ -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;
diff --git a/src/webgl/p5.Camera.js b/src/webgl/p5.Camera.js
index dc383da6f8..f592732317 100644
--- a/src/webgl/p5.Camera.js
+++ b/src/webgl/p5.Camera.js
@@ -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
]);
diff --git a/src/webgl/p5.Matrix.js b/src/webgl/p5.Matrix.js
index a6f938d8b0..8b288c6ed7 100644
--- a/src/webgl/p5.Matrix.js
+++ b/src/webgl/p5.Matrix.js
@@ -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){
@@ -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]
);
}
@@ -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]
);
}
diff --git a/src/webgl/p5.RendererGL.Immediate.js b/src/webgl/p5.RendererGL.Immediate.js
index 442d37ddfa..b92c81eb79 100644
--- a/src/webgl/p5.RendererGL.Immediate.js
+++ b/src/webgl/p5.RendererGL.Immediate.js
@@ -194,17 +194,46 @@ p5.RendererGL.prototype.endShape = function(
this.isProcessingVertices = true;
this._processVertices(...arguments);
this.isProcessingVertices = false;
+
+ // LINE_STRIP and LINES are not used for rendering, instead
+ // they only indicate a way to modify vertices during the _processVertices() step
+ if (
+ this.immediateMode.shapeMode === constants.LINE_STRIP ||
+ this.immediateMode.shapeMode === constants.LINES
+ ) {
+ this.immediateMode.shapeMode = constants.TRIANGLE_FAN;
+ }
+
+ // WebGL doesn't support the QUADS and QUAD_STRIP modes, so we
+ // need to convert them to a supported format. In `vertex()`, we reformat
+ // the input data into the formats specified below.
+ if (this.immediateMode.shapeMode === constants.QUADS) {
+ this.immediateMode.shapeMode = constants.TRIANGLES;
+ } else if (this.immediateMode.shapeMode === constants.QUAD_STRIP) {
+ this.immediateMode.shapeMode = constants.TRIANGLE_STRIP;
+ }
+
if (this._doFill) {
- if (this.immediateMode.geometry.vertices.length > 1) {
+ if (
+ !this.geometryBuilder &&
+ this.immediateMode.geometry.vertices.length > 1
+ ) {
this._drawImmediateFill();
}
}
if (this._doStroke) {
- if (this.immediateMode.geometry.lineVertices.length > 1) {
+ if (
+ !this.geometryBuilder &&
+ this.immediateMode.geometry.lineVertices.length > 1
+ ) {
this._drawImmediateStroke();
}
}
+ if (this.geometryBuilder) {
+ this.geometryBuilder.addImmediate();
+ }
+
this.isBezier = false;
this.isQuadratic = false;
this.isCurve = false;
@@ -234,7 +263,9 @@ p5.RendererGL.prototype._processVertices = function(mode) {
this.immediateMode.geometry.vertices,
shouldClose
);
- this.immediateMode.geometry._edgesToVertices();
+ if (!this.geometryBuilder) {
+ this.immediateMode.geometry._edgesToVertices();
+ }
}
// For hollow shapes, user must set mode to TESS
const convexShape = this.immediateMode.shapeMode === constants.TESS;
@@ -347,6 +378,7 @@ p5.RendererGL.prototype._tesselateShape = function() {
]))
];
const polyTriangles = this._triangulate(contours);
+ const originalVertices = this.immediateMode.geometry.vertices;
this.immediateMode.geometry.vertices = [];
this.immediateMode.geometry.vertexNormals = [];
this.immediateMode.geometry.uvs = [];
@@ -360,6 +392,30 @@ p5.RendererGL.prototype._tesselateShape = function() {
this.normal(...polyTriangles.slice(j + 9, j + 12));
this.vertex(...polyTriangles.slice(j, j + 5));
}
+ if (this.geometryBuilder) {
+ // Tesselating the face causes the indices of edge vertices to stop being
+ // correct. When rendering, this is not a problem, since _edgesToVertices
+ // will have been called before this, and edge vertex indices are no longer
+ // needed. However, the geometry builder still needs this information, so
+ // when one is active, we need to update the indices.
+ //
+ // We record index mappings in a Map so that once we have found a
+ // corresponding vertex, we don't need to loop to find it again.
+ const newIndex = new Map();
+ this.immediateMode.geometry.edges =
+ this.immediateMode.geometry.edges.map(edge => edge.map(origIdx => {
+ if (!newIndex.has(origIdx)) {
+ const orig = originalVertices[origIdx];
+ newIndex.set(origIdx, this.immediateMode.geometry.vertices.findIndex(
+ v =>
+ orig.x === v.x &&
+ orig.y === v.y &&
+ orig.z === v.z
+ ));
+ }
+ return newIndex.get(origIdx);
+ }));
+ }
this.immediateMode.geometry.vertexColors = colors;
};
@@ -371,7 +427,9 @@ p5.RendererGL.prototype._tesselateShape = function() {
p5.RendererGL.prototype._drawImmediateFill = function() {
const gl = this.GL;
this._useVertexColor = (this.immediateMode.geometry.vertexColors.length > 0);
- const shader = this._getImmediateFillShader();
+
+ let shader;
+ shader = this._getImmediateFillShader();
this._setFillUniforms(shader);
@@ -379,31 +437,13 @@ p5.RendererGL.prototype._drawImmediateFill = function() {
buff._prepareBuffer(this.immediateMode.geometry, shader);
}
- // LINE_STRIP and LINES are not used for rendering, instead
- // they only indicate a way to modify vertices during the _processVertices() step
- if (
- this.immediateMode.shapeMode === constants.LINE_STRIP ||
- this.immediateMode.shapeMode === constants.LINES
- ) {
- this.immediateMode.shapeMode = constants.TRIANGLE_FAN;
- }
-
- // WebGL 1 doesn't support the QUADS and QUAD_STRIP modes, so we
- // need to convert them to a supported format. In `vertex()`, we reformat
- // the input data into the formats specified below.
- if (this.immediateMode.shapeMode === constants.QUADS) {
- this.immediateMode.shapeMode = constants.TRIANGLES;
- } else if (this.immediateMode.shapeMode === constants.QUAD_STRIP) {
- this.immediateMode.shapeMode = constants.TRIANGLE_STRIP;
- }
-
this._applyColorBlend(this.curFillColor);
+
gl.drawArrays(
this.immediateMode.shapeMode,
0,
this.immediateMode.geometry.vertices.length
);
-
shader.unbindShader();
};
@@ -415,18 +455,20 @@ p5.RendererGL.prototype._drawImmediateFill = function() {
p5.RendererGL.prototype._drawImmediateStroke = function() {
const gl = this.GL;
+ this._useLineColor =
+ (this.immediateMode.geometry.vertexStrokeColors.length > 0);
+
const faceCullingEnabled = gl.isEnabled(gl.CULL_FACE);
// Prevent strokes from getting removed by culling
gl.disable(gl.CULL_FACE);
const shader = this._getImmediateStrokeShader();
- this._useLineColor =
- (this.immediateMode.geometry.vertexStrokeColors.length > 0);
this._setStrokeUniforms(shader);
for (const buff of this.immediateMode.buffers.stroke) {
buff._prepareBuffer(this.immediateMode.geometry, shader);
}
this._applyColorBlend(this.curStrokeColor);
+
gl.drawArrays(
gl.TRIANGLES,
0,
diff --git a/src/webgl/p5.RendererGL.Retained.js b/src/webgl/p5.RendererGL.Retained.js
index a6f9931333..308b7310ce 100644
--- a/src/webgl/p5.RendererGL.Retained.js
+++ b/src/webgl/p5.RendererGL.Retained.js
@@ -6,6 +6,18 @@ import './p5.RenderBuffer';
import * as constants from '../core/constants';
let hashCount = 0;
+
+/**
+ * @param {p5.Geometry} geometry The model whose resources will be freed
+ */
+p5.RendererGL.prototype.freeGeometry = function(geometry) {
+ if (!geometry.gid) {
+ console.warn('The model you passed to freeGeometry does not have an id!');
+ return;
+ }
+ this._freeBuffers(geometry.gid);
+};
+
/**
* _initBufferDefaults
* @private
@@ -118,7 +130,7 @@ p5.RendererGL.prototype.drawBuffers = function(gId) {
const gl = this.GL;
const geometry = this.retainedMode.geometry[gId];
- if (this._doFill) {
+ if (!this.geometryBuilder && this._doFill) {
this._useVertexColor = (geometry.model.vertexColors.length > 0);
const fillShader = this._getRetainedFillShader();
this._setFillUniforms(fillShader);
@@ -134,12 +146,12 @@ p5.RendererGL.prototype.drawBuffers = function(gId) {
fillShader.unbindShader();
}
- if (this._doStroke && geometry.lineVertexCount > 0) {
+ if (!this.geometryBuilder && this._doStroke && geometry.lineVertexCount > 0) {
+ this._useLineColor = (geometry.model.vertexStrokeColors.length > 0);
const faceCullingEnabled = gl.isEnabled(gl.CULL_FACE);
// Prevent strokes from getting removed by culling
gl.disable(gl.CULL_FACE);
const strokeShader = this._getRetainedStrokeShader();
- this._useLineColor = (geometry.model.vertexStrokeColors.length > 0);
this._setStrokeUniforms(strokeShader);
for (const buff of this.retainedMode.buffers.stroke) {
buff._prepareBuffer(geometry, strokeShader);
@@ -152,6 +164,10 @@ p5.RendererGL.prototype.drawBuffers = function(gId) {
strokeShader.unbindShader();
}
+ if (this.geometryBuilder) {
+ this.geometryBuilder.addRetained(geometry);
+ }
+
return this;
};
diff --git a/src/webgl/p5.RendererGL.js b/src/webgl/p5.RendererGL.js
index ee378ed828..65ca2523eb 100644
--- a/src/webgl/p5.RendererGL.js
+++ b/src/webgl/p5.RendererGL.js
@@ -1,5 +1,6 @@
import p5 from '../core/main';
import * as constants from '../core/constants';
+import GeometryBuilder from './GeometryBuilder';
import libtess from 'libtess';
import './p5.Shader';
import './p5.Camera';
@@ -409,6 +410,9 @@ p5.RendererGL = class RendererGL extends p5.Renderer {
this._initContext();
this.isP3D = true; //lets us know we're in 3d mode
+ // When constructing a new p5.Geometry, this will represent the builder
+ this.geometryBuilder = undefined;
+
// This redundant property is useful in reminding you that you are
// interacting with WebGLRenderingContext, still worth considering future removal
this.GL = this.drawingContext;
@@ -602,6 +606,68 @@ p5.RendererGL = class RendererGL extends p5.Renderer {
this._curShader = undefined;
}
+ /**
+ * Starts creating a new p5.Geometry. Subsequent shapes drawn will be added
+ * to the geometry and then returned when
+ * endGeometry() is called. One can also use
+ * buildGeometry() 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
+ */
+ beginGeometry() {
+ if (this.geometryBuilder) {
+ throw new Error('It looks like `beginGeometry()` is being called while another p5.Geometry is already being build.');
+ }
+ this.geometryBuilder = new GeometryBuilder(this);
+ }
+
+ /**
+ * Finishes creating a new p5.Geometry that was
+ * started using beginGeometry(). One can also
+ * use buildGeometry() to pass a function that
+ * draws shapes.
+ *
+ * @method endGeometry
+ * @returns {p5.Geometry} The model that was built.
+ */
+ endGeometry() {
+ if (!this.geometryBuilder) {
+ throw new Error('Make sure you call beginGeometry() before endGeometry()!');
+ }
+ const geometry = this.geometryBuilder.finish();
+ this.geometryBuilder = undefined;
+ return geometry;
+ }
+
+ /**
+ * Creates a new p5.Geometry that contains all
+ * the shapes drawn in a provided callback function. The returned combined shape
+ * can then be drawn all at once using model().
+ *
+ * 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
+ * beginGeometry() and
+ * endGeometry() 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.
+ */
+ buildGeometry(callback) {
+ this.beginGeometry();
+ callback();
+ return this.endGeometry();
+ }
+
//////////////////////////////////////////////
// Setting
//////////////////////////////////////////////
diff --git a/test/unit/webgl/p5.Geometry.js b/test/unit/webgl/p5.Geometry.js
index 64b9b03a82..0b39a4b95e 100644
--- a/test/unit/webgl/p5.Geometry.js
+++ b/test/unit/webgl/p5.Geometry.js
@@ -144,4 +144,118 @@ suite('p5.Geometry', function() {
assert.equal(geom._addJoin.callCount, 0);
});
});
+
+ suite('buildGeometry', function() {
+ const checkLights = () => myp5.lights();
+ const checkMaterials = () => {
+ myp5.fill('#ffea30');
+ myp5.ambientMaterial(myp5.color('#f2b988'));
+ myp5.specularMaterial(255);
+ myp5.shininess(200);
+ myp5.ambientLight(myp5.color('#88989e'));
+ myp5.pointLight(200, -100, -50, 255, 255, 255);
+ };
+ const checkNormals = () => myp5.normalMaterial();
+ function assertGeometryRendersMatch(drawGeometry, lightingModes) {
+ myp5.createCanvas(50, 50, myp5.WEBGL);
+ myp5.pixelDensity(1);
+ myp5.setAttributes({ antialias: false });
+
+ for (const applyLights of lightingModes) {
+ // Regular mode
+ myp5.background(255);
+ myp5.fill(255);
+ myp5.push();
+ applyLights();
+ drawGeometry();
+ myp5.pop();
+ myp5.resetShader();
+ const regularImage = myp5._renderer.elt.toDataURL();
+
+ // Geometry mode
+ myp5.fill(255);
+ const geom = myp5.buildGeometry(drawGeometry);
+ console.log(geom);
+ myp5.background(255);
+ myp5.push();
+ applyLights();
+ myp5.model(geom);
+ myp5.pop();
+ myp5.resetShader();
+ const geometryImage = myp5._renderer.elt.toDataURL();
+
+ assert.equal(regularImage, geometryImage);
+ }
+ }
+
+ test('Transforms are applied to models', function() {
+ assertGeometryRendersMatch(function() {
+ myp5.push();
+ myp5.translate(0, -20);
+ for (let i = 0; i < 4; i++) {
+ myp5.box(8);
+ myp5.translate(0, 40/3);
+ myp5.rotateY(myp5.PI * 0.2);
+ }
+ myp5.pop();
+ }, [checkLights, checkMaterials, checkNormals]);
+ });
+
+ test('Immediate mode constructs are translated correctly', function() {
+ assertGeometryRendersMatch(function() {
+ myp5.scale(1/6);
+ myp5.push();
+ myp5.translate(100, -50);
+ myp5.scale(0.5);
+ myp5.rotateX(myp5.PI/4);
+ myp5.cone();
+ myp5.pop();
+ myp5.cone();
+
+ myp5.beginShape();
+ myp5.vertex(-20, -50);
+ myp5.quadraticVertex(
+ -40, -70,
+ 0, -60
+ );
+ myp5.endShape();
+
+ myp5.beginShape(myp5.TRIANGLE_STRIP);
+ for (let y = 20; y <= 60; y += 10) {
+ for (let x of [20, 60]) {
+ myp5.vertex(x, y);
+ }
+ }
+ myp5.endShape();
+
+ myp5.beginShape();
+ myp5.vertex(-100, -120);
+ myp5.vertex(-120, -110);
+ myp5.vertex(-105, -100);
+ myp5.endShape();
+ }, [checkLights, checkMaterials, checkNormals]);
+ });
+
+ test('Vertex colors are captured', function() {
+ assertGeometryRendersMatch(function() {
+ myp5.push();
+ myp5.translate(0, -10);
+ myp5.fill('red');
+ myp5.sphere(5, 10, 5);
+ myp5.pop();
+
+ myp5.push();
+ myp5.translate(-10, 10);
+ myp5.fill('lime');
+ myp5.sphere(5, 10, 5);
+ myp5.pop();
+
+ myp5.push();
+ myp5.translate(10, 10);
+ myp5.fill('blue');
+ myp5.sphere(5, 10, 5);
+ myp5.pop();
+ }, [checkLights]);
+ });
+ });
});
diff --git a/test/unit/webgl/p5.Matrix.js b/test/unit/webgl/p5.Matrix.js
index 3de95ac8bf..88550e35a4 100644
--- a/test/unit/webgl/p5.Matrix.js
+++ b/test/unit/webgl/p5.Matrix.js
@@ -341,6 +341,9 @@ suite('p5.Matrix', function() {
});
test('column() and row()', function(){
const m = new p5.Matrix('mat3', [
+ // The matrix data is stored column-major, so each line below is
+ // a column rather than a row. Imagine you are looking at the
+ // transpose of the matrix in the source code.
1, 2, 3,
4, 5, 6,
7, 8, 9
@@ -348,15 +351,15 @@ suite('p5.Matrix', function() {
const column0 = m.column(0);
const column1 = m.column(1);
const column2 = m.column(2);
- assert.deepEqual(column0.array(), [1, 4, 7]);
- assert.deepEqual(column1.array(), [2, 5, 8]);
- assert.deepEqual(column2.array(), [3, 6, 9]);
+ assert.deepEqual(column0.array(), [1, 2, 3]);
+ assert.deepEqual(column1.array(), [4, 5, 6]);
+ assert.deepEqual(column2.array(), [7, 8, 9]);
const row0 = m.row(0);
const row1 = m.row(1);
const row2 = m.row(2);
- assert.deepEqual(row0.array(), [1, 2, 3]);
- assert.deepEqual(row1.array(), [4, 5, 6]);
- assert.deepEqual(row2.array(), [7, 8, 9]);
+ assert.deepEqual(row0.array(), [1, 4, 7]);
+ assert.deepEqual(row1.array(), [2, 5, 8]);
+ assert.deepEqual(row2.array(), [3, 6, 9]);
});
test('diagonal()', function() {
const m = new p5.Matrix('mat3', [
@@ -381,11 +384,11 @@ suite('p5.Matrix', function() {
]);
const multVector = new p5.Vector(3, 2, 1);
const result = m.multiplyVec3(multVector);
- assert.deepEqual(result.array(), [10, 28, 46]);
+ assert.deepEqual(result.array(), [18, 24, 30]);
// If there is a target, set result and return that.
const target = new p5.Vector();
m.multiplyVec3(multVector, target);
- assert.deepEqual(target.array(), [10, 28, 46]);
+ assert.deepEqual(target.array(), [18, 24, 30]);
});
test('createSubMatrix3x3', function() {
const m4x4 = new p5.Matrix([