@@ -158,6 +158,56 @@ p5.prototype.loadModel = function(path,options) {
158
158
model . gid = `${ path } |${ normalize } ` ;
159
159
const self = this ;
160
160
161
+ async function getMaterials ( lines ) {
162
+ const parsedMaterialPromises = [ ] ;
163
+
164
+ for ( let i = 0 ; i < lines . length ; i ++ ) {
165
+ const mtllibMatch = lines [ i ] . match ( / ^ m t l l i b ( .+ ) / ) ;
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
+ }
161
211
if ( fileType . match ( / \. s t l $ / i) ) {
162
212
this . httpDo (
163
213
path ,
@@ -188,31 +238,41 @@ p5.prototype.loadModel = function(path,options) {
188
238
} else if ( fileType . match ( / \. o b j $ / i) ) {
189
239
this . loadStrings (
190
240
path ,
191
- strings => {
192
- parseObj ( model , strings ) ;
241
+ async lines => {
242
+ try {
243
+ const parsedMaterials = await getMaterials ( lines ) ;
193
244
194
- if ( normalize ) {
195
- model . normalize ( ) ;
196
- }
197
-
198
- if ( flipU ) {
199
- model . flipU ( ) ;
200
- }
245
+ parseObj ( model , lines , parsedMaterials ) ;
201
246
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 ;
204
254
}
255
+ finally {
256
+ if ( normalize ) {
257
+ model . normalize ( ) ;
258
+ }
259
+ if ( flipU ) {
260
+ model . flipU ( ) ;
261
+ }
262
+ if ( flipV ) {
263
+ model . flipV ( ) ;
264
+ }
205
265
206
- self . _decrementPreload ( ) ;
207
- if ( typeof successCallback === 'function' ) {
208
- successCallback ( model ) ;
266
+ self . _decrementPreload ( ) ;
267
+ if ( typeof successCallback === 'function' ) {
268
+ successCallback ( model ) ;
269
+ }
209
270
}
210
271
} ,
211
272
failureCallback
212
273
) ;
213
274
} else {
214
275
p5 . _friendlyFileLoadError ( 3 , path ) ;
215
-
216
276
if ( failureCallback ) {
217
277
failureCallback ( ) ;
218
278
} else {
@@ -224,6 +284,52 @@ p5.prototype.loadModel = function(path,options) {
224
284
return model ;
225
285
} ;
226
286
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
+
227
333
/**
228
334
* Parse OBJ lines into model. For reference, this is what a simple model of a
229
335
* square might look like:
@@ -235,7 +341,7 @@ p5.prototype.loadModel = function(path,options) {
235
341
*
236
342
* f 4 3 2 1
237
343
*/
238
- function parseObj ( model , lines ) {
344
+ function parseObj ( model , lines , materials = { } ) {
239
345
// OBJ allows a face to specify an index for a vertex (in the above example),
240
346
// but it also allows you to specify a custom combination of vertex, UV
241
347
// coordinate, and vertex normal. So, "3/4/3" would mean, "use vertex 3 with
@@ -250,16 +356,25 @@ function parseObj(model, lines) {
250
356
vt : [ ] ,
251
357
vn : [ ]
252
358
} ;
253
- const indexedVerts = { } ;
254
359
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 ;
255
367
for ( let line = 0 ; line < lines . length ; ++ line ) {
256
368
// Each line is a separate object (vertex, face, vertex normal, etc)
257
369
// For each line, split it into tokens on whitespace. The first token
258
370
// describes the type.
259
371
const tokens = lines [ line ] . trim ( ) . split ( / \b \s + / ) ;
260
372
261
373
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' ) {
263
378
// Check if this line describes a vertex or vertex normal.
264
379
// It will have three numeric parameters.
265
380
const vertex = new p5 . Vector (
@@ -280,40 +395,44 @@ function parseObj(model, lines) {
280
395
// OBJ faces can have more than three points. Triangulate points.
281
396
for ( let tri = 3 ; tri < tokens . length ; ++ tri ) {
282
397
const face = [ ] ;
283
-
284
398
const vertexTokens = [ 1 , tri - 1 , tri ] ;
285
399
286
400
for ( let tokenInd = 0 ; tokenInd < vertexTokens . length ; ++ tokenInd ) {
287
401
// Now, convert the given token into an index
288
402
const vertString = tokens [ vertexTokens [ tokenInd ] ] ;
289
- let vertIndex = 0 ;
403
+ let vertParts = vertString . split ( '/' ) ;
290
404
291
405
// TODO: Faces can technically use negative numbers to refer to the
292
406
// previous nth vertex. I haven't seen this used in practice, but
293
407
// it might be good to implement this in the future.
294
408
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
+ }
302
412
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
+ }
310
416
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
313
432
}
433
+ } else {
434
+ face . push ( usedVerts [ vertParts [ 0 ] ] [ currentMaterial ] ) ;
314
435
}
315
-
316
- face . push ( vertIndex ) ;
317
436
}
318
437
319
438
if (
@@ -322,6 +441,23 @@ function parseObj(model, lines) {
322
441
face [ 1 ] !== face [ 2 ]
323
442
) {
324
443
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
+ }
325
461
}
326
462
}
327
463
}
@@ -331,7 +467,10 @@ function parseObj(model, lines) {
331
467
if ( model . vertexNormals . length === 0 ) {
332
468
model . computeNormals ( ) ;
333
469
}
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
+ }
335
474
return model ;
336
475
}
337
476
0 commit comments