From 81a10f04c37a09c37a4139087edc5608252d44db Mon Sep 17 00:00:00 2001 From: Srinidhi-4419 Date: Fri, 2 Oct 2026 13:19:09 +0530 Subject: [PATCH 1/4] fix: preserve vertex attributes with smooth normals --- src/webgl/p5.Geometry.js | 77 +++++++++++++++++----------------- test/unit/webgl/p5.Geometry.js | 34 +++++++++++++++ 2 files changed, 73 insertions(+), 38 deletions(-) diff --git a/src/webgl/p5.Geometry.js b/src/webgl/p5.Geometry.js index 5f7dfeff78..10a23bb813 100644 --- a/src/webgl/p5.Geometry.js +++ b/src/webgl/p5.Geometry.js @@ -1193,74 +1193,75 @@ class Geometry { */ computeNormals(shadingType = constants.FLAT, { roundToPrecision = 3 } = {}) { const vertexNormals = this.vertexNormals; - let vertices = this.vertices; + const vertices = this.vertices; const faces = this.faces; let iv; if (shadingType === constants.SMOOTH) { const vertexIndices = {}; const uniqueVertices = []; + const originalToUnique = []; const power = Math.pow(10, roundToPrecision); const rounded = val => Math.round(val * power) / power; const getKey = vert => `${rounded(vert.x)},${rounded(vert.y)},${rounded(vert.z)}`; - // loop through each vertex and add uniqueVertices for (let i = 0; i < vertices.length; i++) { const vertex = vertices[i]; const key = getKey(vertex); + if (vertexIndices[key] === undefined) { vertexIndices[key] = uniqueVertices.length; uniqueVertices.push(vertex); } + + originalToUnique[i] = vertexIndices[key]; } - // update face indices to use the deduplicated vertex indices - faces.forEach(face => { - for (let fv = 0; fv < 3; ++fv) { - const originalVertexIndex = face[fv]; - const originalVertex = vertices[originalVertexIndex]; - const key = getKey(originalVertex); - face[fv] = vertexIndices[key]; - } - }); + const smoothNormals = []; - // update edge indices to use the deduplicated vertex indices - this.edges.forEach(edge => { - for (let ev = 0; ev < 2; ++ev) { - const originalVertexIndex = edge[ev]; - const originalVertex = vertices[originalVertexIndex]; - const key = getKey(originalVertex); - edge[ev] = vertexIndices[key]; + for (iv = 0; iv < uniqueVertices.length; ++iv) { + smoothNormals.push(new Vector(0, 0, 0)); + } + + faces.forEach((face, f) => { + const faceNormal = this._getFaceNormal(f); + + for (let fv = 0; fv < 3; ++fv) { + const vertexIndex = originalToUnique[face[fv]]; + smoothNormals[vertexIndex].add(faceNormal); } }); - // update the deduplicated vertices - this.vertices = vertices = uniqueVertices; - } + for (iv = 0; iv < smoothNormals.length; ++iv) { + smoothNormals[iv].normalize(); + } - // initialize the vertexNormals array with empty vectors - vertexNormals.length = 0; - for (iv = 0; iv < vertices.length; ++iv) { - vertexNormals.push(new Vector(0, 0, 0)); - } + vertexNormals.length = 0; - // loop through all the faces adding its normal to the normal - // of each of its vertices - faces.forEach((face, f) => { - const faceNormal = this._getFaceNormal(f); + for (iv = 0; iv < vertices.length; ++iv) { + vertexNormals.push(smoothNormals[originalToUnique[iv]].copy()); + } + } else { + vertexNormals.length = 0; - // all three vertices get the normal added - for (let fv = 0; fv < 3; ++fv) { - const vertexIndex = face[fv]; - vertexNormals[vertexIndex].add(faceNormal); + for (iv = 0; iv < vertices.length; ++iv) { + vertexNormals.push(new Vector(0, 0, 0)); } - }); - // normalize the normals - for (iv = 0; iv < vertices.length; ++iv) { - vertexNormals[iv].normalize(); + faces.forEach((face, f) => { + const faceNormal = this._getFaceNormal(f); + + for (let fv = 0; fv < 3; ++fv) { + const vertexIndex = face[fv]; + vertexNormals[vertexIndex].add(faceNormal); + } + }); + + for (iv = 0; iv < vertices.length; ++iv) { + vertexNormals[iv].normalize(); + } } return this; diff --git a/test/unit/webgl/p5.Geometry.js b/test/unit/webgl/p5.Geometry.js index 035237b091..2531bfca66 100644 --- a/test/unit/webgl/p5.Geometry.js +++ b/test/unit/webgl/p5.Geometry.js @@ -15,6 +15,40 @@ suite('p5.Geometry', function () { myp5.remove(); }); + suite('computeNormals', function () { + test('SMOOTH preserves vertices with different UVs', function () { + const geom = new p5.Geometry(); + + geom.vertices.push( + myp5.createVector(0, 0, 0), + myp5.createVector(1, 0, 0), + myp5.createVector(0, 1, 0), + myp5.createVector(0, 0, 0) + ); + + geom.uvs.push( + 0, 0, + 1, 0, + 0, 1, + 1, 1 + ); + + geom.faces.push( + [0, 1, 2], + [3, 2, 1] + ); + + const originalVertexCount = geom.vertices.length; + const originalUvs = [...geom.uvs]; + + geom.computeNormals(myp5.SMOOTH); + + expect(geom.vertices.length).toEqual(originalVertexCount); + expect(geom.uvs).toEqual(originalUvs); + expect(geom.vertexNormals[0]).toEqual(geom.vertexNormals[3]); + }); + }); + suite('computeTangents', function () { test('a uv-mapped triangle gets a +u tangent with correct handedness', function () { From 0c30b9960cc005aa906169cc929d03e889238efc Mon Sep 17 00:00:00 2001 From: Srinidhi-4419 Date: Sun, 4 Oct 2026 21:06:58 +0530 Subject: [PATCH 2/4] test: add visual test for smooth normals --- test/unit/visual/cases/webgl.js | 65 +++++++++++++++++++++++++++++++++ 1 file changed, 65 insertions(+) diff --git a/test/unit/visual/cases/webgl.js b/test/unit/visual/cases/webgl.js index e2e40d2843..5cd37797a0 100644 --- a/test/unit/visual/cases/webgl.js +++ b/test/unit/visual/cases/webgl.js @@ -1100,6 +1100,71 @@ visualSuite('WebGL', function () { screenshot(); } ); + visualTest( + 'computeNormals(SMOOTH) preserves smooth normals', + function (p5, screenshot) { + p5.createCanvas(100, 100, p5.WEBGL); + + const geom = p5.buildGeometry(() => { + const verticesPerRing = 20; + const rings = 20; + + for (let ring = 0; ring < rings - 1; ring++) { + p5.beginShape(p5.QUAD_STRIP); + + for (let i = 0; i <= verticesPerRing; i++) { + for (const ringOffset of [0, 1]) { + const y = p5.map( + ring + ringOffset, + 0, + rings, + 40, + -40 + ); + const angle = p5.map( + i, + 0, + verticesPerRing, + 0, + p5.TWO_PI + ); + + const position = p5.createVector(40, 0).rotate(angle); + + const radius = p5.noise( + 200 + position.x * 0.01, + 200 + y * 0.01, + 200 + position.y * 0.01 + ); + + const squishedPosition = p5.createVector( + position.x * radius, + y, + position.y * radius + ); + + p5.vertex( + squishedPosition.x, + squishedPosition.y, + squishedPosition.z + ); + } + } + + p5.endShape(); + } + }); + + geom.computeNormals(p5.SMOOTH); + + p5.background(255); + p5.normalMaterial(); + p5.rotateY(0.5); + p5.model(geom); + + screenshot(); + } + ); }); visualSuite('font data', () => { From 9d5ed8b576cea868f648d1143e16c6b06975fe65 Mon Sep 17 00:00:00 2001 From: Srinidhi-4419 Date: Tue, 6 Oct 2026 20:30:30 +0530 Subject: [PATCH 3/4] test: add visual test screenshot for smooth normals --- .../000.png | Bin 0 -> 5260 bytes .../metadata.json | 3 +++ 2 files changed, 3 insertions(+) create mode 100644 test/unit/visual/screenshots/WebGL/buildGeometry()/computeNormals(SMOOTH) preserves smooth normals/000.png create mode 100644 test/unit/visual/screenshots/WebGL/buildGeometry()/computeNormals(SMOOTH) preserves smooth normals/metadata.json diff --git a/test/unit/visual/screenshots/WebGL/buildGeometry()/computeNormals(SMOOTH) preserves smooth normals/000.png b/test/unit/visual/screenshots/WebGL/buildGeometry()/computeNormals(SMOOTH) preserves smooth normals/000.png new file mode 100644 index 0000000000000000000000000000000000000000..90364077c8199e751aed2c8cbd470f9d02381d17 GIT binary patch literal 5260 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 S8w7&@0000 Date: Wed, 7 Oct 2026 20:19:52 +0530 Subject: [PATCH 4/4] test: make smooth normals visual test deterministic --- test/unit/visual/cases/webgl.js | 2 +- .../000.png | Bin 5260 -> 6445 bytes 2 files changed, 1 insertion(+), 1 deletion(-) diff --git a/test/unit/visual/cases/webgl.js b/test/unit/visual/cases/webgl.js index 5cd37797a0..757e597e47 100644 --- a/test/unit/visual/cases/webgl.js +++ b/test/unit/visual/cases/webgl.js @@ -1104,7 +1104,7 @@ visualSuite('WebGL', function () { 'computeNormals(SMOOTH) preserves smooth normals', function (p5, screenshot) { p5.createCanvas(100, 100, p5.WEBGL); - + p5.noiseSeed(0); const geom = p5.buildGeometry(() => { const verticesPerRing = 20; const rings = 20; diff --git a/test/unit/visual/screenshots/WebGL/buildGeometry()/computeNormals(SMOOTH) preserves smooth normals/000.png b/test/unit/visual/screenshots/WebGL/buildGeometry()/computeNormals(SMOOTH) preserves smooth normals/000.png index 90364077c8199e751aed2c8cbd470f9d02381d17..a9df86a1e728eb20c333b936e5acf0a07cc4f037 100644 GIT binary patch literal 6445 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 Hu0mjfClg^_ literal 5260 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 S8w7&@0000