From bec1f51a1973595deed203c14af7fc92e982f73e Mon Sep 17 00:00:00 2001 From: Avinash Kumar Deepak Date: Sun, 26 Oct 2025 23:25:00 +0530 Subject: [PATCH 1/2] fix(webgl): normalize nearly identical vertices before tessellation --- src/webgl/p5.RendererGL.Immediate.js | 31 ++++++++++++++++++ test/unit/webgl/p5.RendererGL.js | 49 ++++++++++++++++++++++++++++ 2 files changed, 80 insertions(+) diff --git a/src/webgl/p5.RendererGL.Immediate.js b/src/webgl/p5.RendererGL.Immediate.js index 31ce48f630..8e7e4bfb89 100644 --- a/src/webgl/p5.RendererGL.Immediate.js +++ b/src/webgl/p5.RendererGL.Immediate.js @@ -436,6 +436,37 @@ p5.RendererGL.prototype._tesselateShape = function() { this.immediateMode.geometry.vertexNormals[i].z ); } + + // Normalize nearly identical consecutive vertices to avoid numerical issues in libtess. + // This workaround addresses tessellation artifacts when consecutive vertices have + // coordinates that are almost (but not exactly) equal, which can occur when drawing + // contours automatically sampled from fonts with font.textToContours(). + const epsilon = 1e-6; + for (const contour of contours) { + for ( + let i = p5.RendererGL.prototype.tessyVertexSize; + i < contour.length; + i += p5.RendererGL.prototype.tessyVertexSize + ) { + const prevX = contour[i - p5.RendererGL.prototype.tessyVertexSize]; + const prevY = contour[i - p5.RendererGL.prototype.tessyVertexSize + 1]; + const prevZ = contour[i - p5.RendererGL.prototype.tessyVertexSize + 2]; + const currX = contour[i]; + const currY = contour[i + 1]; + const currZ = contour[i + 2]; + + if (Math.abs(prevX - currX) < epsilon) { + contour[i] = prevX; + } + if (Math.abs(prevY - currY) < epsilon) { + contour[i + 1] = prevY; + } + if (Math.abs(prevZ - currZ) < epsilon) { + contour[i + 2] = prevZ; + } + } + } + const polyTriangles = this._triangulate(contours); const originalVertices = this.immediateMode.geometry.vertices; this.immediateMode.geometry.vertices = []; diff --git a/test/unit/webgl/p5.RendererGL.js b/test/unit/webgl/p5.RendererGL.js index 3c7e3df1a2..33e960c3c1 100644 --- a/test/unit/webgl/p5.RendererGL.js +++ b/test/unit/webgl/p5.RendererGL.js @@ -562,6 +562,55 @@ suite('p5.RendererGL', function() { assert.deepEqual(getColors(myp5.P2D), getColors(myp5.WEBGL)); }); + test('tessellation handles nearly identical consecutive vertices', function() { + myp5.createCanvas(100, 100, myp5.WEBGL); + myp5.pixelDensity(1); + myp5.background(255); + myp5.fill(0); + myp5.noStroke(); + + // Contours with nearly identical consecutive vertices (as can occur with textToContours) + const contours = [ + [ + [-30, -30, 0], + [30, -30, 0], + [30, 30, 0], + [-30, 30, 0] + ], + [ + [-10, -10, 0], + [-10, 10, 0], + // This vertex has x coordinate almost equal to previous + [10.00000001, 10, 0], + [10, -10, 0] + ] + ]; + + myp5.beginShape(); + for (const contour of contours) { + if (contour !== contours[0]) { + myp5.beginContour(); + } + for (const v of contour) { + myp5.vertex(...v); + } + if (contour !== contours[0]) { + myp5.endContour(); + } + } + myp5.endShape(myp5.CLOSE); + + myp5.loadPixels(); + + // Check that center pixels are white (hole cut out properly) + const centerIdx = (myp5.width / 2 + myp5.height / 2 * myp5.width) * 4; + assert.equal(myp5.pixels[centerIdx], 255, 'Center should be white (hole)'); + + // Check that corner pixels are black (fill) + const cornerIdx = (10 + 10 * myp5.width) * 4; + assert.equal(myp5.pixels[cornerIdx], 0, 'Corner should be black (filled)'); + }); + 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 8254f05f4bb5c79725f746c388add089e86cff8d Mon Sep 17 00:00:00 2001 From: Avinash Kumar Deepak Date: Fri, 31 Oct 2025 23:02:06 +0530 Subject: [PATCH 2/2] refactor(test): convert tessellation test to visual test Converted pixel-checking unit test to visual test as suggested by maintainer. This makes the test easier to debug when issues arise in the future. --- test/unit/visual/cases/webgl.js | 31 ++++++++++++++++++++ test/unit/webgl/p5.RendererGL.js | 49 -------------------------------- 2 files changed, 31 insertions(+), 49 deletions(-) diff --git a/test/unit/visual/cases/webgl.js b/test/unit/visual/cases/webgl.js index 2f36feb806..0bf2fd7292 100644 --- a/test/unit/visual/cases/webgl.js +++ b/test/unit/visual/cases/webgl.js @@ -141,4 +141,35 @@ visualSuite('WebGL', function() { screenshot(); }); }); + + visualSuite('Tessellation', function() { + visualTest('Handles nearly identical consecutive vertices', function(p5, screenshot) { + p5.createCanvas(100, 100, p5.WEBGL); + p5.pixelDensity(1); + p5.background(255); + p5.fill(0); + p5.noStroke(); + + // Contours with nearly identical consecutive vertices (as can occur with textToContours) + // Outer contour + p5.beginShape(); + p5.vertex(-30, -30, 0); + p5.vertex(30, -30, 0); + p5.vertex(30, 30, 0); + p5.vertex(-30, 30, 0); + + // Inner contour (hole) with nearly identical vertices + p5.beginContour(); + p5.vertex(-10, -10, 0); + p5.vertex(-10, 10, 0); + // This vertex has x coordinate almost equal to previous (10.00000001 vs 10) + p5.vertex(10.00000001, 10, 0); + p5.vertex(10, -10, 0); + p5.endContour(); + + p5.endShape(p5.CLOSE); + + screenshot(); + }); + }); }); diff --git a/test/unit/webgl/p5.RendererGL.js b/test/unit/webgl/p5.RendererGL.js index 33e960c3c1..3c7e3df1a2 100644 --- a/test/unit/webgl/p5.RendererGL.js +++ b/test/unit/webgl/p5.RendererGL.js @@ -562,55 +562,6 @@ suite('p5.RendererGL', function() { assert.deepEqual(getColors(myp5.P2D), getColors(myp5.WEBGL)); }); - test('tessellation handles nearly identical consecutive vertices', function() { - myp5.createCanvas(100, 100, myp5.WEBGL); - myp5.pixelDensity(1); - myp5.background(255); - myp5.fill(0); - myp5.noStroke(); - - // Contours with nearly identical consecutive vertices (as can occur with textToContours) - const contours = [ - [ - [-30, -30, 0], - [30, -30, 0], - [30, 30, 0], - [-30, 30, 0] - ], - [ - [-10, -10, 0], - [-10, 10, 0], - // This vertex has x coordinate almost equal to previous - [10.00000001, 10, 0], - [10, -10, 0] - ] - ]; - - myp5.beginShape(); - for (const contour of contours) { - if (contour !== contours[0]) { - myp5.beginContour(); - } - for (const v of contour) { - myp5.vertex(...v); - } - if (contour !== contours[0]) { - myp5.endContour(); - } - } - myp5.endShape(myp5.CLOSE); - - myp5.loadPixels(); - - // Check that center pixels are white (hole cut out properly) - const centerIdx = (myp5.width / 2 + myp5.height / 2 * myp5.width) * 4; - assert.equal(myp5.pixels[centerIdx], 255, 'Center should be white (hole)'); - - // Check that corner pixels are black (fill) - const cornerIdx = (10 + 10 * myp5.width) * 4; - assert.equal(myp5.pixels[cornerIdx], 0, 'Corner should be black (filled)'); - }); - 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) {