From 3d1f4d66f848e2a5fd2609f9ff9273b7d6416e63 Mon Sep 17 00:00:00 2001 From: Avinash Kumar Deepak Date: Mon, 9 Feb 2026 12:21:11 +0530 Subject: [PATCH] Add support for negative vertex indices in OBJ loader for 2.x --- src/webgl/loading.js | 18 ++++++++---------- test/unit/assets/cube-negative-indices.obj | 20 ++++++++++++++++++++ test/unit/io/loadModel.js | 18 ++++++++++++++++++ 3 files changed, 46 insertions(+), 10 deletions(-) create mode 100644 test/unit/assets/cube-negative-indices.obj diff --git a/src/webgl/loading.js b/src/webgl/loading.js index 9a9707725a..1e804a4c4a 100755 --- a/src/webgl/loading.js +++ b/src/webgl/loading.js @@ -593,12 +593,10 @@ function loading(p5, fn){ const vertString = tokens[vertexTokens[tokenInd]]; let vertParts = vertString.split('/'); - // TODO: Faces can technically use negative numbers to refer to the - // previous nth vertex. I haven't seen this used in practice, but - // it might be good to implement this in the future. - for (let i = 0; i < vertParts.length; i++) { - vertParts[i] = parseInt(vertParts[i]) - 1; + let index = parseInt(vertParts[i]); + if (index > 0) index -= 1; // OBJ uses 1-based indexing + vertParts[i] = index; } if (!usedVerts[vertString]) { @@ -607,11 +605,11 @@ function loading(p5, fn){ if (usedVerts[vertString][currentMaterial] === undefined) { const vertIndex = model.vertices.length; - model.vertices.push(loadedVerts.v[vertParts[0]].copy()); - model.uvs.push(loadedVerts.vt[vertParts[1]] ? - loadedVerts.vt[vertParts[1]].slice() : [0, 0]); - model.vertexNormals.push(loadedVerts.vn[vertParts[2]] ? - loadedVerts.vn[vertParts[2]].copy() : new Vector()); + model.vertices.push(loadedVerts.v.at(vertParts[0]).copy()); + model.uvs.push(loadedVerts.vt.at(vertParts[1]) ? + loadedVerts.vt.at(vertParts[1]).slice() : [0, 0]); + model.vertexNormals.push(loadedVerts.vn.at(vertParts[2]) ? + loadedVerts.vn.at(vertParts[2]).copy() : new Vector()); usedVerts[vertString][currentMaterial] = vertIndex; face.push(vertIndex); diff --git a/test/unit/assets/cube-negative-indices.obj b/test/unit/assets/cube-negative-indices.obj new file mode 100644 index 0000000000..5c9c30cb70 --- /dev/null +++ b/test/unit/assets/cube-negative-indices.obj @@ -0,0 +1,20 @@ +# Cube using negative vertex indices +# Negative indices count backwards: -1 = last vertex, -2 = second-to-last, etc. + +# Vertices +v 0.0 0.0 0.0 +v 1.0 0.0 0.0 +v 1.0 1.0 0.0 +v 0.0 1.0 0.0 +v 0.0 0.0 1.0 +v 1.0 0.0 1.0 +v 1.0 1.0 1.0 +v 0.0 1.0 1.0 + +# Faces using negative indices +f -8 -7 -6 -5 +f -4 -3 -2 -1 +f -8 -4 -1 -5 +f -7 -3 -2 -6 +f -5 -6 -2 -1 +f -8 -7 -3 -4 diff --git a/test/unit/io/loadModel.js b/test/unit/io/loadModel.js index 5b6a718f8f..f88a5807cc 100644 --- a/test/unit/io/loadModel.js +++ b/test/unit/io/loadModel.js @@ -10,6 +10,8 @@ suite('loadModel', function() { const inconsistentColorObjFile = '/test/unit/assets/eg1.obj'; const objMtlMissing = '/test/unit/assets/objMtlMissing.obj'; const validSTLfileWithoutExtension = '/test/unit/assets/ascii'; + const validCubeFile = '/test/unit/assets/cube.obj'; + const negativeIndexCubeFile = '/test/unit/assets/cube-negative-indices.obj'; beforeAll(async () => { loading(mockP5, mockP5Prototype); @@ -115,4 +117,20 @@ suite('loadModel', function() { const model = await mockP5Prototype.loadModel(validSTLfileWithoutExtension, '.STL'); assert.instanceOf(model, Geometry); }); + + test('OBJ with negative vertex indices loads correctly', async function() { + const model = await mockP5Prototype.loadModel(negativeIndexCubeFile); + assert.instanceOf(model, Geometry); + assert.isAbove(model.vertices.length, 0, 'Model should have vertices'); + assert.isAbove(model.faces.length, 0, 'Model should have faces'); + }); + + test('OBJ negative indices produce same geometry as positive', async function() { + const positiveModel = await mockP5Prototype.loadModel(validCubeFile); + const negativeModel = await mockP5Prototype.loadModel(negativeIndexCubeFile); + assert.equal(positiveModel.vertices.length, negativeModel.vertices.length, + 'Vertex count should match'); + assert.equal(positiveModel.faces.length, negativeModel.faces.length, + 'Face count should match'); + }); });