Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
6 changes: 6 additions & 0 deletions test/unit/assets/missing_texture.mtl
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
newmtl m0
Kd 1 0 0
map_Kd does_not_exist.png

newmtl m1
Kd 0 0 1
13 changes: 13 additions & 0 deletions test/unit/assets/missing_texture.obj
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
mtllib missing_texture.mtl
v 0 0 0
v 1 0 0
v 0 1 0
v 1 1 0
vt 0 0
vt 1 0
vt 0 1
vt 1 1
usemtl m0
f 1/1 2/2 3/3
usemtl m1
f 2/2 4/4 3/3
36 changes: 36 additions & 0 deletions test/unit/assets/multi_material_12.mtl
Original file line number Diff line number Diff line change
@@ -0,0 +1,36 @@
newmtl m00
Kd 0.000 0.876 0.844

newmtl m01
Kd 0.500 0.518 0.999

newmtl m02
Kd 0.866 0.021 0.886

newmtl m03
Kd 1.000 0.482 0.536

newmtl m04
Kd 0.866 0.855 0.042

newmtl m05
Kd 0.500 1.000 0.463

newmtl m06
Kd 0.000 0.876 0.844

newmtl m07
Kd 0.500 0.518 0.999

newmtl m08
Kd 0.866 0.021 0.886

newmtl m09
Kd 1.000 0.482 0.536

newmtl m10
Kd 0.866 0.855 0.042

newmtl m11
Kd 0.500 1.000 0.463

61 changes: 61 additions & 0 deletions test/unit/assets/multi_material_12.obj
Original file line number Diff line number Diff line change
@@ -0,0 +1,61 @@
mtllib multi_material_12.mtl
v -1.5 -1 0
v -0.7 -1 0
v -1.5 -0.19999999999999996 0
v -0.5 -1 0
v 0.30000000000000004 -1 0
v -0.5 -0.19999999999999996 0
v 0.5 -1 0
v 1.3 -1 0
v 0.5 -0.19999999999999996 0
v 1.5 -1 0
v 2.3 -1 0
v 1.5 -0.19999999999999996 0
v -1.5 0 0
v -0.7 0 0
v -1.5 0.8 0
v -0.5 0 0
v 0.30000000000000004 0 0
v -0.5 0.8 0
v 0.5 0 0
v 1.3 0 0
v 0.5 0.8 0
v 1.5 0 0
v 2.3 0 0
v 1.5 0.8 0
v -1.5 1 0
v -0.7 1 0
v -1.5 1.8 0
v -0.5 1 0
v 0.30000000000000004 1 0
v -0.5 1.8 0
v 0.5 1 0
v 1.3 1 0
v 0.5 1.8 0
v 1.5 1 0
v 2.3 1 0
v 1.5 1.8 0
usemtl m00
f 1 2 3
usemtl m01
f 4 5 6
usemtl m02
f 7 8 9
usemtl m03
f 10 11 12
usemtl m04
f 13 14 15
usemtl m05
f 16 17 18
usemtl m06
f 19 20 21
usemtl m07
f 22 23 24
usemtl m08
f 25 26 27
usemtl m09
f 28 29 30
usemtl m10
f 31 32 33
usemtl m11
f 34 35 36
3 changes: 3 additions & 0 deletions test/unit/assets/single_material.mtl
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
newmtl only
Kd 0.2 0.7 0.9
Ns 40
22 changes: 22 additions & 0 deletions test/unit/assets/single_material.obj
Original file line number Diff line number Diff line change
@@ -0,0 +1,22 @@
mtllib single_material.mtl
v -0.5 -0.5 -0.5
v 0.5 -0.5 -0.5
v 0.5 0.5 -0.5
v -0.5 0.5 -0.5
v -0.5 -0.5 0.5
v 0.5 -0.5 0.5
v 0.5 0.5 0.5
v -0.5 0.5 0.5
usemtl only
f 1 2 3
f 1 3 4
f 5 6 7
f 5 7 8
f 1 5 8
f 1 8 4
f 2 6 7
f 2 7 3
f 4 8 7
f 4 7 3
f 1 5 6
f 1 6 2
68 changes: 68 additions & 0 deletions test/unit/io/loadModel.js
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
import { mockP5, mockP5Prototype, httpMock } from '../../js/mocks';
import loading from '../../../src/webgl/loading';
import { Geometry } from '../../../src/webgl/p5.Geometry';
import { vi } from 'vitest';

