From 5385a31eccc22973e69f84ab6f1c303fea63550e Mon Sep 17 00:00:00 2001 From: Nixxx19 Date: Sun, 23 Aug 2026 18:27:50 +0530 Subject: [PATCH] apply mtl material state to single-material models --- src/webgl/loading.js | 17 ++++++++++++++--- test/unit/assets/single_material_textured.mtl | 5 +++++ test/unit/assets/single_material_textured.obj | 12 ++++++++++++ test/unit/io/loadModel.js | 19 +++++++++++++++++++ 4 files changed, 50 insertions(+), 3 deletions(-) create mode 100644 test/unit/assets/single_material_textured.mtl create mode 100644 test/unit/assets/single_material_textured.obj diff --git a/src/webgl/loading.js b/src/webgl/loading.js index cb29bae4bc..1c6271c499 100755 --- a/src/webgl/loading.js +++ b/src/webgl/loading.js @@ -190,10 +190,21 @@ async function loadMaterialTextures(materials, modelPath, instance) { // as the aggregate; each part gets its own localised verts with faces re-indexed // against them, plus its material's state. function buildMaterialParts(model, faceMaterials, materials) { - // only split when there are genuinely multiple materials. a single material - // (or none) stays as the geometry's own part and renders as before. one group - // per material, plus a null group for faces before any usemtl so none drop. + // one group per material, plus a null group for faces before any usemtl so + // none drop. const names = [...new Set(faceMaterials)]; + + // one material covering every face. the geometry is already its own part, so + // hand it the state directly: splitting would duplicate every vertex to say + // the same thing, and would stop parts[0] being the geometry itself. without + // this a single material model never receives its maps at all. + if (names.length === 1 && names[0] != null) { + Object.assign(model.partState, mtlToPartState(materials[names[0]])); + return; + } + + // nothing to split on: no materials, or one material alongside faces that + // were declared before any usemtl and so have none. if (names.filter(name => name != null).length < 2) return; const hasUvs = model.uvs.length > 0; diff --git a/test/unit/assets/single_material_textured.mtl b/test/unit/assets/single_material_textured.mtl new file mode 100644 index 0000000000..1dafacd0af --- /dev/null +++ b/test/unit/assets/single_material_textured.mtl @@ -0,0 +1,5 @@ +newmtl only +Kd 1.000 1.000 1.000 +Ks 0.500 0.500 0.500 +Ns 60 +map_Kd cat.jpg diff --git a/test/unit/assets/single_material_textured.obj b/test/unit/assets/single_material_textured.obj new file mode 100644 index 0000000000..422091c0a9 --- /dev/null +++ b/test/unit/assets/single_material_textured.obj @@ -0,0 +1,12 @@ +mtllib single_material_textured.mtl +v 0 0 0 +v 1 0 0 +v 1 1 0 +v 0 1 0 +vt 0 0 +vt 1 0 +vt 1 1 +vt 0 1 +usemtl only +f 1/1 2/2 3/3 +f 1/1 3/3 4/4 diff --git a/test/unit/io/loadModel.js b/test/unit/io/loadModel.js index 8f640f85f9..d512733d49 100644 --- a/test/unit/io/loadModel.js +++ b/test/unit/io/loadModel.js @@ -159,6 +159,25 @@ suite('loadModel', function () { assert.equal(model.parts[0], model, 'the geometry is its own single part'); }); + test('a single-material OBJ still receives its maps', async function () { + const fakeImage = { width: 1, height: 1 }; + mockP5Prototype.loadImage = async () => fakeImage; + try { + const model = await mockP5Prototype.loadModel( + '/test/unit/assets/single_material_textured.obj' + ); + // one material, so no split: the geometry stays its own only part + assert.equal(model.parts.length, 1); + assert.equal(model.parts[0], model); + // and it carries the material's state rather than dropping it + assert.equal(model.partState.texture, fakeImage); + assert.equal(model.partState.shininess, 60); + assert.deepEqual(model.partState.specularColor, [0.5, 0.5, 0.5]); + } finally { + delete mockP5Prototype.loadImage; + } + }); + test('a 12-material OBJ splits into 12 parts', async function () { const model = await mockP5Prototype.loadModel( '/test/unit/assets/multi_material_12.obj'