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) {