-
-
Notifications
You must be signed in to change notification settings - Fork 3.4k
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
.mtl files support for .obj files to render color in 3D custom geometry #6710
Changes from 38 commits
a47fbbc
2143dba
3498ad0
5c7c07e
b46af55
0b7a1fe
77637ba
1169993
06c5e28
2b46a55
0790146
1fa08a2
be5cf6c
cd6039d
32f94f3
16fad39
c3538b7
3cb1a13
90a3a53
2fffa53
48b07a3
341b131
60a3b06
9538edc
56bc7f2
97950c7
4e46332
104463a
a4047af
7236340
b32c935
19e4655
14a2420
687e010
2aa937f
31bdeb9
ac68110
d7aa2f7
25bbdc9
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -158,6 +158,57 @@ p5.prototype.loadModel = function(path,options) { | |
model.gid = `${path}|${normalize}`; | ||
const self = this; | ||
|
||
async function getMaterials(lines){ | ||
const parsedMaterialPromises=[]; | ||
const mtlPaths=[]; | ||
return new Promise(async (resolve,reject)=>{ | ||
for (let i = 0; i < lines.length; i++) { | ||
const mtllibMatch = lines[i].match(/^mtllib (.+)/); | ||
if (mtllibMatch) { | ||
let mtlPath=''; | ||
const mtlFilename = mtllibMatch[1]; | ||
const objPathParts = path.split('/'); | ||
if(objPathParts.length > 1){ | ||
objPathParts.pop(); | ||
const objFolderPath = objPathParts.join('/'); | ||
mtlPath = objFolderPath + '/' + mtlFilename; | ||
}else{ | ||
mtlPath = mtlFilename; | ||
} | ||
try { | ||
if(await fileExists(mtlPath)){ | ||
const parsedMaterialsIndividual = await parseMtl(self, mtlPath); | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. By adding There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Removing |
||
mtlPaths.push(mtlPath); | ||
parsedMaterialPromises.push(parsedMaterialsIndividual); | ||
}else{ | ||
console.warn('MTL file not found or error in parsing; proceeding without materials.'); | ||
resolve({}); | ||
return; | ||
} | ||
} catch (error){ | ||
console.warn('Error loading MTL file:', error); | ||
resolve({}); | ||
return; | ||
} | ||
} | ||
}try { | ||
const parsedMaterials = await Promise.all(parsedMaterialPromises); | ||
const materials=await Object.assign({}, ...parsedMaterials); | ||
resolve (materials); | ||
} catch (error) { | ||
resolve({}); | ||
} | ||
|
||
}); | ||
} | ||
async function fileExists(url) { | ||
try { | ||
const response = await fetch(url, { method: 'HEAD' }); | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Nice! |
||
return response.ok; | ||
} catch (error) { | ||
return false; | ||
} | ||
} | ||
if (fileType.match(/\.stl$/i)) { | ||
this.httpDo( | ||
path, | ||
|
@@ -188,31 +239,42 @@ p5.prototype.loadModel = function(path,options) { | |
} else if (fileType.match(/\.obj$/i)) { | ||
this.loadStrings( | ||
path, | ||
strings => { | ||
parseObj(model, strings); | ||
|
||
if (normalize) { | ||
model.normalize(); | ||
} | ||
|
||
if (flipU) { | ||
model.flipU(); | ||
} | ||
|
||
if (flipV) { | ||
model.flipV(); | ||
async lines => { | ||
try{ | ||
const parsedMaterials=await getMaterials(lines); | ||
|
||
parseObj(model, lines, parsedMaterials); | ||
|
||
}catch (error) { | ||
console.error(error.message); | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Do we need to There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Ah yes! I thought of it but was only checking all errors in console while making this work. Sorry for the oversight while pushing. I'll make the changes. |
||
if (failureCallback) { | ||
failureCallback(error); | ||
} else { | ||
p5._friendlyError('Error during parsing: ' + error.message); | ||
} | ||
return; | ||
} | ||
finally{ | ||
if (normalize) { | ||
model.normalize(); | ||
} | ||
if (flipU) { | ||
model.flipU(); | ||
} | ||
if (flipV) { | ||
model.flipV(); | ||
} | ||
|
||
self._decrementPreload(); | ||
if (typeof successCallback === 'function') { | ||
successCallback(model); | ||
self._decrementPreload(); | ||
if (typeof successCallback === 'function') { | ||
successCallback(model); | ||
} | ||
} | ||
}, | ||
failureCallback | ||
); | ||
} else { | ||
p5._friendlyFileLoadError(3, path); | ||
|
||
if (failureCallback) { | ||
failureCallback(); | ||
} else { | ||
|
@@ -224,6 +286,52 @@ p5.prototype.loadModel = function(path,options) { | |
return model; | ||
}; | ||
|
||
function parseMtl(p5,mtlPath){ | ||
return new Promise((resolve, reject)=>{ | ||
let currentMaterial = null; | ||
let materials= {}; | ||
p5.loadStrings( | ||
mtlPath, | ||
lines => { | ||
for (let line = 0; line < lines.length; ++line){ | ||
const tokens = lines[line].trim().split(/\s+/); | ||
if(tokens[0] === 'newmtl') { | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Nice work on this parser, looks great! |
||
const materialName = tokens[1]; | ||
currentMaterial = materialName; | ||
materials[currentMaterial] = {}; | ||
}else if (tokens[0] === 'Kd'){ | ||
//Diffuse color | ||
materials[currentMaterial].diffuseColor = [ | ||
parseFloat(tokens[1]), | ||
parseFloat(tokens[2]), | ||
parseFloat(tokens[3]) | ||
]; | ||
} else if (tokens[0] === 'Ka'){ | ||
//Ambient Color | ||
materials[currentMaterial].ambientColor = [ | ||
parseFloat(tokens[1]), | ||
parseFloat(tokens[2]), | ||
parseFloat(tokens[3]) | ||
]; | ||
}else if (tokens[0] === 'Ks'){ | ||
//Specular color | ||
materials[currentMaterial].specularColor = [ | ||
parseFloat(tokens[1]), | ||
parseFloat(tokens[2]), | ||
parseFloat(tokens[3]) | ||
]; | ||
|
||
}else if (tokens[0] === 'map_Kd') { | ||
//Texture path | ||
materials[currentMaterial].texturePath = tokens[1]; | ||
} | ||
} | ||
resolve(materials); | ||
},reject | ||
); | ||
}); | ||
} | ||
|
||
/** | ||
* Parse OBJ lines into model. For reference, this is what a simple model of a | ||
* square might look like: | ||
|
@@ -235,7 +343,7 @@ p5.prototype.loadModel = function(path,options) { | |
* | ||
* f 4 3 2 1 | ||
*/ | ||
function parseObj(model, lines) { | ||
function parseObj(model, lines, materials= {}) { | ||
// OBJ allows a face to specify an index for a vertex (in the above example), | ||
// but it also allows you to specify a custom combination of vertex, UV | ||
// coordinate, and vertex normal. So, "3/4/3" would mean, "use vertex 3 with | ||
|
@@ -250,16 +358,25 @@ function parseObj(model, lines) { | |
vt: [], | ||
vn: [] | ||
}; | ||
const indexedVerts = {}; | ||
|
||
|
||
// Map from source index → Map of material → destination index | ||
const usedVerts = {}; // Track colored vertices | ||
let currentMaterial = null; | ||
const coloredVerts = new Set(); //unique vertices with color | ||
let hasColoredVertices = false; | ||
let hasColorlessVertices = false; | ||
for (let line = 0; line < lines.length; ++line) { | ||
// Each line is a separate object (vertex, face, vertex normal, etc) | ||
// For each line, split it into tokens on whitespace. The first token | ||
// describes the type. | ||
const tokens = lines[line].trim().split(/\b\s+/); | ||
|
||
if (tokens.length > 0) { | ||
if (tokens[0] === 'v' || tokens[0] === 'vn') { | ||
if (tokens[0] === 'usemtl') { | ||
// Switch to a new material | ||
currentMaterial = tokens[1]; | ||
}else if (tokens[0] === 'v' || tokens[0] === 'vn') { | ||
// Check if this line describes a vertex or vertex normal. | ||
// It will have three numeric parameters. | ||
const vertex = new p5.Vector( | ||
|
@@ -280,40 +397,44 @@ function parseObj(model, lines) { | |
// OBJ faces can have more than three points. Triangulate points. | ||
for (let tri = 3; tri < tokens.length; ++tri) { | ||
const face = []; | ||
|
||
const vertexTokens = [1, tri - 1, tri]; | ||
|
||
for (let tokenInd = 0; tokenInd < vertexTokens.length; ++tokenInd) { | ||
// Now, convert the given token into an index | ||
const vertString = tokens[vertexTokens[tokenInd]]; | ||
let vertIndex = 0; | ||
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. | ||
|
||
if (indexedVerts[vertString] !== undefined) { | ||
vertIndex = indexedVerts[vertString]; | ||
} else { | ||
const vertParts = vertString.split('/'); | ||
for (let i = 0; i < vertParts.length; i++) { | ||
vertParts[i] = parseInt(vertParts[i]) - 1; | ||
} | ||
for (let i = 0; i < vertParts.length; i++) { | ||
vertParts[i] = parseInt(vertParts[i]) - 1; | ||
} | ||
|
||
vertIndex = indexedVerts[vertString] = model.vertices.length; | ||
model.vertices.push(loadedVerts.v[vertParts[0]].copy()); | ||
if (loadedVerts.vt[vertParts[1]]) { | ||
model.uvs.push(loadedVerts.vt[vertParts[1]].slice()); | ||
} else { | ||
model.uvs.push([0, 0]); | ||
} | ||
if (!usedVerts[vertParts[0]]) { | ||
usedVerts[vertParts[0]] = {}; | ||
} | ||
|
||
if (loadedVerts.vn[vertParts[2]]) { | ||
model.vertexNormals.push(loadedVerts.vn[vertParts[2]].copy()); | ||
if (usedVerts[vertParts[0]][currentMaterial] === undefined) { | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Nice! |
||
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 p5.Vector()); | ||
|
||
usedVerts[vertParts[0]][currentMaterial] = vertIndex; | ||
face.push(vertIndex); | ||
if (currentMaterial | ||
&& materials[currentMaterial] | ||
&& materials[currentMaterial].diffuseColor) { | ||
// Mark this vertex as colored | ||
coloredVerts.add(loadedVerts.v[vertParts[0]]); //since a set would only push unique values | ||
} | ||
} else { | ||
face.push(usedVerts[vertParts[0]][currentMaterial]); | ||
} | ||
|
||
face.push(vertIndex); | ||
} | ||
|
||
if ( | ||
|
@@ -322,6 +443,23 @@ function parseObj(model, lines) { | |
face[1] !== face[2] | ||
) { | ||
model.faces.push(face); | ||
//same material for all vertices in a particular face | ||
if (currentMaterial | ||
&& materials[currentMaterial] | ||
&& materials[currentMaterial].diffuseColor) { | ||
hasColoredVertices=true; | ||
//flag to track color or no color model | ||
hasColoredVertices = true; | ||
davepagurek marked this conversation as resolved.
Show resolved
Hide resolved
|
||
const materialDiffuseColor = | ||
materials[currentMaterial].diffuseColor; | ||
for (let i = 0; i < face.length; i++) { | ||
model.vertexColors.push(materialDiffuseColor[0]); | ||
model.vertexColors.push(materialDiffuseColor[1]); | ||
model.vertexColors.push(materialDiffuseColor[2]); | ||
} | ||
}else{ | ||
hasColorlessVertices=true; | ||
} | ||
} | ||
} | ||
} | ||
|
@@ -331,7 +469,10 @@ function parseObj(model, lines) { | |
if (model.vertexNormals.length === 0) { | ||
model.computeNormals(); | ||
} | ||
|
||
if (hasColoredVertices === hasColorlessVertices) { | ||
// If both are true or both are false, throw an error because the model is inconsistent | ||
throw new Error('Model coloring is inconsistent. Either all vertices should have colors or none should.'); | ||
} | ||
return model; | ||
} | ||
|
||
|
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,19 @@ | ||
# Simple Cube OBJ File | ||
|
||
# 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 | ||
f 1 2 3 4 | ||
f 5 6 7 8 | ||
f 1 5 8 4 | ||
f 2 6 7 3 | ||
f 4 3 7 8 | ||
f 1 2 6 5 |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,9 @@ | ||
newmtl coloredMaterial | ||
Ns 96.078431 | ||
Ka 1.000000 1.000000 1.000000 | ||
Kd 1.000000 0.000000 0.000000 # Only this material has a diffuse color | ||
Ks 0.500000 0.500000 0.500000 | ||
Ke 0.0 0.0 0.0 | ||
Ni 1.000000 | ||
d 1.000000 | ||
illum 2 |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,16 @@ | ||
mtllib eg1.mtl | ||
|
||
v 0 0 0 | ||
v 1 0 0 | ||
v 1 1 0 | ||
v 0 1 0 | ||
v 0.5 0.5 1 | ||
|
||
# Define faces without material | ||
f 1 2 5 | ||
f 2 3 5 | ||
|
||
# Apply material to subsequent faces | ||
usemtl coloredMaterial | ||
f 1 4 5 | ||
f 4 1 5 |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,23 @@ | ||
mtllib octa.mtl | ||
v 1 0 0 | ||
v 0 1 0 | ||
v 0 0 1 | ||
v -1 0 0 | ||
v 0 -1 0 | ||
v 0 0 -1 | ||
usemtl m000001 | ||
f 1 5 6 | ||
usemtl m003627 | ||
f 2 1 6 | ||
usemtl m010778 | ||
f 1 2 3 | ||
usemtl m012003 | ||
f 4 2 6 | ||
usemtl m019240 | ||
f 4 5 3 | ||
usemtl m021392 | ||
f 5 4 6 | ||
usemtl m022299 | ||
f 2 4 3 | ||
usemtl m032767 | ||
f 5 1 3 |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
This
await
will also cause us to do theseHEAD
requests one at a time instead of parallel. Maybe we can do something like:(Also, while we're at it, maybe we can add the URL for the mtl file that we couldn't find to the warning we log?)