From 8e204a7601534bda04c994eacda285f7ae00a62a Mon Sep 17 00:00:00 2001 From: Dave Pagurek Date: Mon, 24 Jul 2023 10:25:56 -0400 Subject: [PATCH 01/11] Add geometry building --- src/webgl/GeometryBuilder.js | 110 +++++++++++++++++++++++++++ src/webgl/p5.Matrix.js | 18 +++++ src/webgl/p5.RendererGL.Immediate.js | 63 +++++++++------ src/webgl/p5.RendererGL.Retained.js | 22 +++++- src/webgl/p5.RendererGL.js | 37 +++++++++ 5 files changed, 224 insertions(+), 26 deletions(-) create mode 100644 src/webgl/GeometryBuilder.js diff --git a/src/webgl/GeometryBuilder.js b/src/webgl/GeometryBuilder.js new file mode 100644 index 0000000000..8b278b0ab0 --- /dev/null +++ b/src/webgl/GeometryBuilder.js @@ -0,0 +1,110 @@ +import p5 from '../core/main'; +import * as constants from '../core/constants'; + +class GeometryBuilder { + constructor(renderer) { + this.renderer = renderer; + renderer.push(); + this.identityMatrix = new p5.Matrix(); + renderer.uMVMatrix = new p5.Matrix(); + this.geometry = new p5.Geometry(); + this.geometry.gid = `_p5_GeometryBuilder_${renderer.nextGeometryId}`; + renderer.nextGeometryId++; + this.hasTransform = false; + } + + /** + * @private + */ + transformVertices(vertices) { + if (!this.hasTransform) return vertices; + + return vertices.map(v => this.renderer.uMVMatrix.multiplyPoint(v)); + } + + /** + * @private + */ + transformNormals(normals) { + if (!this.hasTransform) return normals; + + return normals.map(v => this.renderer.uNMatrix.multiplyVec3Direction(v)); + } + + /** + * @private + */ + addGeometry(input) { + this.hasTransform = this.renderer.uMVMatrix.mat4 + .every((v, i) => v === this.identity.mat4[i]); + + if (this.hasTransform) { + this.renderer.uNMatrix.inverseTranspose(this.renderer.uMVMatrix); + } + + const 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); + } + + 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 === this.p5.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)); + } + + addRetained(geometry) { + this.addGeometry(geometry.model); + } + + finish() { + renderer.pop(); + return this.geometry; + } +} + +export default GeometryBuilder; diff --git a/src/webgl/p5.Matrix.js b/src/webgl/p5.Matrix.js index add1da0657..b8ada9f7cc 100644 --- a/src/webgl/p5.Matrix.js +++ b/src/webgl/p5.Matrix.js @@ -739,6 +739,24 @@ p5.Matrix = class { return result; } + /** + * TODO pull and remove + */ + multiplyVec3(x, y, z) { + const result = new Array(3); + const m = this.mat3; + + result[0] = m[0] * x + m[3] * y + m[6] * z; + result[1] = m[1] * x + m[4] * y + m[7] * z; + result[2] = m[2] * x + m[5] * y + m[8] * z; + + return result; + } + + multiplyVec3Direction({ x, y, z }) { + return new p5.Vector(...this.multiplyVec3(x, y, z)); + } + /** * Applies a matrix to a vector. * The fourth component is set to 1. diff --git a/src/webgl/p5.RendererGL.Immediate.js b/src/webgl/p5.RendererGL.Immediate.js index 442d37ddfa..dc430dc644 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; @@ -371,7 +400,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 +410,15 @@ 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; - } + this._applyColorBlend(this.curFillColor); - // 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.drawImmediateArrays(); - this._applyColorBlend(this.curFillColor); gl.drawArrays( this.immediateMode.shapeMode, 0, this.immediateMode.geometry.vertices.length ); - shader.unbindShader(); }; @@ -415,18 +430,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..c58f8d55a7 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 geometry p5.Geometry The model whose resources will be freed + */ +p5.RendererGL.prototype.freeGeometry = function(geometry) { + if (!geometry.gid) { + console.warn('The model you passed to freeModel 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 c85c873303..2b81a652dc 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,10 @@ 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.nextGeometryId = 0; + // This redundant property is useful in reminding you that you are // interacting with WebGLRenderingContext, still worth considering future removal this.GL = this.drawingContext; @@ -604,6 +609,38 @@ p5.RendererGL = class RendererGL extends p5.Renderer { return this; } + /** + * Starts creating a new p5.Geometry. + * TODO add examples + */ + beginGeometry() { + this.geometryBuilder = new GeometryBuilder(this); + } + + /** + * Finishes creating a new p5.Geometry. + * @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; + } + + /** + * @param callback Function A function that draws shapes to store in a + * p5.Geometry for faster rendering. + * @returns p5.Geometry The model that was built from the draw functions + */ + buildGeometry(callback) { + this.beginGeometry(); + callback(); + return this.endGeometry(); + } + ////////////////////////////////////////////// // Setting ////////////////////////////////////////////// From 9ae7c5c839e98245fcc90eb6e4505ebaa653c3e0 Mon Sep 17 00:00:00 2001 From: Dave Pagurek Date: Mon, 24 Jul 2023 10:45:42 -0400 Subject: [PATCH 02/11] Fix bugs --- src/core/rendering.js | 25 +++++++++++++++++++++++++ src/webgl/GeometryBuilder.js | 6 +++--- 2 files changed, 28 insertions(+), 3 deletions(-) diff --git a/src/core/rendering.js b/src/core/rendering.js index 32f448770b..891247f043 100644 --- a/src/core/rendering.js +++ b/src/core/rendering.js @@ -327,6 +327,31 @@ p5.prototype.createFramebuffer = function(options) { return new p5.Framebuffer(this, options); }; +/** + * Starts creating a new p5.Geometry. + * TODO add examples + */ +p5.prototype.beginGeometry = function() { + return this._renderer.beginGeometry(); +}; + +/** + * Finishes creating a new p5.Geometry. + * @returns p5.Geometry The model that was built + */ +p5.prototype.endGeometry = function() { + return this._renderer.endGeometry(); +}; + +/** + * @param callback Function A function that draws shapes to store in a + * p5.Geometry for faster rendering. + * @returns p5.Geometry The model that was built from the draw functions + */ +p5.prototype.buildGeometry = function(callback) { + return this._renderer.buildGeometry(callback); +}; + /** * Blends the pixels in the display window according to the defined mode. * There is a choice of the following modes to blend the source pixels (A) diff --git a/src/webgl/GeometryBuilder.js b/src/webgl/GeometryBuilder.js index 8b278b0ab0..27c61e6386 100644 --- a/src/webgl/GeometryBuilder.js +++ b/src/webgl/GeometryBuilder.js @@ -4,7 +4,7 @@ import * as constants from '../core/constants'; class GeometryBuilder { constructor(renderer) { this.renderer = renderer; - renderer.push(); + renderer._pInst.push(); this.identityMatrix = new p5.Matrix(); renderer.uMVMatrix = new p5.Matrix(); this.geometry = new p5.Geometry(); @@ -36,7 +36,7 @@ class GeometryBuilder { */ addGeometry(input) { this.hasTransform = this.renderer.uMVMatrix.mat4 - .every((v, i) => v === this.identity.mat4[i]); + .every((v, i) => v === this.identityMatrix.mat4[i]); if (this.hasTransform) { this.renderer.uNMatrix.inverseTranspose(this.renderer.uMVMatrix); @@ -102,7 +102,7 @@ class GeometryBuilder { } finish() { - renderer.pop(); + this.renderer._pInst.pop(); return this.geometry; } } From dfaa4d68cdf60c711540a51ec2c9718d2805af1f Mon Sep 17 00:00:00 2001 From: Dave Pagurek Date: Mon, 24 Jul 2023 11:46:19 -0400 Subject: [PATCH 03/11] Fix row()/column() bug --- src/core/rendering.js | 8 ++++++++ src/webgl/GeometryBuilder.js | 8 +++++--- src/webgl/p5.Matrix.js | 30 ++++++------------------------ 3 files changed, 19 insertions(+), 27 deletions(-) diff --git a/src/core/rendering.js b/src/core/rendering.js index 749001fd2b..88d24d033c 100644 --- a/src/core/rendering.js +++ b/src/core/rendering.js @@ -399,6 +399,14 @@ p5.prototype.buildGeometry = function(callback) { return this._renderer.buildGeometry(callback); }; +/** + * TODO + * @param {p5.Geometry} The geometry whose resources should be freed + */ +p5.prototype.freeGeometry = function(geometry) { + this._renderer._freeBuffers(geometry.gid); +}; + /** * Blends the pixels in the display window according to the defined mode. * There is a choice of the following modes to blend the source pixels (A) diff --git a/src/webgl/GeometryBuilder.js b/src/webgl/GeometryBuilder.js index 27c61e6386..b5c2d762ca 100644 --- a/src/webgl/GeometryBuilder.js +++ b/src/webgl/GeometryBuilder.js @@ -28,14 +28,16 @@ class GeometryBuilder { transformNormals(normals) { if (!this.hasTransform) return normals; - return normals.map(v => this.renderer.uNMatrix.multiplyVec3Direction(v)); + return normals.map( + v => this.renderer.uNMatrix.multiplyVec3(v) + ); } /** * @private */ addGeometry(input) { - this.hasTransform = this.renderer.uMVMatrix.mat4 + this.hasTransform = !this.renderer.uMVMatrix.mat4 .every((v, i) => v === this.identityMatrix.mat4[i]); if (this.hasTransform) { @@ -61,7 +63,7 @@ class GeometryBuilder { } const vertexColors = [...input.vertexColors]; while (vertexColors.length < input.vertices.length * 4) { - vertexColors.push(this.renderer.curFillColor); + vertexColors.push(...this.renderer.curFillColor); } this.geometry.vertexColors.push(...vertexColors); } diff --git a/src/webgl/p5.Matrix.js b/src/webgl/p5.Matrix.js index 28df6532f3..2d9ce7fe6c 100644 --- a/src/webgl/p5.Matrix.js +++ b/src/webgl/p5.Matrix.js @@ -767,24 +767,6 @@ p5.Matrix = class { return result; } - /** - * TODO pull and remove - */ - multiplyVec3(x, y, z) { - const result = new Array(3); - const m = this.mat3; - - result[0] = m[0] * x + m[3] * y + m[6] * z; - result[1] = m[1] * x + m[4] * y + m[7] * z; - result[2] = m[2] * x + m[5] * y + m[8] * z; - - return result; - } - - multiplyVec3Direction({ x, y, z }) { - return new p5.Vector(...this.multiplyVec3(x, y, z)); - } - /** * Applies a matrix to a vector. * The fourth component is set to 1. @@ -895,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] ); } @@ -911,9 +893,9 @@ p5.Matrix = class { */ 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] ); } From 4c8a45b17aede2ed448af9bb49485c5dad3e2c0e Mon Sep 17 00:00:00 2001 From: Dave Pagurek Date: Mon, 24 Jul 2023 13:40:33 -0400 Subject: [PATCH 04/11] Remove old function call --- src/webgl/p5.RendererGL.Immediate.js | 2 -- 1 file changed, 2 deletions(-) diff --git a/src/webgl/p5.RendererGL.Immediate.js b/src/webgl/p5.RendererGL.Immediate.js index dc430dc644..0000dd2680 100644 --- a/src/webgl/p5.RendererGL.Immediate.js +++ b/src/webgl/p5.RendererGL.Immediate.js @@ -412,8 +412,6 @@ p5.RendererGL.prototype._drawImmediateFill = function() { this._applyColorBlend(this.curFillColor); - this.drawImmediateArrays(); - gl.drawArrays( this.immediateMode.shapeMode, 0, From 8f44275dbea4359d9e5f9dc7a1c1f6ca92df2a8a Mon Sep 17 00:00:00 2001 From: Dave Pagurek Date: Mon, 24 Jul 2023 13:45:47 -0400 Subject: [PATCH 05/11] Fix broken constants reference --- src/webgl/GeometryBuilder.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/webgl/GeometryBuilder.js b/src/webgl/GeometryBuilder.js index b5c2d762ca..6ea586f1c8 100644 --- a/src/webgl/GeometryBuilder.js +++ b/src/webgl/GeometryBuilder.js @@ -85,7 +85,7 @@ class GeometryBuilder { faces.push([i, i - 2, i - 1]); } } - } else if (shapeMode === this.p5.TRIANGLE_FAN) { + } else if (shapeMode === constants.TRIANGLE_FAN) { for (let i = 2; i < geometry.vertices.length; i++) { faces.push([0, i - 1, i]); } From b4c388cd2628b4fff48b8ddc7d524a69e0ce57d9 Mon Sep 17 00:00:00 2001 From: Dave Pagurek Date: Tue, 25 Jul 2023 08:19:50 -0400 Subject: [PATCH 06/11] Fix camera/mat3 tests --- src/webgl/p5.Camera.js | 8 ++++---- src/webgl/p5.Matrix.js | 4 ++-- test/unit/webgl/p5.Matrix.js | 19 +++++++++++-------- 3 files changed, 17 insertions(+), 14 deletions(-) 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 2d9ce7fe6c..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){ @@ -888,7 +888,7 @@ 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) { 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([ From 1ac3d29408ce604d76fbbc1245121fdc968527b1 Mon Sep 17 00:00:00 2001 From: Dave Pagurek Date: Tue, 25 Jul 2023 13:13:52 -0400 Subject: [PATCH 07/11] Add docs and tests --- src/core/rendering.js | 33 ----- src/webgl/3d_primitives.js | 173 +++++++++++++++++++++++++++ src/webgl/GeometryBuilder.js | 37 +++++- src/webgl/p5.RendererGL.Immediate.js | 29 ++++- src/webgl/p5.RendererGL.js | 14 +-- test/unit/webgl/p5.Geometry.js | 106 ++++++++++++++++ 6 files changed, 346 insertions(+), 46 deletions(-) diff --git a/src/core/rendering.js b/src/core/rendering.js index 88d24d033c..792d06ed27 100644 --- a/src/core/rendering.js +++ b/src/core/rendering.js @@ -374,39 +374,6 @@ p5.prototype.createFramebuffer = function(options) { return new p5.Framebuffer(this, options); }; -/** - * Starts creating a new p5.Geometry. - * TODO add examples - */ -p5.prototype.beginGeometry = function() { - return this._renderer.beginGeometry(); -}; - -/** - * Finishes creating a new p5.Geometry. - * @returns p5.Geometry The model that was built - */ -p5.prototype.endGeometry = function() { - return this._renderer.endGeometry(); -}; - -/** - * @param callback Function A function that draws shapes to store in a - * p5.Geometry for faster rendering. - * @returns p5.Geometry The model that was built from the draw functions - */ -p5.prototype.buildGeometry = function(callback) { - return this._renderer.buildGeometry(callback); -}; - -/** - * TODO - * @param {p5.Geometry} The geometry whose resources should be freed - */ -p5.prototype.freeGeometry = function(geometry) { - this._renderer._freeBuffers(geometry.gid); -}; - /** * Blends the pixels in the display window according to the defined mode. * There is a choice of the following modes to blend the source pixels (A) diff --git a/src/webgl/3d_primitives.js b/src/webgl/3d_primitives.js index f92ef2f22f..b9b173f9fa 100644 --- a/src/webgl/3d_primitives.js +++ b/src/webgl/3d_primitives.js @@ -10,6 +10,179 @@ 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. + * + * @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 likely + * 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 tree; + * let button; + * + * function setup() { + * createCanvas(100, 100, WEBGL); + * button = createButton('New'); + * button.mousePressed(makeTree); + * makeTree(); + * } + * + * function makeTree() { + * if (tree) freeGeometry(tree); + * tree = buildGeometry(() => { + * const addBranch = function(depth) { + * push(); + * translate(0, -50); + * cylinder(15, 100); + * translate(0, -50); + * if (depth >= 5) { + * sphere(30); + * } else { + * const numChildren = round(random(1, 3)); + * for (let i = 0; i < numChildren; i++) { + * push(); + * rotateZ(random(-0.3, 0.3) * PI); + * rotateY(random(TWO_PI)); + * addBranch(depth + 1); + * pop(); + * } + * } + * pop(); + * }; + * translate(0, 25); + * scale(0.18); + * addBranch(0); + * }); + * } + * + * function draw() { + * background(255); + * lights(); + * noStroke(); + * orbitControl(); + * model(tree); + * } + * + *
+ * + * @alt + * A 3D tree made of multiple cylinders and 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. + * + * @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 index 6ea586f1c8..502f4b2bee 100644 --- a/src/webgl/GeometryBuilder.js +++ b/src/webgl/GeometryBuilder.js @@ -1,6 +1,11 @@ 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; @@ -8,13 +13,14 @@ class GeometryBuilder { this.identityMatrix = new p5.Matrix(); renderer.uMVMatrix = new p5.Matrix(); this.geometry = new p5.Geometry(); - this.geometry.gid = `_p5_GeometryBuilder_${renderer.nextGeometryId}`; - renderer.nextGeometryId++; + 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; @@ -24,6 +30,7 @@ class GeometryBuilder { /** * @private + * Applies the current normal matrix to each normal. */ transformNormals(normals) { if (!this.hasTransform) return normals; @@ -35,6 +42,8 @@ class GeometryBuilder { /** * @private + * Adds a p5.Geometry to the builder's combined geometry, flattening + * transformations. */ addGeometry(input) { this.hasTransform = !this.renderer.uMVMatrix.mat4 @@ -44,7 +53,7 @@ class GeometryBuilder { this.renderer.uNMatrix.inverseTranspose(this.renderer.uMVMatrix); } - const startIdx = this.geometry.vertices.length; + let startIdx = this.geometry.vertices.length; this.geometry.vertices.push(...this.transformVertices(input.vertices)); this.geometry.vertexNormals.push( ...this.transformNormals(input.vertexNormals) @@ -68,6 +77,10 @@ class GeometryBuilder { 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; @@ -95,18 +108,32 @@ class GeometryBuilder { } } } - - this.addGeometry(Object.assign({}, geometry, faces)); + 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; diff --git a/src/webgl/p5.RendererGL.Immediate.js b/src/webgl/p5.RendererGL.Immediate.js index 0000dd2680..b92c81eb79 100644 --- a/src/webgl/p5.RendererGL.Immediate.js +++ b/src/webgl/p5.RendererGL.Immediate.js @@ -263,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; @@ -376,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 = []; @@ -389,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; }; diff --git a/src/webgl/p5.RendererGL.js b/src/webgl/p5.RendererGL.js index 4a2971f61d..1d12603dea 100644 --- a/src/webgl/p5.RendererGL.js +++ b/src/webgl/p5.RendererGL.js @@ -412,7 +412,6 @@ p5.RendererGL = class RendererGL extends p5.Renderer { // When constructing a new p5.Geometry, this will represent the builder this.geometryBuilder = undefined; - this.nextGeometryId = 0; // This redundant property is useful in reminding you that you are // interacting with WebGLRenderingContext, still worth considering future removal @@ -609,15 +608,17 @@ p5.RendererGL = class RendererGL extends p5.Renderer { /** * Starts creating a new p5.Geometry. - * TODO add examples */ 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. - * @returns p5.Geometry The model that was built + * Finishes creating a new p5.Geometry and returns it. + * @returns {p5.Geometry} The combined model that was built. */ endGeometry() { if (!this.geometryBuilder) { @@ -629,9 +630,8 @@ p5.RendererGL = class RendererGL extends p5.Renderer { } /** - * @param callback Function A function that draws shapes to store in a - * p5.Geometry for faster rendering. - * @returns p5.Geometry The model that was built from the draw functions + * @param {Function} callback A function that draws shapes. + * @returns {p5.Geometry} The model that was built from the callback function. */ buildGeometry(callback) { this.beginGeometry(); diff --git a/test/unit/webgl/p5.Geometry.js b/test/unit/webgl/p5.Geometry.js index 64b9b03a82..a97074a57a 100644 --- a/test/unit/webgl/p5.Geometry.js +++ b/test/unit/webgl/p5.Geometry.js @@ -144,4 +144,110 @@ suite('p5.Geometry', function() { assert.equal(geom._addJoin.callCount, 0); }); }); + + suite('buildGeometry', function() { + const checkLights = () => myp5.lights(); + 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, 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, 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]); + }); + }); }); From 66980a6d3e818ac074ad80e5642daa2f195d674b Mon Sep 17 00:00:00 2001 From: Dave Pagurek Date: Tue, 25 Jul 2023 13:25:22 -0400 Subject: [PATCH 08/11] Fix error message --- src/webgl/p5.RendererGL.Retained.js | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/webgl/p5.RendererGL.Retained.js b/src/webgl/p5.RendererGL.Retained.js index c58f8d55a7..308b7310ce 100644 --- a/src/webgl/p5.RendererGL.Retained.js +++ b/src/webgl/p5.RendererGL.Retained.js @@ -8,11 +8,11 @@ import * as constants from '../core/constants'; let hashCount = 0; /** - * @param geometry p5.Geometry The model whose resources will be freed + * @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 freeModel does not have an id!'); + console.warn('The model you passed to freeGeometry does not have an id!'); return; } this._freeBuffers(geometry.gid); From fde8273a60864df0b1cc01c76514da6414bad52d Mon Sep 17 00:00:00 2001 From: Dave Pagurek Date: Tue, 25 Jul 2023 18:09:56 -0400 Subject: [PATCH 09/11] Update documentation and examples --- src/webgl/3d_primitives.js | 59 ++++++++++++++++++-------------------- 1 file changed, 28 insertions(+), 31 deletions(-) diff --git a/src/webgl/3d_primitives.js b/src/webgl/3d_primitives.js index b9b173f9fa..39d7509461 100644 --- a/src/webgl/3d_primitives.js +++ b/src/webgl/3d_primitives.js @@ -17,6 +17,10 @@ import * as constants from '../core/constants'; * 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 @@ -101,8 +105,8 @@ p5.prototype.endGeometry = function() { * 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 likely - * run faster than repeatedly drawing the individual pieces. + * 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 @@ -116,56 +120,45 @@ p5.prototype.endGeometry = function() { * @example *
* - * let tree; + * let particles; * let button; * * function setup() { * createCanvas(100, 100, WEBGL); * button = createButton('New'); - * button.mousePressed(makeTree); - * makeTree(); + * button.mousePressed(makeParticles); + * makeParticles(); * } * - * function makeTree() { - * if (tree) freeGeometry(tree); - * tree = buildGeometry(() => { - * const addBranch = function(depth) { + * function makeParticles() { + * if (particles) freeGeometry(particles); + * + * particles = buildGeometry(() => { + * for (let i = 0; i < 60; i++) { * push(); - * translate(0, -50); - * cylinder(15, 100); - * translate(0, -50); - * if (depth >= 5) { - * sphere(30); - * } else { - * const numChildren = round(random(1, 3)); - * for (let i = 0; i < numChildren; i++) { - * push(); - * rotateZ(random(-0.3, 0.3) * PI); - * rotateY(random(TWO_PI)); - * addBranch(depth + 1); - * pop(); - * } - * } + * translate( + * randomGaussian(0, 20), + * randomGaussian(0, 20), + * randomGaussian(0, 20) + * ); + * sphere(5); * pop(); - * }; - * translate(0, 25); - * scale(0.18); - * addBranch(0); + * } * }); * } * * function draw() { * background(255); - * lights(); * noStroke(); + * lights(); * orbitControl(); - * model(tree); + * model(particles); * } * *
* * @alt - * A 3D tree made of multiple cylinders and spheres. + * A cluster of spheres. */ p5.prototype.buildGeometry = function(callback) { return this._renderer.buildGeometry(callback); @@ -176,6 +169,10 @@ p5.prototype.buildGeometry = function(callback) { * 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 */ From 3ea6e7b9a8f383f811dcdbf8b73b84f8e5591e69 Mon Sep 17 00:00:00 2001 From: Dave Pagurek Date: Wed, 26 Jul 2023 13:17:43 -0400 Subject: [PATCH 10/11] Update docs --- src/webgl/p5.RendererGL.js | 35 ++++++++++++++++++++++++++++++++--- 1 file changed, 32 insertions(+), 3 deletions(-) diff --git a/src/webgl/p5.RendererGL.js b/src/webgl/p5.RendererGL.js index 1d12603dea..65ca2523eb 100644 --- a/src/webgl/p5.RendererGL.js +++ b/src/webgl/p5.RendererGL.js @@ -607,7 +607,17 @@ p5.RendererGL = class RendererGL extends p5.Renderer { } /** - * Starts creating a new p5.Geometry. + * 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) { @@ -617,8 +627,13 @@ p5.RendererGL = class RendererGL extends p5.Renderer { } /** - * Finishes creating a new p5.Geometry and returns it. - * @returns {p5.Geometry} The combined model that was built. + * 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) { @@ -630,6 +645,20 @@ p5.RendererGL = class RendererGL extends p5.Renderer { } /** + * 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. */ From a9f4eb3de4acf232106d60fafc029afa051e512a Mon Sep 17 00:00:00 2001 From: Dave Pagurek Date: Thu, 27 Jul 2023 11:27:43 -0400 Subject: [PATCH 11/11] Fix vertex colors + add materials test --- src/webgl/GeometryBuilder.js | 15 +++++++++++++++ test/unit/webgl/p5.Geometry.js | 12 ++++++++++-- 2 files changed, 25 insertions(+), 2 deletions(-) diff --git a/src/webgl/GeometryBuilder.js b/src/webgl/GeometryBuilder.js index 502f4b2bee..289786b651 100644 --- a/src/webgl/GeometryBuilder.js +++ b/src/webgl/GeometryBuilder.js @@ -126,6 +126,21 @@ class GeometryBuilder { */ 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; } } diff --git a/test/unit/webgl/p5.Geometry.js b/test/unit/webgl/p5.Geometry.js index a97074a57a..0b39a4b95e 100644 --- a/test/unit/webgl/p5.Geometry.js +++ b/test/unit/webgl/p5.Geometry.js @@ -147,6 +147,14 @@ suite('p5.Geometry', function() { 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); @@ -190,7 +198,7 @@ suite('p5.Geometry', function() { myp5.rotateY(myp5.PI * 0.2); } myp5.pop(); - }, [checkLights, checkNormals]); + }, [checkLights, checkMaterials, checkNormals]); }); test('Immediate mode constructs are translated correctly', function() { @@ -225,7 +233,7 @@ suite('p5.Geometry', function() { myp5.vertex(-120, -110); myp5.vertex(-105, -100); myp5.endShape(); - }, [checkLights, checkNormals]); + }, [checkLights, checkMaterials, checkNormals]); }); test('Vertex colors are captured', function() {