Skip to content

Commit 316a5c0

Browse files
authored
Merge pull request #6710 from diyaayay/Mtl-support-to-obj-files
.mtl files support for .obj files to render color in 3D custom geometry
2 parents 7ba5dc2 + 25bbdc9 commit 316a5c0

File tree

14 files changed

+471
-46
lines changed

14 files changed

+471
-46
lines changed

src/webgl/loading.js

Lines changed: 178 additions & 39 deletions
Original file line numberDiff line numberDiff line change
@@ -158,6 +158,56 @@ p5.prototype.loadModel = function(path,options) {
158158
model.gid = `${path}|${normalize}`;
159159
const self = this;
160160

161+
async function getMaterials(lines){
162+
const parsedMaterialPromises=[];
163+
164+
for (let i = 0; i < lines.length; i++) {
165+
const mtllibMatch = lines[i].match(/^mtllib (.+)/);
166+
if (mtllibMatch) {
167+
let mtlPath='';
168+
const mtlFilename = mtllibMatch[1];
169+
const objPathParts = path.split('/');
170+
if(objPathParts.length > 1){
171+
objPathParts.pop();
172+
const objFolderPath = objPathParts.join('/');
173+
mtlPath = objFolderPath + '/' + mtlFilename;
174+
}else{
175+
mtlPath = mtlFilename;
176+
}
177+
parsedMaterialPromises.push(
178+
fileExists(mtlPath).then(exists => {
179+
if (exists) {
180+
return parseMtl(self, mtlPath);
181+
} else {
182+
console.warn(`MTL file not found or error in parsing; proceeding without materials: ${mtlPath}`);
183+
return {};
184+
185+
}
186+
}).catch(error => {
187+
console.warn(`Error loading MTL file: ${mtlPath}`, error);
188+
return {};
189+
})
190+
);
191+
}
192+
}
193+
try {
194+
const parsedMaterials = await Promise.all(parsedMaterialPromises);
195+
const materials= Object.assign({}, ...parsedMaterials);
196+
return materials ;
197+
} catch (error) {
198+
return {};
199+
}
200+
}
201+
202+
203+
async function fileExists(url) {
204+
try {
205+
const response = await fetch(url, { method: 'HEAD' });
206+
return response.ok;
207+
} catch (error) {
208+
return false;
209+
}
210+
}
161211
if (fileType.match(/\.stl$/i)) {
162212
this.httpDo(
163213
path,
@@ -188,31 +238,41 @@ p5.prototype.loadModel = function(path,options) {
188238
} else if (fileType.match(/\.obj$/i)) {
189239
this.loadStrings(
190240
path,
191-
strings => {
192-
parseObj(model, strings);
241+
async lines => {
242+
try{
243+
const parsedMaterials=await getMaterials(lines);
193244

194-
if (normalize) {
195-
model.normalize();
196-
}
197-
198-
if (flipU) {
199-
model.flipU();
200-
}
245+
parseObj(model, lines, parsedMaterials);
201246

202-
if (flipV) {
203-
model.flipV();
247+
}catch (error) {
248+
if (failureCallback) {
249+
failureCallback(error);
250+
} else {
251+
p5._friendlyError('Error during parsing: ' + error.message);
252+
}
253+
return;
204254
}
255+
finally{
256+
if (normalize) {
257+
model.normalize();
258+
}
259+
if (flipU) {
260+
model.flipU();
261+
}
262+
if (flipV) {
263+
model.flipV();
264+
}
205265

206-
self._decrementPreload();
207-
if (typeof successCallback === 'function') {
208-
successCallback(model);
266+
self._decrementPreload();
267+
if (typeof successCallback === 'function') {
268+
successCallback(model);
269+
}
209270
}
210271
},
211272
failureCallback
212273
);
213274
} else {
214275
p5._friendlyFileLoadError(3, path);
215-
216276
if (failureCallback) {
217277
failureCallback();
218278
} else {
@@ -224,6 +284,52 @@ p5.prototype.loadModel = function(path,options) {
224284
return model;
225285
};
226286

287+
function parseMtl(p5,mtlPath){
288+
return new Promise((resolve, reject)=>{
289+
let currentMaterial = null;
290+
let materials= {};
291+
p5.loadStrings(
292+
mtlPath,
293+
lines => {
294+
for (let line = 0; line < lines.length; ++line){
295+
const tokens = lines[line].trim().split(/\s+/);
296+
if(tokens[0] === 'newmtl') {
297+
const materialName = tokens[1];
298+
currentMaterial = materialName;
299+
materials[currentMaterial] = {};
300+
}else if (tokens[0] === 'Kd'){
301+
//Diffuse color
302+
materials[currentMaterial].diffuseColor = [
303+
parseFloat(tokens[1]),
304+
parseFloat(tokens[2]),
305+
parseFloat(tokens[3])
306+
];
307+
} else if (tokens[0] === 'Ka'){
308+
//Ambient Color
309+
materials[currentMaterial].ambientColor = [
310+
parseFloat(tokens[1]),
311+
parseFloat(tokens[2]),
312+
parseFloat(tokens[3])
313+
];
314+
}else if (tokens[0] === 'Ks'){
315+
//Specular color
316+
materials[currentMaterial].specularColor = [
317+
parseFloat(tokens[1]),
318+
parseFloat(tokens[2]),
319+
parseFloat(tokens[3])
320+
];
321+
322+
}else if (tokens[0] === 'map_Kd') {
323+
//Texture path
324+
materials[currentMaterial].texturePath = tokens[1];
325+
}
326+
}
327+
resolve(materials);
328+
},reject
329+
);
330+
});
331+
}
332+
227333
/**
228334
* Parse OBJ lines into model. For reference, this is what a simple model of a
229335
* square might look like:
@@ -235,7 +341,7 @@ p5.prototype.loadModel = function(path,options) {
235341
*
236342
* f 4 3 2 1
237343
*/
238-
function parseObj(model, lines) {
344+
function parseObj(model, lines, materials= {}) {
239345
// OBJ allows a face to specify an index for a vertex (in the above example),
240346
// but it also allows you to specify a custom combination of vertex, UV
241347
// coordinate, and vertex normal. So, "3/4/3" would mean, "use vertex 3 with
@@ -250,16 +356,25 @@ function parseObj(model, lines) {
250356
vt: [],
251357
vn: []
252358
};
253-
const indexedVerts = {};
254359

360+
361+
// Map from source index → Map of material → destination index
362+
const usedVerts = {}; // Track colored vertices
363+
let currentMaterial = null;
364+
const coloredVerts = new Set(); //unique vertices with color
365+
let hasColoredVertices = false;
366+
let hasColorlessVertices = false;
255367
for (let line = 0; line < lines.length; ++line) {
256368
// Each line is a separate object (vertex, face, vertex normal, etc)
257369
// For each line, split it into tokens on whitespace. The first token
258370
// describes the type.
259371
const tokens = lines[line].trim().split(/\b\s+/);
260372

261373
if (tokens.length > 0) {
262-
if (tokens[0] === 'v' || tokens[0] === 'vn') {
374+
if (tokens[0] === 'usemtl') {
375+
// Switch to a new material
376+
currentMaterial = tokens[1];
377+
}else if (tokens[0] === 'v' || tokens[0] === 'vn') {
263378
// Check if this line describes a vertex or vertex normal.
264379
// It will have three numeric parameters.
265380
const vertex = new p5.Vector(
@@ -280,40 +395,44 @@ function parseObj(model, lines) {
280395
// OBJ faces can have more than three points. Triangulate points.
281396
for (let tri = 3; tri < tokens.length; ++tri) {
282397
const face = [];
283-
284398
const vertexTokens = [1, tri - 1, tri];
285399

286400
for (let tokenInd = 0; tokenInd < vertexTokens.length; ++tokenInd) {
287401
// Now, convert the given token into an index
288402
const vertString = tokens[vertexTokens[tokenInd]];
289-
let vertIndex = 0;
403+
let vertParts=vertString.split('/');
290404

291405
// TODO: Faces can technically use negative numbers to refer to the
292406
// previous nth vertex. I haven't seen this used in practice, but
293407
// it might be good to implement this in the future.
294408

295-
if (indexedVerts[vertString] !== undefined) {
296-
vertIndex = indexedVerts[vertString];
297-
} else {
298-
const vertParts = vertString.split('/');
299-
for (let i = 0; i < vertParts.length; i++) {
300-
vertParts[i] = parseInt(vertParts[i]) - 1;
301-
}
409+
for (let i = 0; i < vertParts.length; i++) {
410+
vertParts[i] = parseInt(vertParts[i]) - 1;
411+
}
302412

303-
vertIndex = indexedVerts[vertString] = model.vertices.length;
304-
model.vertices.push(loadedVerts.v[vertParts[0]].copy());
305-
if (loadedVerts.vt[vertParts[1]]) {
306-
model.uvs.push(loadedVerts.vt[vertParts[1]].slice());
307-
} else {
308-
model.uvs.push([0, 0]);
309-
}
413+
if (!usedVerts[vertParts[0]]) {
414+
usedVerts[vertParts[0]] = {};
415+
}
310416

311-
if (loadedVerts.vn[vertParts[2]]) {
312-
model.vertexNormals.push(loadedVerts.vn[vertParts[2]].copy());
417+
if (usedVerts[vertParts[0]][currentMaterial] === undefined) {
418+
const vertIndex = model.vertices.length;
419+
model.vertices.push(loadedVerts.v[vertParts[0]].copy());
420+
model.uvs.push(loadedVerts.vt[vertParts[1]] ?
421+
loadedVerts.vt[vertParts[1]].slice() : [0, 0]);
422+
model.vertexNormals.push(loadedVerts.vn[vertParts[2]] ?
423+
loadedVerts.vn[vertParts[2]].copy() : new p5.Vector());
424+
425+
usedVerts[vertParts[0]][currentMaterial] = vertIndex;
426+
face.push(vertIndex);
427+
if (currentMaterial
428+
&& materials[currentMaterial]
429+
&& materials[currentMaterial].diffuseColor) {
430+
// Mark this vertex as colored
431+
coloredVerts.add(loadedVerts.v[vertParts[0]]); //since a set would only push unique values
313432
}
433+
} else {
434+
face.push(usedVerts[vertParts[0]][currentMaterial]);
314435
}
315-
316-
face.push(vertIndex);
317436
}
318437

319438
if (
@@ -322,6 +441,23 @@ function parseObj(model, lines) {
322441
face[1] !== face[2]
323442
) {
324443
model.faces.push(face);
444+
//same material for all vertices in a particular face
445+
if (currentMaterial
446+
&& materials[currentMaterial]
447+
&& materials[currentMaterial].diffuseColor) {
448+
hasColoredVertices=true;
449+
//flag to track color or no color model
450+
hasColoredVertices = true;
451+
const materialDiffuseColor =
452+
materials[currentMaterial].diffuseColor;
453+
for (let i = 0; i < face.length; i++) {
454+
model.vertexColors.push(materialDiffuseColor[0]);
455+
model.vertexColors.push(materialDiffuseColor[1]);
456+
model.vertexColors.push(materialDiffuseColor[2]);
457+
}
458+
}else{
459+
hasColorlessVertices=true;
460+
}
325461
}
326462
}
327463
}
@@ -331,7 +467,10 @@ function parseObj(model, lines) {
331467
if (model.vertexNormals.length === 0) {
332468
model.computeNormals();
333469
}
334-
470+
if (hasColoredVertices === hasColorlessVertices) {
471+
// If both are true or both are false, throw an error because the model is inconsistent
472+
throw new Error('Model coloring is inconsistent. Either all vertices should have colors or none should.');
473+
}
335474
return model;
336475
}
337476

test/unit/assets/cube.obj

Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,19 @@
1+
# Simple Cube OBJ File
2+
3+
# Vertices
4+
v 0.0 0.0 0.0
5+
v 1.0 0.0 0.0
6+
v 1.0 1.0 0.0
7+
v 0.0 1.0 0.0
8+
v 0.0 0.0 1.0
9+
v 1.0 0.0 1.0
10+
v 1.0 1.0 1.0
11+
v 0.0 1.0 1.0
12+
13+
# Faces
14+
f 1 2 3 4
15+
f 5 6 7 8
16+
f 1 5 8 4
17+
f 2 6 7 3
18+
f 4 3 7 8
19+
f 1 2 6 5

test/unit/assets/eg1.mtl

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,9 @@
1+
newmtl coloredMaterial
2+
Ns 96.078431
3+
Ka 1.000000 1.000000 1.000000
4+
Kd 1.000000 0.000000 0.000000 # Only this material has a diffuse color
5+
Ks 0.500000 0.500000 0.500000
6+
Ke 0.0 0.0 0.0
7+
Ni 1.000000
8+
d 1.000000
9+
illum 2

test/unit/assets/eg1.obj

Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,16 @@
1+
mtllib eg1.mtl
2+
3+
v 0 0 0
4+
v 1 0 0
5+
v 1 1 0
6+
v 0 1 0
7+
v 0.5 0.5 1
8+
9+
# Define faces without material
10+
f 1 2 5
11+
f 2 3 5
12+
13+
# Apply material to subsequent faces
14+
usemtl coloredMaterial
15+
f 1 4 5
16+
f 4 1 5

test/unit/assets/objMtlMissing.obj

Lines changed: 23 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,23 @@
1+
mtllib octa.mtl
2+
v 1 0 0
3+
v 0 1 0
4+
v 0 0 1
5+
v -1 0 0
6+
v 0 -1 0
7+
v 0 0 -1
8+
usemtl m000001
9+
f 1 5 6
10+
usemtl m003627
11+
f 2 1 6
12+
usemtl m010778
13+
f 1 2 3
14+
usemtl m012003
15+
f 4 2 6
16+
usemtl m019240
17+
f 4 5 3
18+
usemtl m021392
19+
f 5 4 6
20+
usemtl m022299
21+
f 2 4 3
22+
usemtl m032767
23+
f 5 1 3

0 commit comments

Comments
 (0)