From bfa549fab18d9f45aa7821bee9eba4b744a9eebb Mon Sep 17 00:00:00 2001 From: Dave Pagurek Date: Wed, 26 Jul 2023 10:32:47 -0400 Subject: [PATCH 1/2] Add contours support for WebGL --- src/core/shape/vertex.js | 12 +++- src/webgl/p5.Geometry.js | 95 +++++++++++++++++++--------- src/webgl/p5.RendererGL.Immediate.js | 63 ++++++++++++++---- src/webgl/p5.RendererGL.js | 5 ++ test/unit/webgl/p5.RendererGL.js | 35 ++++++++++ 5 files changed, 167 insertions(+), 43 deletions(-) diff --git a/src/core/shape/vertex.js b/src/core/shape/vertex.js index b6abe9aec6..a51cd97b5c 100644 --- a/src/core/shape/vertex.js +++ b/src/core/shape/vertex.js @@ -59,8 +59,12 @@ let isFirstContour = true; * white rect and smaller grey rect with red outlines in center of canvas. */ p5.prototype.beginContour = function() { - contourVertices = []; - isContour = true; + if (this._renderer.isP3D) { + this._renderer.beginContour(); + } else { + contourVertices = []; + isContour = true; + } return this; }; @@ -563,6 +567,10 @@ p5.prototype.curveVertex = function(...args) { * white rect and smaller grey rect with red outlines in center of canvas. */ p5.prototype.endContour = function() { + if (this._renderer.isP3D) { + return this; + } + const vert = contourVertices[0].slice(); // copy all data vert.isVert = contourVertices[0].isVert; vert.moveTo = false; diff --git a/src/webgl/p5.Geometry.js b/src/webgl/p5.Geometry.js index 4d05315bf5..ef63ea5c96 100644 --- a/src/webgl/p5.Geometry.js +++ b/src/webgl/p5.Geometry.js @@ -266,10 +266,8 @@ p5.Geometry = class { this.lineTangentsOut.length = 0; this.lineSides.length = 0; - const closed = - this.edges.length > 1 && - this.edges[0][0] === this.edges[this.edges.length - 1][1]; - let addedStartingCap = false; + const potentialCaps = new Map(); + const connected = new Set(); let lastValidDir; for (let i = 0; i < this.edges.length; i++) { const prevEdge = this.edges[i - 1]; @@ -298,46 +296,80 @@ p5.Geometry = class { } if (i > 0 && prevEdge[1] === currEdge[0]) { - // Add a join if this segment shares a vertex with the previous. Skip - // actually adding join vertices if either the previous segment or this - // one has a length of 0. - // - // Don't add a join if the tangents point in the same direction, which - // would mean the edges line up exactly, and there is no need for a join. - if (lastValidDir && dirOK && dir.dot(lastValidDir) < 1 - 1e-8) { - this._addJoin(begin, lastValidDir, dir, fromColor); - } - if (dirOK && !addedStartingCap && !closed) { - this._addCap(begin, dir.copy().mult(-1), fromColor); - addedStartingCap = true; + if (!connected.has(currEdge[0])) { + connected.add(currEdge[0]); + potentialCaps.delete(currEdge[0]); + // Add a join if this segment shares a vertex with the previous. Skip + // actually adding join vertices if either the previous segment or this + // one has a length of 0. + // + // Don't add a join if the tangents point in the same direction, which + // would mean the edges line up exactly, and there is no need for a join. + if (lastValidDir && dirOK && dir.dot(lastValidDir) < 1 - 1e-8) { + this._addJoin(begin, lastValidDir, dir, fromColor); + } } } else { - addedStartingCap = false; // Start a new line - if (dirOK && (!closed || i > 0)) { - this._addCap(begin, dir.copy().mult(-1), fromColor); - addedStartingCap = true; + if (dirOK && !connected.has(currEdge[0])) { + const existingCap = potentialCaps.get(currEdge[0]); + if (existingCap) { + this._addJoin( + begin, + existingCap.dir, + dir, + fromColor + ); + potentialCaps.delete(currEdge[0]); + connected.add(currEdge[0]); + } else { + potentialCaps.set(currEdge[0], { + point: begin, + dir: dir.copy().mult(-1), + color: fromColor + }); + } } - if (lastValidDir && (!closed || i < this.edges.length - 1)) { - // Close off the last segment with a cap - this._addCap(this.vertices[prevEdge[1]], lastValidDir, fromColor); + if (lastValidDir && !connected.has(prevEdge[1])) { + const existingCap = potentialCaps.get(prevEdge[1]); + if (existingCap) { + this._addJoin( + this.vertices[prevEdge[1]], + lastValidDir, + existingCap.dir.copy().mult(-1), + fromColor + ); + potentialCaps.delete(prevEdge[1]); + connected.add(prevEdge[1]); + } else { + // Close off the last segment with a cap + potentialCaps.set(prevEdge[1], { + point: this.vertices[prevEdge[1]], + dir: lastValidDir, + color: fromColor + }); + } lastValidDir = undefined; } } - if (i === this.edges.length - 1) { - if (closed) { + if (i === this.edges.length - 1 && !connected.has(currEdge[1])) { + const existingCap = potentialCaps.get(currEdge[1]); + if (existingCap) { this._addJoin( end, dir, - this.vertices[this.edges[0][1]] - .copy() - .sub(end) - .normalize(), + existingCap.dir.copy().mult(-1), toColor ); + potentialCaps.delete(currEdge[1]); + connected.add(currEdge[1]); } else { - this._addCap(end, dir, toColor); + potentialCaps.set(currEdge[1], { + point: end, + dir, + color: toColor + }); } } @@ -345,6 +377,9 @@ p5.Geometry = class { lastValidDir = dir; } } + for (const { point, dir, color } of potentialCaps.values()) { + this._addCap(point, dir, color); + } return this; } diff --git a/src/webgl/p5.RendererGL.Immediate.js b/src/webgl/p5.RendererGL.Immediate.js index 442d37ddfa..1ae9940d7b 100644 --- a/src/webgl/p5.RendererGL.Immediate.js +++ b/src/webgl/p5.RendererGL.Immediate.js @@ -34,6 +34,7 @@ p5.RendererGL.prototype.beginShape = function(mode) { this.immediateMode.shapeMode = mode !== undefined ? mode : constants.TESS; this.immediateMode.geometry.reset(); + this.immediateMode.contourIndices = []; return this; }; @@ -45,6 +46,15 @@ const immediateBufferStrides = { uvs: 2 }; +p5.RendererGL.prototype.beginContour = function() { + if (this.immediateMode.shapeMode !== constants.TESS) { + throw new Error('WebGL mode can only use contours with beginShape(TESS).'); + } + this.immediateMode.contourIndices.push( + this.immediateMode.geometry.vertices.length + ); +}; + /** * adds a vertex to be drawn in a custom Shape. * @private @@ -238,10 +248,19 @@ p5.RendererGL.prototype._processVertices = function(mode) { } // For hollow shapes, user must set mode to TESS const convexShape = this.immediateMode.shapeMode === constants.TESS; + // If the shape has a contour, we have to re-triangulate to cut out the + // contour region + const hasContour = this.immediateMode.contourIndices.length > 0; // We tesselate when drawing curves or convex shapes const shouldTess = this._doFill && - (this.isBezier || this.isQuadratic || this.isCurve || convexShape) && + ( + this.isBezier || + this.isQuadratic || + this.isCurve || + convexShape || + hasContour + ) && this.immediateMode.shapeMode !== constants.LINES; if (shouldTess) { @@ -262,6 +281,8 @@ p5.RendererGL.prototype._calculateEdges = function( ) { const res = []; let i = 0; + const contourIndices = this.immediateMode.contourIndices.slice(); + let contourStart = 0; switch (shapeMode) { case constants.TRIANGLE_STRIP: for (i = 0; i < verts.length - 2; i++) { @@ -313,12 +334,23 @@ p5.RendererGL.prototype._calculateEdges = function( res.push([i, i + 1]); break; default: - for (i = 0; i < verts.length - 1; i++) { - res.push([i, i + 1]); + // TODO: handle contours in other modes too + for (i = 0; i < verts.length; i++) { + // Handle breaks between contours + if (i + 1 < verts.length && i + 1 !== contourIndices[0]) { + res.push([i, i + 1]); + } else { + if (shouldClose || contourStart) { + res.push([i, contourStart]); + } + if (contourIndices.length > 0) { + contourStart = contourIndices.shift(); + } + } } break; } - if (shouldClose) { + if (shapeMode !== constants.TESS && shouldClose) { res.push([verts.length - 1, 0]); } return res; @@ -329,12 +361,21 @@ p5.RendererGL.prototype._calculateEdges = function( * @private */ p5.RendererGL.prototype._tesselateShape = function() { + // TODO: handle non-TESS shape modes that have contours this.immediateMode.shapeMode = constants.TRIANGLES; - const contours = [ - this._flatten(this.immediateMode.geometry.vertices.map((vert, i) => [ - vert.x, - vert.y, - vert.z, + const contours = [[]]; + for (let i = 0; i < this.immediateMode.geometry.vertices.length; i++) { + if ( + this.immediateMode.contourIndices.length > 0 && + this.immediateMode.contourIndices[0] === i + ) { + this.immediateMode.contourIndices.shift(); + contours.push([]); + } + contours[contours.length-1].push( + this.immediateMode.geometry.vertices[i].x, + this.immediateMode.geometry.vertices[i].y, + this.immediateMode.geometry.vertices[i].z, this.immediateMode.geometry.uvs[i * 2], this.immediateMode.geometry.uvs[i * 2 + 1], this.immediateMode.geometry.vertexColors[i * 4], @@ -344,8 +385,8 @@ p5.RendererGL.prototype._tesselateShape = function() { this.immediateMode.geometry.vertexNormals[i].x, this.immediateMode.geometry.vertexNormals[i].y, this.immediateMode.geometry.vertexNormals[i].z - ])) - ]; + ); + } const polyTriangles = this._triangulate(contours); this.immediateMode.geometry.vertices = []; this.immediateMode.geometry.vertexNormals = []; diff --git a/src/webgl/p5.RendererGL.js b/src/webgl/p5.RendererGL.js index ee378ed828..6937a5792f 100644 --- a/src/webgl/p5.RendererGL.js +++ b/src/webgl/p5.RendererGL.js @@ -543,6 +543,7 @@ p5.RendererGL = class RendererGL extends p5.Renderer { this.immediateMode = { geometry: new p5.Geometry(), shapeMode: constants.TRIANGLE_FAN, + contourIndices: [], _bezierVertex: [], _quadraticVertex: [], _curveVertex: [], @@ -1763,6 +1764,10 @@ p5.RendererGL = class RendererGL extends p5.Renderer { tessy.gluTessCallback(libtess.gluEnum.GLU_TESS_ERROR, errorcallback); tessy.gluTessCallback(libtess.gluEnum.GLU_TESS_COMBINE, combinecallback); tessy.gluTessCallback(libtess.gluEnum.GLU_TESS_EDGE_FLAG, edgeCallback); + tessy.gluTessProperty( + libtess.gluEnum.GLU_TESS_WINDING_RULE, + libtess.windingRule.GLU_TESS_WINDING_NONZERO + ); return tessy; } diff --git a/test/unit/webgl/p5.RendererGL.js b/test/unit/webgl/p5.RendererGL.js index 0201eb440f..d075bbaa29 100644 --- a/test/unit/webgl/p5.RendererGL.js +++ b/test/unit/webgl/p5.RendererGL.js @@ -118,6 +118,41 @@ suite('p5.RendererGL', function() { }); }); + test('contours match 2D', function() { + const getColors = function(mode) { + myp5.createCanvas(50, 50, mode); + myp5.pixelDensity(1); + myp5.background(200); + myp5.strokeCap(myp5.SQUARE); + myp5.strokeJoin(myp5.MITER); + if (mode === myp5.WEBGL) { + myp5.translate(-myp5.width/2, -myp5.height/2); + } + myp5.stroke('black'); + myp5.strokeWeight(1); + myp5.translate(25, 25); + myp5.beginShape(); + // Exterior part of shape, clockwise winding + myp5.vertex(-20, -20); + myp5.vertex(20, -20); + myp5.vertex(20, 20); + myp5.vertex(-20, 20); + // Interior part of shape, counter-clockwise winding + myp5.beginContour(); + myp5.vertex(-10, -10); + myp5.vertex(-10, 10); + myp5.vertex(10, 10); + myp5.vertex(10, -10); + myp5.endContour(); + myp5.endShape(myp5.CLOSE); + console.log(myp5._renderer.elt.toDataURL()); + myp5.loadPixels(); + return [...myp5.pixels]; + }; + + assert.deepEqual(getColors(myp5.P2D), getColors(myp5.WEBGL)); + }); + suite('text shader', function() { test('rendering looks the same in WebGL1 and 2', function(done) { myp5.loadFont('manual-test-examples/p5.Font/Inconsolata-Bold.ttf', function(font) { From be73d35774b50317c3ef06e6c3c7c5fd44a0d46f Mon Sep 17 00:00:00 2001 From: Dave Pagurek Date: Wed, 26 Jul 2023 10:42:42 -0400 Subject: [PATCH 2/2] Fix bug with 2D paths not closing properly --- src/core/p5.Renderer2D.js | 1 + test/unit/webgl/p5.RendererGL.js | 3 +-- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/src/core/p5.Renderer2D.js b/src/core/p5.Renderer2D.js index c355333f13..ed77da4a56 100644 --- a/src/core/p5.Renderer2D.js +++ b/src/core/p5.Renderer2D.js @@ -1016,6 +1016,7 @@ class Renderer2D extends p5.Renderer{ v = vertices[i]; if (v.isVert) { if (v.moveTo) { + if (closeShape) this.drawingContext.closePath(); this.drawingContext.moveTo(v[0], v[1]); } else { this.drawingContext.lineTo(v[0], v[1]); diff --git a/test/unit/webgl/p5.RendererGL.js b/test/unit/webgl/p5.RendererGL.js index d075bbaa29..dbbe569e44 100644 --- a/test/unit/webgl/p5.RendererGL.js +++ b/test/unit/webgl/p5.RendererGL.js @@ -129,7 +129,7 @@ suite('p5.RendererGL', function() { myp5.translate(-myp5.width/2, -myp5.height/2); } myp5.stroke('black'); - myp5.strokeWeight(1); + myp5.strokeWeight(2); myp5.translate(25, 25); myp5.beginShape(); // Exterior part of shape, clockwise winding @@ -145,7 +145,6 @@ suite('p5.RendererGL', function() { myp5.vertex(10, -10); myp5.endContour(); myp5.endShape(myp5.CLOSE); - console.log(myp5._renderer.elt.toDataURL()); myp5.loadPixels(); return [...myp5.pixels]; };