OBJLoader displays strange texture mapping with cube made in Blender (problem and solution)
Nutriz opened this issue · 5 comments
Hi. Thank you for this amazing book !
In the Loading more complex models chapter, I struggled to understand why my cube's texture was not displayed correctly. The cube was made with Blender and with correct UV.
image
After a quick search I have found #59 issue and understood the problem.
Maybe a little reword could help to not fall in this trap for further readers. IMO the sentence "And we will get our familiar textured cube." can be misunderstood in the way that "the obj loader correctly works with a cube and texture".
BTW here is my kotlin code (straight forward I hope) that supports both index by position and index by texture coords. It is inspired from code in #59 but cleaner and refactored to not have code duplication.
Anyone can copy/paste and use it (no external function used) and of course you can links this issue/code in your book
data class Face(
val indexGroups: List<IndexesGroup>,
)
data class IndexesGroup(val idxPos: Int = NO_VALUE, val idxTextCoords: Int = NO_VALUE, val idxVecNormal: Int = NO_VALUE) {
companion object {
const val NO_VALUE = -1
}
}
@OptIn(ExperimentalTime::class)
object ObjLoader {
fun loadMesh(fileName: String): Mesh {
val positionsData = mutableListOf<Vector3f>()
val texCoordsData = mutableListOf<Vector2f>()
val normalsData = mutableListOf<Vector3f>()
val facesData = mutableListOf<Face>()
File(fileName).forEachLine { line ->
val tokens = line.split(" ")
when (tokens.first()) {
"v" -> positionsData += Vector3f(tokens[1].toFloat(), tokens[2].toFloat(), tokens[3].toFloat())
"vt" -> texCoordsData += Vector2f(tokens[1].toFloat(), tokens[2].toFloat())
"vn" -> normalsData += Vector3f(tokens[1].toFloat(), tokens[2].toFloat(), tokens[3].toFloat())
"f" -> {
val indexGroups = mutableListOf<IndexesGroup>()
tokens.drop(1).forEach { groupString ->
val group = groupString.split("/").map { it.toInt() - 1 }
indexGroups += IndexesGroup(group[0], group[1], group[2])
}
facesData += Face(indexGroups)
}
}
}
val usePosForIdx = positionsData.size >= texCoordsData.size
return reorderLists(positionsData, texCoordsData, normalsData, facesData, usePosForIdx)
}
private fun reorderLists(
positionsData: MutableList<Vector3f>,
texCoordsData: MutableList<Vector2f>,
normalsData: MutableList<Vector3f>,
facesData: MutableList<Face>,
usePosForIdx: Boolean = true
): Mesh {
println("Mesh reordering with positions: $usePosForIdx")
val arraySize = if (usePosForIdx) positionsData.size else texCoordsData.size
val positions = FloatArray(arraySize * 3)
val texCoords = FloatArray(arraySize * 2)
val normals = FloatArray(arraySize * 3)
val indices = mutableListOf<Int>()
facesData.forEach { face ->
face.indexGroups.forEach { groupId ->
val index = if (usePosForIdx) groupId.idxPos else groupId.idxTextCoords
val position = positionsData[groupId.idxPos]
positions[index * 3] = position.x
positions[index * 3 + 1] = position.y
positions[index * 3 + 2] = position.z
val coords = texCoordsData[groupId.idxTextCoords]
texCoords[index * 2] = coords.x
texCoords[index * 2 + 1] = 1 - coords.y
val normal = normalsData[groupId.idxVecNormal]
normals[index * 3] = normal.x
normals[index * 3 + 1] = normal.y
normals[index * 3 + 2] = normal.z
indices += index
}
}
return Mesh(positions, texCoords, normals, indices.toIntArray())
}
}
Hi,
Thanks for the comments. The idea for this simple OBJ loader is to introduce some concepts, not to provide a complete loader (for example, it does not even support materials). So, I would suggest you to try the chapters that use assimp. However, as you say, it would be interesting to prevent some other developers to fall into this trap, so I will add some text to explain this. If I have more time, maybe I can use your code If you don't mind.
Best regards.
Yes, no problem to use my code :)
Hi,
I was reviewing the text of chapter 9, and at the end of it there is this sentence: "Remember to split edges when exporting, since we cannot assign several texture coordinates to the same vertex. Also, we need the normals to be defined per each triangle, not assigned to vertices". I think this clarifies the issue. What do you think?
I was thinking that If you have some public repository, instead of modifying the code, I could include a link to that. This way you get more credit.
Best regards.
Hi,
You are right, I forgot this sentence in chapter 9. I have a public repository with the code, but with 2 methods used from outside the OBJLoader in repo class (measureAndLog
and Log.debug
) and I could move file/package in the future.
So I have created an OBJLoader gits gits with no external function. Anyone can copy/paste and use it in seconds.
Of course you can link that in your book, if I can bring my little contribution for future readers, I'm happy with that :)
Done!
Thank you very much for the example.