lwjglgamedev/lwjglbook-leg

OBJ-Loader can't handle OBJ file with more Tex-Coordinates than vertices

TheJWays opened this issue · 1 comments

Good Day
Thank you for this very well made tutorial. I have found an issue with the OBJ-Loader-System:
If you try to load an OBJ-File which has more Texture-Coordinates than Position-Vectors (That happens for example with UV-Maps where you need multiple tex-coords per Vertex), the Loaded Mesh looks very wrong. I have extended the code to handle that case. Maybe something like that could be added to the source?

`package graphics;

import java.util.ArrayList;
import java.util.List;

import org.joml.Vector2f;
import org.joml.Vector3f;
import graphics.FileUtility;

public class OBJLoader {

/**
 * Liesst ein OBJ-Modell-File ein und extrahiert daraus alle Daten so, dass man sie als Mesh hat.
 * @param fileName Resourcen-Name des Modells.¨
 * @param XZNormalize true => Modell so skallieren, dass in der XZ-Ebene der auesserste Punkt die Entfernung 1.0 von der mitte hat...
 * @return Fertig geladenes Mesh
 * @throws Exception Wenn etwas schieflaeuft.
 */
public static Mesh loadMesh(String fileName, boolean XZNormalize) throws Exception {
	//File Zeile fuer Zeile einlesen.
    List<String> lines = FileUtility.readAllLines(fileName);
    
    //Grundlisten vorbereiten
    List<Vector3f> vertices = new ArrayList<>();
    List<Vector2f> textures = new ArrayList<>();
    List<Vector3f> normals = new ArrayList<>();
    List<Face> faces = new ArrayList<>();
    

    //Zeile fuer Zeile einlesen.
    for (String line : lines) {
        String[] tokens = line.split("\\s+");
        switch (tokens[0]) {
            case "v":
                // Das ist ein Positionsvektor
                Vector3f vec3f = new Vector3f(
                        Float.parseFloat(tokens[1]),
                        Float.parseFloat(tokens[2]),
                        Float.parseFloat(tokens[3]));
                vertices.add(vec3f);
                break;
            case "vt":
                // Das ist eine Texturkoordinate
                Vector2f vec2f = new Vector2f(
                		Float.parseFloat(tokens[1]),
                        Float.parseFloat(tokens[2]));
                textures.add(vec2f);
                break;
            case "vn":
                // Das ist ein Normalenvektor
                Vector3f vec3fNorm = new Vector3f(
                        Float.parseFloat(tokens[1]),
                        Float.parseFloat(tokens[2]),
                        Float.parseFloat(tokens[3]));
                normals.add(vec3fNorm);
                break;
            case "f":
            	//Das ist eine Face-Beschreibung. Daraus generiert man den Index-Buffer.
                Face face = new Face(tokens[1], tokens[2], tokens[3]);
                faces.add(face);
                break;
            default:
                // Ignore other lines
                break;
        }
    }
    
    if (XZNormalize) {
    	float furthest = 0.0f;
        for(Vector3f v : vertices) {
        	float dist = v.x * v.x + v.z * v.z;
        	if(dist > furthest) {
        		furthest = dist;
        	}
        }
        
        float scale = (float)Math.sqrt(furthest);
        
        for(Vector3f v : vertices) {
        	v.x = v.x / scale;
        	v.y = v.y / scale;
        	v.z = v.z / scale;
        }
    }
    
    
    //Listen expandieren und ordnen und daraus das Mesh erstellen.
    if(vertices.size() >= textures.size()) {
    	return reorderListsByVerts(vertices, textures, normals, faces);
    }
    else {
    	return reorderListsByTexcoords(vertices, textures, normals, faces);
    }
}

/**
 * Expandiert die kompressierten Listen anhand des Texturbuffers und berechnet den Index-buffer.
 * Wir haben das Problem, das OBJ-Files drei Index-Buffers haben.
 * Einen fuer Positionen, einen fuer Texturkoordinaten und einer Fuer Normalen.
 * OpenGL erlaubt aber nur einen. Der dort gespeicherte Index gilt fuer alle Arrays und alle Arrays
 * muessen gleich lang sein. Deshalb muessen die Fehlenden eintraege in
 * Textur- und Normalenbuffer nachgerechnet und hinzugefuehgt werden.
 * Ausserdem muss die Listenordnung so gemacht werden, dass alle zusammengehoerenden Werte
 * jeweils den gleichen Index haben.
 * 
 * @param posList Eingelesene positionsliste
 * @param textCoordList Eingelesene Texturkoordinatenliste.
 * @param normList Eingelesene Normalenliste.
 * @param facesList Eingelesene Facettenliste.
 * @return Fertig instantiiertes Mesh.
 */
private static Mesh reorderListsByTexcoords(List<Vector3f> posList, List<Vector2f> textCoordList,
        List<Vector3f> normList, List<Face> facesList) {
	
	List<Integer> indices = new ArrayList<Integer>();
	
	//Masterliste ist die mit den texturkoordinaten.
	float[] texArr = new float[textCoordList.size() * 2];
	int i = 0;
	for(Vector2f tex : textCoordList) {
		texArr[i*2] = tex.x;
		texArr[i*2 + 1] = 1.0f - tex.y;
		i++;
	}
	
	//Arrays fuer Vertices und Normalenstream erstellen
	float[] posArr = new float[textCoordList.size() * 3];
	float[] normArr = new float[textCoordList.size() * 3];
	
	//Durch die Facettenliste durchgehen und daraus den Indexbuffer und die Textur/Normalenbuffer rechnen.
	for (Face face : facesList) {
		IdxGroup[] faceVertexIndices = face.getFaceVertexIndices();
		for(IdxGroup indValue : faceVertexIndices) {
			processFaceVertexByTexcoord(indValue, posList, normList, indices, posArr, normArr);
		}
	}
	
	int[] indicesArr = new int[indices.size()];
    indicesArr = indices.stream().mapToInt((Integer v) -> v).toArray();
    Mesh mesh = new Mesh(posArr, texArr, normArr, indicesArr);
    return mesh;
}

/**
 * Mappt die Positions- und Normalenvektoren auf den richtigen Platz und 
 * fuegt den Index hinzu...
 * @param indices Indexgruppe
 * @param posList Liste mit den Positionsvektoren
 * @param normList Liste mit den Normalenvektoren
 * @param indicesList Liste mit den Indices.
 * @param posArr Array mit den Positionsdaten
 * @param normArr Array mit den Normalendaten.
 */
private static void processFaceVertexByTexcoord(IdxGroup indices, List<Vector3f> posList,
        List<Vector3f> normList, List<Integer> indicesList,
        float[] posArr, float[] normArr) {
	
	int texIndex = indices.idxTextCoord;
	indicesList.add(texIndex);
	
	if(indices.idxPos != IdxGroup.NO_VALUE) {
		Vector3f pos = posList.get(indices.idxPos);
		posArr[texIndex * 3] = pos.x;
		posArr[texIndex * 3 + 1] = pos.y;
		posArr[texIndex * 3 + 2] = pos.z;
	}
	if(indices.idxVecNormal != IdxGroup.NO_VALUE) {
		//Normale einsetzen(Falls Vorhanden).
        Vector3f vecNorm = normList.get(indices.idxVecNormal);
        normArr[texIndex * 3] = vecNorm.x;
        normArr[texIndex* 3 + 1] = vecNorm.y;
        normArr[texIndex* 3 + 2] = vecNorm.z;
	}
	
}

/**
 * Expandiert die kompressierten Listen und berechnet den Index-buffer.
 * Wir haben das Problem, das OBJ-Files drei Index-Buffers haben.
 * Einen fuer Positionen, einen fuer Texturkoordinaten und einer Fuer Normalen.
 * OpenGL erlaubt aber nur einen. Der dort gespeicherte Index gilt fuer alle Arrays und alle Arrays
 * muessen gleich lang sein. Deshalb muessen die Fehlenden eintraege in
 * Textur- und Normalenbuffer nachgerechnet und hinzugefuehgt werden.
 * Ausserdem muss die Listenordnung so gemacht werden, dass alle zusammengehoerenden Werte
 * jeweils den gleichen Index haben.
 * 
 * @param posList Eingelesene positionsliste
 * @param textCoordList Eingelesene Texturkoordinatenliste.
 * @param normList Eingelesene Normalenliste.
 * @param facesList Eingelesene Facettenliste.
 * @return Fertig instantiiertes Mesh.
 */
private static Mesh reorderListsByVerts(List<Vector3f> posList, List<Vector2f> textCoordList,
        List<Vector3f> normList, List<Face> facesList) {

    List<Integer> indices = new ArrayList<Integer>();
    
    // Positionsvektorliste in ein Array von Floats umwandeln (Format: X, Y, Z, X, Y, Z... So wie fuer Mesh benoetigt)
    float[] posArr = new float[posList.size() * 3];
    int i = 0;
    for (Vector3f pos : posList) {
        posArr[i * 3] = pos.x;
        posArr[i * 3 + 1] = pos.y;
        posArr[i * 3 + 2] = pos.z;
        i++;
    }
    //Arrays fuer Texturkoordinatenstream und Normalenstream erstellen.
    float[] textCoordArr = new float[posList.size() * 2];
    float[] normArr = new float[posList.size() * 3];

    //Durch die Facettenliste durchgehen und daraus den Indexbuffer und die Textur/Normalenbuffer rechnen.
    for (Face face : facesList) {
        IdxGroup[] faceVertexIndices = face.getFaceVertexIndices();
        
        for (IdxGroup indValue : faceVertexIndices) {
            processFaceVertexByVert(indValue, textCoordList, normList, indices, textCoordArr, normArr);
        }
    }
    
    int[] indicesArr = new int[indices.size()];
    indicesArr = indices.stream().mapToInt((Integer v) -> v).toArray();
    Mesh mesh = new Mesh(posArr, textCoordArr, normArr, indicesArr);
    return mesh;
}

/**
 * Einen Eintrag in der Facettenliste verarbeiten.
 * @param indices Aktuelle Facette
 * @param textCoordList Texturkoordinatenliste
 * @param normList		Normalenliste
 * @param indicesList	Indexliste
 * @param texCoordArr   Finalarray fuer Textur
 * @param normArr		Finalarray fuer Normalen
 */
private static void processFaceVertexByVert(IdxGroup indices, List<Vector2f> textCoordList,
        List<Vector3f> normList, List<Integer> indicesList,
        float[] texCoordArr, float[] normArr) {

    // Index fuer die Positionsvektoren setzen.
    int posIndex = indices.idxPos;
    indicesList.add(posIndex);

    // Texturkoordinaten einsetzen(falls Vorhenden)
    if (indices.idxTextCoord != IdxGroup.NO_VALUE) {
        Vector2f textCoord = textCoordList.get(indices.idxTextCoord);
        texCoordArr[posIndex * 2] = textCoord.x;
        texCoordArr[posIndex * 2 + 1] = 1 - textCoord.y;
    }
    if (indices.idxVecNormal != IdxGroup.NO_VALUE) {
        //Normale einsetzen(Falls Vorhanden).
        Vector3f vecNorm = normList.get(indices.idxVecNormal);
        normArr[posIndex * 3] = vecNorm.x;
        normArr[posIndex * 3 + 1] = vecNorm.y;
        normArr[posIndex * 3 + 2] = vecNorm.z;
    }
}

/**
 * Hilfsklasse die alle Daten fuer eine Facette enthaelt.

 */
protected static class Face {

    /**
     * Die Indexgruppenliste fuer ein Dreieck.
     */
    private IdxGroup[] idxGroups = new IdxGroup[3];

    public Face(String v1, String v2, String v3) {
        idxGroups = new IdxGroup[3];
        //Die Zeilen aus dem Textfile umwandeln.
        idxGroups[0] = parseLine(v1);
        idxGroups[1] = parseLine(v2);
        idxGroups[2] = parseLine(v3);
    }

    /**
     * Extrahiert die Indexgruppendaten aus einer Textzeile.
     * @param Zu verarbeitende Textzeile
     * @return IdxGroup Hilfsklasse mit den geladenen Daten.
     */
    private IdxGroup parseLine(String line) {
        IdxGroup idxGroup = new IdxGroup();

        String[] lineTokens = line.split("/");
        int length = lineTokens.length;
        idxGroup.idxPos = Integer.parseInt(lineTokens[0]) - 1;
       if (length > 1) {

            String textCoord = lineTokens[1];
            idxGroup.idxTextCoord = textCoord.length() > 0 ? Integer.parseInt(textCoord) - 1 : IdxGroup.NO_VALUE;
            if (length > 2) {
                idxGroup.idxVecNormal = Integer.parseInt(lineTokens[2]) - 1;
            }
        }

        return idxGroup;
    }

    public IdxGroup[] getFaceVertexIndices() {
        return idxGroups;
    }
}

/**
 * Hilfsklasse die eine Indexgruppe darstellt.
 * Eine Indexgruppe besteht aus folgenden Eintraegen: 
 * Index auf die Positionsinformation
 * Index auf die texturkoordinaten
 * Index auf die Normale
 *
 */
protected static class IdxGroup {

    public static final int NO_VALUE = -1;

    public int idxPos;

    public int idxTextCoord;

    public int idxVecNormal;

    public IdxGroup() {
        idxPos = NO_VALUE;
        idxTextCoord = NO_VALUE;
        idxVecNormal = NO_VALUE;
    }
}

}
`

Have a nice day.

Thanks for the code!

Two comments / questions:

  • The idea of the custom OBJ loader was to present something simple to introduce other topics and to get some inisghts about how 3D models can be created. For instance, materials are not habdled also. A more complete approach is presented when using assimp. Have you checked if this solves your problems?
  • Although including this code could help in loading these types of models, I think that the current shaders only expect one texture coordinate per vertex, so they could not be used.

What do you think ?

As a final note, if you have already this implemente, may I could inlcude a link to your repo.