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([