Edit
I've rewritten the loader to duplicate data if only one element (v, vt, vn) is different. I get a much better result, but it is still incorrect. The seams on the mesh match what they should look like, but the texture appears a bit deformed. I've checked a list of the texture coords, and there appear to be some missing, but I don't understand how this could happen.
Here is the new code:
int bytesLoaded = 0;
ArrayList<Float> vertices = new ArrayList<>();
ArrayList<Float> textureCoords = new ArrayList<>();
ArrayList<Float> normals = new ArrayList<>();
ArrayList<Float> plainVertices = new ArrayList<>();
ArrayList<Float> plainTextureCoords = new ArrayList<>();
ArrayList<Float> plainNormals = new ArrayList<>();
ArrayList<Integer> indices = new ArrayList<>();
ArrayList<String> writtenData = new ArrayList<>();
String line;
try {
InputStream stream = IO.class.getClassLoader().getResourceAsStream(filePath); //get file off classpath
BufferedReader reader = new BufferedReader(new InputStreamReader(stream)); //create file reader
while((line = reader.readLine())!=null) { //repeat until document's end has been reached
String[] data = line.split(" "); //split line into each data point
if (line.startsWith("v ")) { //add vertices in the 3 data points, excluding the first which is the "v"
for (int i=1; i<4; i++) {
plainVertices.add(Float.parseFloat(data[i]));
}
} else if (line.startsWith("vt ")) {
for (int i=1; i<3; i++) {
plainTextureCoords.add(Float.parseFloat(data[i])); //add vertex textures in the 2 data points, excluding the first which is the "vt"
}
} else if (line.startsWith("vn ")) {
for (int i=1; i<4; i++) {
plainNormals.add(Float.parseFloat(data[i])); //add vertices in the 3 data points, excluding the first which is the "vn"
}
} else if (line.startsWith("f ")) { //indices
for (int i = 1; i<4; i++) {
String[] faceIndices = data[i].split("/"); //split each face into v indice, vt indice and vn indice
int vertexIndex = Integer.parseInt(faceIndices[0]) - 1; //get the indices, subtract 1 because OpenGL indices are 0 indexed
int textureCoordIndex = Integer.parseInt(faceIndices[1]) - 1;
int normalIndex = Integer.parseInt(faceIndices[2]) - 1;
StringBuilder tokenBuilder = new StringBuilder(); //generate a unique token to prevent repeats
tokenBuilder.append(vertexIndex);
tokenBuilder.append("+");
tokenBuilder.append(textureCoordIndex);
tokenBuilder.append("+");
tokenBuilder.append(normalIndex);
String token = tokenBuilder.toString();
System.out.println(token);
if (!writtenData.contains(token)) { //add the 3 indices' values to their list
writtenData.add(token);
vertices.add(plainVertices.get(vertexIndex * 3));
vertices.add(plainVertices.get(vertexIndex * 3 + 1));
vertices.add(plainVertices.get(vertexIndex * 3 + 2));
textureCoords.add(plainTextureCoords.get(textureCoordIndex * 2));
textureCoords.add(plainTextureCoords.get(textureCoordIndex * 2 + 1));
normals.add(plainNormals.get(normalIndex * 3));
normals.add(plainNormals.get(normalIndex * 3 + 1));
normals.add(plainNormals.get(normalIndex * 3 + 2));
}
indices.add(writtenData.indexOf(token)); //generate an index
}
}
bytesLoaded = bytesLoaded + line.getBytes().length; //keep track of bytes loaded
//System.out.println(bytesLoaded);
}
Original
I'm trying to create an OBJ file loader for my 3D game library (with Java and LWJGL), but having issues with loading texture coordinates (and normals too but I assume they'll apply the same as texture coords). I should get a Suzanne head looking like this, but instead looks like this.
Here's the code:
int bytesLoaded = 0;
ArrayList<Float> vertices = new ArrayList<>();
ArrayList<Float> textureCoords = new ArrayList<>();
ArrayList<Float> normals = new ArrayList<>();
ArrayList<Float> plainTextureCoords = new ArrayList<>();
ArrayList<Float> plainNormals = new ArrayList<>();
ArrayList<Integer> vertexIndices = new ArrayList<>();
ArrayList<Integer> textureCoordIndices = new ArrayList<>();
ArrayList<Integer> normalIndices = new ArrayList<>();
ArrayList<String> textureCoordsAdded = new ArrayList<>();
ArrayList<String> normalsAdded = new ArrayList<>();
String line;
try {
InputStream stream = IO.class.getClassLoader().getResourceAsStream(filePath); //get file off classpath
BufferedReader reader = new BufferedReader(new InputStreamReader(stream)); //create file reader
while((line = reader.readLine())!=null) { //repeat until document's end has been reached
String[] data = line.split(" "); //split line into each data point
textureCoords.add(0.0f); //filler
normals.add(0.0f);
if (line.startsWith("v ")) { //add vertices in the 3 data points, exculding the first which is the "v"
for (int i=1; i<4; i++) {
vertices.add(Float.parseFloat(data[i]));
}
} else if (line.startsWith("vt ")) {
for (int i=1; i<3; i++) {
plainTextureCoords.add(Float.parseFloat(data[i])); //add vertex textures in the 2 data points, excluding the first which is the "vt"
}
} else if (line.startsWith("vn ")) {
for (int i=1; i<4; i++) {
plainNormals.add(Float.parseFloat(data[i])); //add vertices in the 3 data points, excluding the first which is the "vn"
}
} else if (line.startsWith("f ")) { //indices
for (int i = 1; i<4; i++) {
String[] faceIndices = data[i].split("/"); //split each face into v indice, vt indice and vn indice
for (int j = 0; j<3; j++) {
switch (j){
case 0:
vertexIndices.add(Integer.parseInt(faceIndices[j]) - 1); //minus one as OpenGL indices are 0 indexed, not 1
break;
case 1:
textureCoordIndices.add(Integer.parseInt(faceIndices[j]));
break;
case 2:
normalIndices.add(Integer.parseInt(faceIndices[j]));
break;
}
}
}
}
bytesLoaded = bytesLoaded + line.getBytes().length; //just keep track of bytes loaded
}
for (int i = 0; i<textureCoordIndices.size(); i++) { //process texture coords
int index = textureCoordIndices.get(i),
pos = index * 2 - 2; //times 2 to get the location (2 vt's) and minus 2 to be 0 indexed
float targetCoord1 = plainTextureCoords.get(pos), //get the texture coords
targetCoord2 = plainTextureCoords.get(pos + 1);
String combined = String.valueOf(targetCoord1).concat("+").concat(String.valueOf(targetCoord2)); //Generate an 'ID' for the 2 vertex coords
if (!textureCoordsAdded.contains(combined)) { //Check if that ID has already been processed
textureCoordsAdded.add(combined); //Add that ID to the list of processed indices
int controlIndice = vertexIndices.get(pos); //Align the position of the texture coord with the vertices indices (OpenGL only uses vertex indices)
textureCoords.add(controlIndice, targetCoord1); //Add the texture coords to the correct position
textureCoords.add(controlIndice + 1, targetCoord2);
}
}
for (int i = 0; i<normalIndices.size(); i++) { //process normals
int index = normalIndices.get(i),
pos = index * 3 - 3; //times 3 to get the location 32 vn's) and minus 3 to be 0 indexed
float targetNorm1 = plainNormals.get(pos), //get the normals
targetNorm2 = plainNormals.get(pos + 1),
targetNorm3 = plainNormals.get(pos + 2);
String combined = String.valueOf(targetNorm1).concat("+").concat(String.valueOf(targetNorm2).concat("+").concat(String.valueOf(targetNorm3))); //Generate an 'ID' for the 3 vertex normals
if (!normalsAdded.contains(combined)) { //Check if that ID has already been processed
normalsAdded.add(combined);//Add that ID to the list of processed indices
int controlIndice = vertexIndices.get(pos); //Align the position of the normals with the vertices indices (OpenGL only uses vertex indices)
normals.add(controlIndice, targetNorm1);
normals.add(controlIndice + 1, targetNorm2); //Add the normals to the correct position
normals.add(controlIndice + 2, targetNorm3);
}
}
} catch (IOException|NullPointerException e) {
System.err.println("File " + filePath + " not found!");
Manager.terminate();
}
I think this might be to do with my understanding of the obj file format, rather than the implementation, but I cannot be sure.