suite('loadModel', function () {
const invalidFile = '404file';
Expand Down Expand Up @@ -131,6 +132,73 @@ suite('loadModel', function () {
}
});

test('a single-material OBJ stays one part', async function () {
// eg1.obj has one real material, so it is not split
const model = await mockP5Prototype.loadModel(inconsistentColorObjFile);
assert.equal(model.parts.length, 1);
assert.equal(model.parts[0], model, 'the geometry is its own single part');
});

test('a 12-material OBJ splits into 12 parts', async function () {
const model = await mockP5Prototype.loadModel(
'/test/unit/assets/multi_material_12.obj'
);
assert.equal(model.parts.length, 12);
// every face still lands in exactly one part
const totalFaces = model.parts.reduce((s, p) => s + p.faces.length, 0);
assert.equal(totalFaces, model.faces.length);
});

test('parts get computed normals when the OBJ has none', async function () {
// textured.obj has no vn lines, so normals are computed before the split
const model = await mockP5Prototype.loadModel(
'/test/unit/assets/textured.obj'
);
assert.equal(model.parts.length, 2);
for (const part of model.parts) {
assert.equal(
part.vertexNormals.length,
part.vertices.length,
'each part has one computed normal per vertex'
);
}
});

test('each part carries its own localised uvs', async function () {
const model = await mockP5Prototype.loadModel(
'/test/unit/assets/textured.obj'
);
assert.equal(model.parts.length, 2);
for (const part of model.parts) {
assert.equal(
part.uvs.length,
part.vertices.length,
'each part has one uv per localised vertex'
);
}
});

test('a failed texture load warns instead of failing silently', async function () {
const warnSpy = vi.spyOn(console, 'warn').mockImplementation(() => {});
mockP5Prototype.loadImage = async () => {
throw new Error('Not Found');
};
try {
const model = await mockP5Prototype.loadModel(
'/test/unit/assets/textured.obj'
);
// the model still loads with both material parts
assert.equal(model.parts.length, 2);
// the failed texture is skipped, so no part carries it
assert.ok(model.parts.every(p => p.partState.texture == null));
// and the failure is surfaced, not silent
assert.ok(warnSpy.mock.calls.length > 0, 'a warning is emitted');
} finally {
delete mockP5Prototype.loadImage;
warnSpy.mockRestore();
}
});

test('mixed material coloring loads model with sentinel colors for uncolored vertices', async function () {
const model = await mockP5Prototype.loadModel(inconsistentColorObjFile);
assert.instanceOf(model, Geometry);
Expand Down
15 changes: 15 additions & 0 deletions test/unit/io/parseMtl.js
Original file line number Diff line number Diff line change
Expand Up @@ -48,6 +48,21 @@ suite('parseMtlData', function () {
expect(materials.a.diffuseColor).toEqual([0, 0, 0]);
expect(Object.keys(materials)).toEqual(['a']);
});

test('a Kd-only material parses without error', function () {
// the common case: a material that only sets a diffuse colour
const materials = parseMtlData('newmtl plain\nKd 0.2 0.4 0.6');
expect(materials.plain.diffuseColor).toEqual([0.2, 0.4, 0.6]);
expect(materials.plain.texturePath).toBeUndefined();
});

test('a malformed mtl does not throw', function () {
// an unknown token, a token missing its value, and a blank line
const mtl = 'newmtl weird\nKd 1 0 0\nfoo bar baz\nNs\n\nsomething_else';
expect(() => parseMtlData(mtl)).not.toThrow();
// the valid tokens around the junk are still read
expect(parseMtlData(mtl).weird.diffuseColor).toEqual([1, 0, 0]);
});
});

suite('mtlToPartState', function () {
Expand Down
64 changes: 64 additions & 0 deletions test/unit/visual/cases/webgl.js
Original file line number Diff line number Diff line change
Expand Up @@ -411,6 +411,70 @@ visualSuite('WebGL', function () {
screenshot();
}
);
visualTest(
'multi-material OBJ renders each material part',
async function (p5, screenshot) {
p5.createCanvas(50, 50, p5.WEBGL);
// textured.obj has two materials: a cat texture and a plain colour
const model = await new Promise(resolve =>
p5.loadModel('test/unit/assets/textured.obj', resolve)
);
model.normalize();
p5.background(255);
p5.rotateX(0.4);
p5.rotateY(0.4);
p5.noStroke();
p5.model(model);
screenshot();
}
);
visualTest(
'a slice with a missing texture falls back to its colour',
async function (p5, screenshot) {
p5.createCanvas(50, 50, p5.WEBGL);
// missing_texture.obj: m0 has a 404 map_Kd (falls back to red), m1 blue
const model = await new Promise(resolve =>
p5.loadModel('test/unit/assets/missing_texture.obj', resolve)
);
model.normalize();
p5.background(255);
p5.rotateX(0.4);
p5.rotateY(0.4);
p5.noStroke();
p5.model(model);
screenshot();
}
);
visualTest(
'a 12-material OBJ renders every material',
async function (p5, screenshot) {
p5.createCanvas(50, 50, p5.WEBGL);
const model = await new Promise(resolve =>
p5.loadModel('test/unit/assets/multi_material_12.obj', resolve)
);
model.normalize();
p5.background(255);
p5.noStroke();
p5.model(model);
screenshot();
}
);
visualTest(
'a single-material OBJ renders through the part path',
async function (p5, screenshot) {
p5.createCanvas(50, 50, p5.WEBGL);
const model = await new Promise(resolve =>
p5.loadModel('test/unit/assets/single_material.obj', resolve)
);
model.normalize();
p5.background(255);
p5.rotateX(0.4);
p5.rotateY(0.4);
p5.noStroke();
p5.model(model);
screenshot();
}
);
});

visualSuite('vertexProperty', function () {
Expand Down
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
{
"numScreenshots": 1
}
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
{
"numScreenshots": 1
}
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
{
"numScreenshots": 1
}
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
{
"numScreenshots": 1
}
Loading