Hot questions for Using Lightweight Java Game Library in wavefront

Question:

Introduction

I am building a simple wavefront .obj file parser. I've managed to make it read the file, store the contents of it (vertex positions, vertex coordinates, vertex normals (not yet using them) and polygonal face element information (eg. 5/2/3)). This data is then passed to a class (called GameEntity) and from there the data is used to render that specific entity (in this case a cube) to the screen inside the render loop, using glDrawElements in mode GL_TRIANGLES. However, textures are rendered incorrectly.

Source code

OBJLoader.java

public class OBJLoader {    
    /**
     * This method loads a model represented by a wavefront .obj file from resources/models using
     * the specified name of the file (without the .obj extension) and a full path to the texture used
     * by the model. It passes the information (vertex positions, texture coordinates, indices)
     * obtained from the .obj file to the GameEntity constructor.
     * @param fileName
     * @param texturePath
     * @return
     * @throws Exception
     */
    public static GameEntity loadObjModel(String fileName, String texturePath) throws Exception {
        double start = System.nanoTime();

        List<Vector3f> vertices = null;
        List<Vector2f> textures = null;
        List<Vector3f> normals = null;
        List<Integer> indices = null;

        String line;

        float[] vertexPosArray = null;
        float[] texturesArray = null;
        float[] normalsArray = null;
        int[] indicesArray = null;

        try {
            FileReader fr = new FileReader(new File("resources/models/" + fileName + ".obj"));
            BufferedReader br = new BufferedReader(fr);
            vertices = new ArrayList<>();
            textures = new ArrayList<>();
            normals = new ArrayList<>();
            indices = new ArrayList<>();

            while((line = br.readLine()) != null) {

                if (!line.equals("") || !line.startsWith("#")) {
                    String[] splitLine = line.split(" ");

                    switch(splitLine[0]) {
                    case "v":
                        Vector3f vertex = new Vector3f(Float.parseFloat(splitLine[1]), Float.parseFloat(splitLine[2]), Float.parseFloat(splitLine[3]));
                        vertices.add(vertex);
                        System.out.println("[OBJLoader.loadObjModel]: Vertex " + vertex.toString() + " has been added to vertices from " + fileName);
                        break;
                    case "vt":
                        Vector2f texture = new Vector2f(Float.parseFloat(splitLine[1]), Float.parseFloat(splitLine[2]));
                        textures.add(texture);
                        System.out.println("[OBJLoader.loadObjModel]: Texture coordinate [" + texture.x +  ", " + texture.y  + "] has been added to textures from " + fileName);
                        break;
                    case "vn":
                        Vector3f normal = new Vector3f(Float.parseFloat(splitLine[1]), Float.parseFloat(splitLine[2]), Float.parseFloat(splitLine[3]));
                        normals.add(normal);
                        System.out.println("[OBJLoader.loadObjModel]: Normal " + normal + " has been added to normals from " + fileName);
                        break;
                    }
                }
            }

            int numVertices = vertices.size();
            System.out.println("[OBJLoader.loadObjModel]: numVertices = " + numVertices);
            texturesArray = new float[numVertices*2];
            System.out.println("[OBJLoader.loadObjModel]: length of texturesArray = " + texturesArray.length);
            normalsArray = new float[numVertices*3];

            br.close(); //find a better way to start a file again
            br = new BufferedReader(new FileReader("resources/models/" + fileName + ".obj"));

            while((line = br.readLine()) != null) {
                if (line.startsWith("f")) {
                    System.out.println("    [OBJLoader.loadObjModel]: Found line starting with f!"); 
                    String[] splitLine = line.split(" ");

                    //f should be omitted, therefore not starting at index 0
                    String[] v1 = splitLine[1].split("/");
                    String[] v2 = splitLine[2].split("/");
                    String[] v3 = splitLine[3].split("/");

                    System.out.println("        v1 | " + v1[0] + ", " + v1[1] + ", " + v1[2]);
                    System.out.println("        v2 | " + v2[0] + ", " + v2[1] + ", " + v2[2]);
                    System.out.println("        v3 | " + v3[0] + ", " + v3[1] + ", " + v3[2]);

                    processVertex(v1, indices, textures, normals, texturesArray, normalsArray);
                    processVertex(v2, indices, textures, normals, texturesArray, normalsArray);
                    processVertex(v3, indices, textures, normals, texturesArray, normalsArray);
                }
            }
            br.close();

        } catch (Exception e) {
            System.err.println("[OBJLoader.loadObjModel]: Error loading obj model!");
            e.printStackTrace();
        }

        vertexPosArray = new float[vertices.size()*3];
        indicesArray = new int[indices.size()];

        int i = 0;
        for(Vector3f vertex : vertices) {
            vertexPosArray[i++] = vertex.x;
            vertexPosArray[i++] = vertex.y;
            vertexPosArray[i++] = vertex.z;
        }

        for(int j = 0; j<indices.size(); j++) {
            indicesArray[j] = indices.get(j);
        }

        double end = System.nanoTime();
        double delta = (end - start) / 1000_000;
        System.out.println("[OBJLoader.loadObjModel]: Vertices array of " + fileName + ": ");
        System.out.println("[OBJLoader.loadObjModel]: It took " + delta + " milliseconds to load " + fileName);

        System.out.println("[OBJLoader.loadObjModel]: Ordered vertex position array: " + ArrayUtils.getFloatArray(vertexPosArray));
        System.out.println("[OBJLoader.loadObjModel]: Ordererd texture coordinates array: " + ArrayUtils.getFloatArray(texturesArray));
        System.out.println("[OBJLoader.loadObjModel]: Ordererd indices array: " + ArrayUtils.getIntArray(indicesArray));

        return new GameEntity(vertexPosArray, indicesArray, texturesArray, texturePath);
    }

    /**
     * The input to this method is vertex data as a String array, which is used to determine how to
     * arrange texture coordinate and normal vector data (this data is associated with each vertex position)
     * into the correct order in the texture and normals array
     * @param vertexData
     * @param indices
     * @param textrues
     * @param normals
     * @param textureArray
     * @param normalsArray
     */
    private static void processVertex(String[] vertexData, List<Integer> indices, List<Vector2f> textures,
            List<Vector3f> normals, float[] textureArray, float[] normalsArray) {
        int currentVertexPointer = Integer.parseInt(vertexData[0]) - 1;
        System.out.println("[OBJLoader.processVertex]: currentVertexPointer = " + currentVertexPointer);
        indices.add(currentVertexPointer);
        System.out.println("[OBJLoader.processVertex]: Adding " + currentVertexPointer + " to indices!");

        Vector2f currentTex = textures.get(Integer.parseInt(vertexData[1]) - 1);
        textureArray[currentVertexPointer*2] = currentTex.x;
        textureArray[currentVertexPointer*2 + 1] = 1.0f - currentTex.y;
        System.out.println("[OBJLoader.processVertex]: Added vt " + currentTex.x + " to index " + currentVertexPointer*2 + 
                " and vt " + (1.0f - currentTex.y) + " to index " + (currentVertexPointer*2+1) + " of the textureArray");

        Vector3f currentNorm = normals.get(Integer.parseInt(vertexData[2]) - 1);
        normalsArray[currentVertexPointer*3] = currentNorm.x;
        normalsArray[currentVertexPointer*3 + 1] = currentNorm.y;
        normalsArray[currentVertexPointer*3 + 2] = currentNorm.z;
    }
}

GameEntity constructor:

    /**
 * Creates a new textured GameEntity
 * @param vPositions The vertex coordinates of a model
 * @param indices The indices of a model (in which order should the vertices be bound by OpenGL?)
 * @param textureCoordinates The coordinates of a texture (which texture coordinate should be applied to which vertex?)
 * @param texturePath The path of the texture 
 * @throws Exception
 */
public GameEntity(float[] vPositions, int[] indices, float[] textureCoordinates, String texturePath) throws Exception{
    System.out.println("[GameEntity.GameEntity]: Creating a new model texture...");
    modelTexture = new Texture(texturePath);
    System.out.println("[GameEntity.GameEntity]: Creating new mesh based on parameters... ");
    mesh = new Mesh(vPositions, indices, textureCoordinates, modelTexture);

    System.out.println("[GameEntity.GameEntity]: Initializing position, scale and rotation instance fields... ");
    position = new Vector3f(0, 0, 0);
    scale = 1;
    rotation = new Vector3f(0, 0, 0);
}

Pay attention only to the fact that vertex positions, indices and texture coordinates (along with the created texture) are sent to the Mesh constructor:

Mesh constructor

/**
     * This constructor creates a renderable object (instance of Mesh with its texture) out of input parameters by storing them
     * in the vao of that Mesh instance
     * @param vertices The vertex positions of a model
     * @param indices The indices to tell OpenGL how to connect the vertices
     * @param texCoords Texture coordinates (used for texture mapping)
     * @param texture A Texture object
     */
    public Mesh(float[] vertices, int[] indices, float[] texCoords, renderEngine.Texture texture) {
        System.out.println("[Mesh.Mesh]: Creating a new textured Mesh instance... ");

        verticesBuffer = null;
        textureBuffer = null;
        indicesBuffer = null;

        try {
            this.texture = texture;
            vertexCount = indices.length;

            vbos = new ArrayList<>();
            vaos = new ArrayList<>();
            textures = new ArrayList<>();

            System.out.println("[Mesh] Creating and binding the vao (vaoID)");
            vaoID = glGenVertexArrays();
            vaos.add(vaoID);
            glBindVertexArray(vaoID);

            setupVerticesVbo(vertices);

            setupIndicesBuffer(indices);

            setupTextureVbo(texCoords);

            textures.add(texture);

            glBindBuffer(GL_ARRAY_BUFFER, 0);
            glBindVertexArray(0);
        }
    }

Relevant methods for the Mesh class are setupIndicesBuffer and setupTextureVbo:

    private void setupIndicesBuffer(int[] indices)  {
        indicesVboID = glGenBuffers();
        vbos.add(indicesVboID);
        glBindBuffer(GL_ELEMENT_ARRAY_BUFFER, indicesVboID);
        indicesBuffer = BufferUtilities.storeDataInIntBuffer(indices);
        glBufferData(GL_ELEMENT_ARRAY_BUFFER, indicesBuffer, GL_STATIC_DRAW);
    }

    /**
 * This method sets up the texture vbo for a mesh object (buffers data to it and assigns it to attribute list
 * index 1 of the vao)
 * 
 * @param colours - an array of colours of the vertices of a model
 */
private void setupTextureVbo(float[] textures) {
    System.out.println("[Mesh] Creating texture vbo (textureVboID)...");
    textureVboID = glGenBuffers();
    vbos.add(textureVboID);

    System.out.println("   - [Mesh] Creating texture buffer (textureBuffer)...");
    textureBuffer = BufferUtilities.storeDataInFloatBuffer(textures);

    System.out.println("   - [Mesh] Binding textureVboID to GL_ARRAY_BUFER...");
    glBindBuffer(GL_ARRAY_BUFFER, textureVboID);

    System.out.println("   - [Mesh] Buffering data from textureBuffer to GL_ARRAY_BUFFER...");
    glBufferData(GL_ARRAY_BUFFER, textureBuffer, GL_STATIC_DRAW);

    System.out.println("   - [Mesh] Sending texture vbo to index 1 of the active vao...");
    glVertexAttribPointer(1, 2, GL_FLOAT, false, 0, 0);
}

cube.obj

# Blender v2.78 (sub 0) OBJ File: 'cube.blend'
# www.blender.org
o Cube
v 1.000000 -1.000000 -1.000000
v 1.000000 -1.000000 1.000000
v -1.000000 -1.000000 1.000000
v -1.000000 -1.000000 -1.000000
v 1.000000 1.000000 -0.999999
v 0.999999 1.000000 1.000001
v -1.000000 1.000000 1.000000
v -1.000000 1.000000 -1.000000
vt 0.2766 0.2633
vt 0.5000 0.4867
vt 0.2766 0.4867
vt 0.7234 0.4867
vt 0.9467 0.2633
vt 0.9467 0.4867
vt 0.0533 0.4867
vt 0.0533 0.2633
vt 0.2766 0.0400
vt 0.5000 0.2633
vt 0.0533 0.7100
vt 0.7234 0.2633
vt 0.0533 0.0400
vt 0.2766 0.7100
vn 0.0000 -1.0000 0.0000
vn 0.0000 1.0000 0.0000
vn 1.0000 -0.0000 0.0000
vn 0.0000 -0.0000 1.0000
vn -1.0000 -0.0000 -0.0000
vn 0.0000 0.0000 -1.0000
s off
f 2/1/1 4/2/1 1/3/1
f 8/4/2 6/5/2 5/6/2
f 5/7/3 2/1/3 1/3/3
f 6/8/4 3/9/4 2/1/4
f 3/10/5 8/4/5 4/2/5
f 1/3/6 8/11/6 5/7/6
f 2/1/1 3/10/1 4/2/1
f 8/4/2 7/12/2 6/5/2
f 5/7/3 6/8/3 2/1/3
f 6/8/4 7/13/4 3/9/4
f 3/10/5 7/12/5 8/4/5
f 1/3/6 4/14/6 8/11/6
What I have achieved
  • take a look at this video
  • take a look at this page on GitHub for explanation for OBJLoader
  • take a look at this repository for source code (OBJLoader isn't yet included, but you can take a look at other classes such as GameEntity or Mesh, since these two classes are the classes to which vertex data gets sent to after having been extracted from the .obj file).

The video first displays source code for the OBJLoader class. Then, it displays how textures on the cube are mapped incorrectly (with the exception of the back and left face of the cube). Then, it shows a file in which I have analysed how many times each index in the array which holds texture coordinate information is written to. At the end, the texture which should be mapped is shown.

The problem

As shown in the video, four out of six faces of the cube are mapped with the texture incorrectly.

I do know that: - texture coordinates are read from the .obj file correctly and stored inside textures ArrayList. The code for this can be found in the switch clause:

case "vt":
        Vector2f texture = new Vector2f(Float.parseFloat(splitLine[1]), Float.parseFloat(splitLine[2]));
        textures.add(texture);
        break;

I am 50/50 sure that: - indices are determined correctly from the .obj file, since if they weren't determined correctly, the cube would not have been drawn at all. The code relevant for this can be found in the processVertex method:

int currentVertexPointer = Integer.parseInt(vertexData[0]) - 1;
indices.add(currentVertexPointer);

I am not sure whether: - the textures are ordered correctly in the final float array called texturesArray. The code relevant for this step can be found in the processVertex method:

Vector2f currentTex = textures.get(Integer.parseInt(vertexData[1]) - 1);
textureArray[currentVertexPointer*2] = currentTex.x;
textureArray[currentVertexPointer*2 + 1] = 1.0f - currentTex.y;
How it was supposed to work:

First, texture coordinates are to be read from a .obj file and stored as Vector2f's (2 dimensional vectors, basically just storage for x and y values) in ArrayList named textures.

For OpenGL to work correctly, however, these texture coordinates should be rearranged so they are matched with the corresponding vertex (at least this is how I've read from multiple tutorials). This is done by reading so called polygonal face elements.

These polygonal face elements are described by each line beginning with an f. Each such line describes three vertices. Each vertex is represented by a vertex position, texture coordinate and a normal vector. An example of such line: f 8/4/2 6/5/2 5/6/2. Take a closer look at the 8/4/2 vertex representation. This tells that this vertex has position equal to the eighth specified vertex position in the .obj file (-1.000000 1.000000 -1.000000), texture coordinate equal to the 4th specified texture coordinate in the file (0.7234 0.4867) and second normal vector (0.0000 1.0000 0.0000).

processVertex method is called three times per when a line beginning with an f is found, to process each of the vertices describing that polygonal face element (once for 8/4/2, once for 6/5/2 and once for 5/6/2). Each time, one set of data is passed to the method as a String array (which is split at the location of forward slashes), followed by List textures, List normals, float[] textureArray and float[] normalsArray.

private static void processVertex(String[] vertexData, List<Integer> indices, List<Vector2f> textures,
        List<Vector3f> normals, float[] textureArray, float[] normalsArray) {
    int currentVertexPointer = Integer.parseInt(vertexData[0]) - 1;
    System.out.println("[OBJLoader.processVertex]: currentVertexPointer = " + currentVertexPointer);
    indices.add(currentVertexPointer);
    System.out.println("[OBJLoader.processVertex]: Adding " + currentVertexPointer + " to indices!");

    //something probably wrong here 
    Vector2f currentTex = textures.get(Integer.parseInt(vertexData[1]) - 1);
    textureArray[currentVertexPointer*2] = currentTex.x;
    textureArray[currentVertexPointer*2 + 1] = 1.0f - currentTex.y;
    System.out.println("[OBJLoader.processVertex]: Added vt " + currentTex.x + " to index " + currentVertexPointer*2 + 
            " and vt " + (1.0f - currentTex.y) + " to index " + (currentVertexPointer*2+1) + " of the textureArray");

    Vector3f currentNorm = normals.get(Integer.parseInt(vertexData[2]) - 1);
    normalsArray[currentVertexPointer*3] = currentNorm.x;
    normalsArray[currentVertexPointer*3 + 1] = currentNorm.y;
    normalsArray[currentVertexPointer*3 + 2] = currentNorm.z;
}

Note that vertex normal data can be ignored, as it is irrelevant.

The current vertex index is first determined by substracting one from the first number in the passed String array (for example, if 8/4/2 was passed as parameter, 7 would be assigned to currentVertexPointer). The reason for substracting one is that images in wavefront .obj files start at one, whereas indices in Java start at 0. Then, this number is added to the indices List.

Then, the corresponding texture coordinates (represented as a Vector2f) are obtained from textures List, by reading the second number in the array of Strings passed as parameter and substracting one (for example, if 8/4/2 was passed as parameter, the Vector3f at index 3 of textures List would be obtained): Vector2f currentTex = textures.get(Integer.parseInt(vertexData[1]) - 1);

Now that corresponding texture coordinates to the vertex which is currently being processed (denoted by currentVertexPointer) are now obtained, the data must now be stored accordingly inside textureArray, which is then passed to GameEntity to construct a renderable object (details about this won't be discussed... bottomline: ordering in this array is important, as it impacts how the texture is mapped to the model).

To store the texture coordinates accordingly, the first texture coordinate (u or s as some call them) is stored at the index of textureArray which is twice as big as currentVertexPointer, as each vertex has two texture coordinates: textureArray[currentVertexPointer*2] = currentTex.x;. The second texture coordinate (v or t as some prefer) is stored at the index of textureArray which is one bigger than the index of the first texture coordinate: textureArray[currentVertexPointer*2 + 1] = 1.0f - currentTex.y;.

Note that the second texture coordinate is substracted from 1.0f, due to differences between texture coordinate spaces of wavefront files and OpenGL.

My observations

I have tracked each time a new texture is assigned to a new (or existing) index in the textureArray and found out that some of the indexes are overwritten, which may be the cause of the problems

After analysing the data, I've ended up with such file, which displays populated indexes in the textureArray on the right and various elements which are assigned to these indexes during execution:

 Index | values that get assigned to the index during execution of the program
    0 | 0.2766 ... 0.2766 (third f-call) ... 0.2766 (sixth f-call) ... 0.2766 (14th f-call)
    1 | 0.5133 ... 0.5133 (third f-call) ... 0.5133 (sixth f-call) ... 0.5133 (14th f-call)
    2 | 0.2766 ... 0.2766 (third f-call) ... 0.2766 (fourth f-call) ... 0.2766 (seventh f-call) ... 0.2766 (ninth f-call)
    3 | 0.7367 ... 0.7367 (third f-call) ... 0.7367 (fourth f-call) ... 0.7367 (seventh f-call) ... 0.7367 (ninth f-call)
    4 | 0.2766 ... 0.5 (fifth f-call) ... 0.5 (seventh f-call) ... 0.2766 (twelveth f-call) ... 0.5 (13th f-call)
    5 | 0.96 ... 0.7367 (fifth f-call) ... 0.7367 (seventh f-call) ... 0.96 (twelveth f-call) ... 0.7367 (13th f-call)
    6 | 0.5 ... 0.5 (fifth f-call) ... 0.5 (seventh f-call) ... 0.2766 (14th f-call)
    7 | 0.5133 ... 0.5133 (fifth f-call) ... 0.5133 (seventh f-call) ... 0.29000002 (14th f-call)
    8 | 0.9467 ... 0.0533 (third f-call) ... 0.0533 (sixth f-call) ... 0.0533 (ninth f-call)
    9 | 0.5133 ... 0.5133 (third f-call) ... 0.5133 (sixth f-call) ... 0.5133 (ninth f-call)
    10 | 0.9467 ... 0.0533 (fourth f-call) ... 0.9467 (eighth f-call) ... 0.0533 (ninth f-call) ... 0.0533 (twelveth f-call)
    11 | 0.7367 ... 0.7367 (fourth f-call) ... 0.7367 (eighth f-call) ... 0.7367 (ninth f-call) ... 0.7367 (twelveth f-call)
    12 | 0.7234 ... 0.0533 (twelveth f-call) ... 0.7234 (13th f-call)
    13 | 0.7367 ... 0.96 (twelveth f-call) ... 0.7367 (13th f-call)
    14 | 0.7234 ... 0.7234 (fifth f-call) ... 0.0533 (sixth f-call) ... 0.7234 (eighth f-call) ... 0.7234 (13th f-call) ... 0.0533 (14th f-call)
    15 | 0.5133 ... 0.5133 (fifth f-call) ... 0.29000002 (sixth f-call) ... 0.5133 (eighth f-call) ... 0.5133 (13th f-call) ... 0.29000002 (14th f-call)

    All of the indexes in the texturesArray have been accessed and assigned values several time.

    Indexes with unchanged values (ie, indexes which have been assigned the same value every time):
    0, 1, 2, 3, 9, 11

    Indexes with changed value (ie, indexes which have been assigned different values):
    4, 5, 6, 7, 8, 10, 12, 13, 14

It is obvious that the majority of the indexes are overwritten with different texture coordinate data. Indexes which are overwritten with the same texture coordinate data are 0, 1, 2, 3, 9 and 11. I therefore speculate, that the back and left face are mapped correctly due to the face that these indexes are overwritten with the same values, whereas other indexes are overwritten with different values.

How to fix the problem?

Huh, this has turned out to be quite long, hasn't it? Thank you for all the time taken, I really appreciate it.

Edit #1

After the first answer from @florentt I have accomplished to integrate the following code into the processVertex method:

    private static void processVertex(String[] vertexData, List<Integer> indices, List<Vector2f> textures,
        List<Vector3f> normals, float[] textureArray, float[] normalsArray) {

    int currentVertexPointer = Integer.parseInt(vertexData[0]) - 1;
    System.out.println("[OBJLoader.processVertex]: currentVertexPointer = " + currentVertexPointer);
    indices.add(currentVertexPointer);

    //THIS IS NEW
    Vector2f currentTex = textures.get(Integer.parseInt(vertexData[1]) - 1);
    if ((textureArray[currentVertexPointer*2] + textureArray[currentVertexPointer*2+1])== 0 ) { //if the index hasn't been populated yet, store it
        textureArray[currentVertexPointer*2] = currentTex.x;
        textureArray[currentVertexPointer*2+1] = 1.0f - currentTex.y;
    } else {
        //create a new vertex (index?) and associate it with second coordinate u
        //create a new vertex (index?) and associate it with texture coordinate v
    }
    //END OF NEW CODE

    Vector3f currentNorm = normals.get(Integer.parseInt(vertexData[2]) - 1);
    normalsArray[currentVertexPointer*3] = currentNorm.x;
    normalsArray[currentVertexPointer*3 + 1] = currentNorm.y;
    normalsArray[currentVertexPointer*3 + 2] = currentNorm.z;
}

He reported, that the problem is caused by the fact that one vertex is associated with several different vertex coordinates (which belong to different faces). Each such vertex should be duplicated and assigned a corresponding texture coordinate. I've added an if clause to processVertex method, which checks whether indexes for a specific texture coordinate set are empty or not inside texturesArray. If an index in a float array is empty, then it holds value 0. To calculate if this and the consecutive indexes are empty (each vertex has two texture coordinates), then the sums of the values at these indexes must be 0 if they are both empty. If these two indexes haven't been populated with texture coordinates yet, then assign them the texture coordinates which can be obtained from the currently processing polygonal face element (ie. 8/4/2).

However, I haven't got the slightest clue about what to do when the index has already been populated. I know that the position vector should be duplicated and assigned the corresponding texture coordinates (accessed the same way as mentioned above), but doesn't that change the whole ArrayList of original positional vectors which had been read from the .obj file? In the case of the processVertex(String[] vertexData, List<Integer> indices, List<Vector2f> textures, List<Vector3f> normals, float[] textureArray, float[] normalsArray) method, where should these duplicated vectors be stored? Should you just duplicate the index of this positional vector and then assign a texture coordinate to this index? How should one then store texture coordinates to this new index?

My first try at this was to introduce the following if-else statement:

if ((textureArray[currentVertexPointer*2] + textureArray[currentVertexPointer*2+1])== 0 ) { //if the index hasn't been populated yet, store it
        textureArray[currentVertexPointer*2] = currentTex.x;
        textureArray[currentVertexPointer*2+1] = 1.0f - currentTex.y;
    } else {
        int duplicateVertexPointer = currentVertexPointer;
        indices.add(duplicateVertexPointer);
        textureArray[duplicateVertexPointer*2] = currentTex.x;
        textureArray[duplicateVertexPointer*2+1] = currentTex.y;
    }

The above, hwoever, works even worse as it previously had. Now, the cube is not even rendered as a cube, but rather as one separate triangle and face with emptyness in between. Please help :(


Answer:

It seems you are very close to finding out the issue. Yes some data is beeing overwritten because an obj vertex can have multiple normal or texture vector on different faces. The issue is that it is not the case for vertices attributes in openGL.

What you need to do is check if your vertex already has a texture coordinate and if it does you create a new vertex and associate to it the second texture. Be carefull it can be a bit messy. Let's say you have v1, a vertex. in different faces it has several texture coordinates.

V1 -> t1; v1 -> t2; v1 -> t2. So you say V1 -> t1, easy. But then you see another coordinate on the same vertex. So you clone v1, and get v2 -> t2 (with v1 == v2). Now come v1->t2, you shouldn't create a new vertex v3, because v2 already exists and fits perfectly.

So to do it properly you need to keep track of the clones and see if any of them fit the combination index/coordinate.

It gets even messier when you have both normal and texture coordinate because two faces can share texture coordinates but not normals or the other way around, all cominations exist. So sometimes you'll have a clone that fits a specific combination but not all of them.

I did a obj parser that works(ish) but it's kinda messy, so giving you the code would be a disservice. I hope I at least sent you on the right track.

EDIT here's how I'd do it if I were to make a parser again. Every time I add a vertex to my face I'd call the following function:

vector<vertex> vertices; //All the vertices in my object, this includes the position, texture coordinate and normal
vector<vector<int>> synonyms; // For each vertex, the array of vertices which are the same (cloned vertices)
vector<int> normalIndex;
vector<int> UV index;
int normalIn;
int textureIn;
int vertexIn; //each elements in one point of my face declaration in the obj file vertexIn/textureIn/normalIn

funtion(all of the above)
{

    vector<int> synonymsVertex = synonyms[vertexIn]; //We get the synonyms of the vertex we want to add
    for(int vertexClone : synonymsVertex)
    {
        vertex vertexObj = vertices[vertexClone];
        //In the case the combination doesn't exist, we clone the vertex and add it to the list
        if(vertexObj.normal != normalIn || vertexObj.UV != textureIn)
        {
             vertex newVertex(vertexObj, normalIn, textureIn);
             vertices.push_back(newVertex);
             synonymsVertex.push_back(vertices.size - 1);
        }
    }
}

Question:

I'm working on a simple render engine in java which can render OBJ files to the screen. I'm currently working on the lighting system which is used to light up the models which are present on the screen. Before introducing the lighting system I was able to load models to the screan easely:

however, when I add the light to the screen the model does no longer show up. I'm using the following shader to render the light:

VertexShader:

#version 150

in vec3 position;
in vec2 textureCoordinates;
in vec3 normals;

out vec2 passTextureCoordinates;
out vec3 surfaceNormal;
out vec3 toLightVector;

uniform mat4 transformationMatrixTextured;
uniform mat4 projectionMatrixTextured;
uniform mat4 viewMatrixTextured;
uniform vec3 lightLocation;

void main(void){
    vec4 worldPosition = transformationMatrixTextured * vec4(position,1.0);
    gl_Position = projectionMatrixTextured * viewMatrixTextured * worldPosition;
    passTextureCoordinates = textureCoordinates;

    surfaceNormal =  (transformationMatrixTextured * vec4(normals,0.0)).xyz;
    toLightVector =  lightLocation - worldPosition.xyz;
}

FragmentShader:

#version 150

in vec2 passTextureCoordinates;
in vec3 surfaceNormal;
in vec3 toLightVector;

out vec4 out_Color;

uniform sampler2D textureSampler;
uniform vec3 lightColor;

void main(void){

    vec3 unitNormal = normalize(surfaceNormal);
    vec3 unitLightVector = normalize(toLightVector);

    float nDot1 = dot(unitNormal, unitLightVector);
    float brightness = max(nDot1, 0.0);
    vec3 diffuse = brightness * lightColor;

    out_Color = vec4(diffuse, 1.0) * texture(textureSampler,passTextureCoordinates);

}

I've been using the tutorial series by ThinMatrix to help me with creating this program. However, the one big difference is that I also want to be able to load programmaticly created models, as uppose to only using models loaded by the OBJLoader. Because of this I had to create a way to calculate normals given a vertices array and an index array.

My approach to this problem was this:

/**
 * Sum.
 *
 * @param arg1 the arg 1
 * @param arg2 the arg 2
 * @return the vector 3 f
 */
public static Vector3f sum(Vector3f arg1, Vector3f arg2) {
    return new Vector3f(arg1.x + arg2.x, arg1.y + arg2.y, arg1.z + arg2.z);
}

/**
 * Subtract.
 *
 * @param arg1 the arg 1
 * @param arg2 the arg 2
 * @return the vector 3 f
 */
public static Vector3f subtract(Vector3f arg1, Vector3f arg2) {
    return new Vector3f(arg1.x - arg2.x, arg1.y - arg2.y, arg1.z - arg2.z);
}

/**
 * Cross product.
 *
 * @param arg1 the arg 1
 * @param arg2 the arg 2
 * @return the vector 3 f
 */
public static Vector3f crossProduct(Vector3f arg1, Vector3f arg2) {
    return new Vector3f(arg1.y * arg2.z - arg2.y * arg1.z, arg2.x * arg1.z - arg1.x * arg2.z, arg1.x * arg2.y - arg2.x * arg1.y);
}

/**
 * Gets the normals.
 *
 * @param vertices the vertices
 * @param indexes the indexes
 * @return the normals
 */
public static float[] getNormals(float[] vertices, int[] indexes) {
    vertices = convertToIndexless(vertices, indexes);
    Vector3f tmp;
    float[] tmpArray = new float[vertices.length / 3];
    int tmpArrayCounter = 0;
    for(int i = 0; i < vertices.length; i+=9) {
        Vector3f edge1 = subtract(new Vector3f(vertices[i], vertices[i + 1], vertices[i + 2]) , new Vector3f(vertices[i + 3], vertices[i + 4], vertices[i + 5]));
        Vector3f edge2 = subtract(new Vector3f(vertices[i], vertices[i + 1], vertices[i + 2]) , new Vector3f(vertices[i + 6], vertices[i + 7], vertices[i + 8]));

        tmp = crossProduct(edge1, edge2);
        tmpArray[tmpArrayCounter++] = tmp.getX();
        tmpArray[tmpArrayCounter++] = tmp.getY();
        tmpArray[tmpArrayCounter++] = tmp.getZ();
    }
    return tmpArray;
}

/**
 * Convert to indexless.
 *
 * @param vertices the vertices
 * @param indexes the indexes
 * @return the float[]
 */
private static float[] convertToIndexless(float[] vertices, int[] indexes) {
    float[] tmpArray = new float[indexes.length * 3];
    for(int i = 0; i < indexes.length; i++) {
        tmpArray[i * 3]     = vertices[indexes[i] * 3];
        tmpArray[i * 3 + 1] = vertices[indexes[i] * 3 + 1];
        tmpArray[i * 3 + 2] = vertices[indexes[i] * 3 + 2];
    }
    return tmpArray;
}

I've based this approach on this question about calculating normals. As I said before I'm not able to render the model when adding a light to the program. Am I doing something wrong in my calculations?


Answer:

Long description to find out root problem

Honestly I do not get what means the model does no longer show up. So I am going to post a few hints how you can find out what is going on.

  • Is the rendered model all black?

    • There could be several sources of problems which have to be checked:

      • Turn on some background color like blue to see what is the result.
        • Is the model invisible, so all is blue? Normals of the model are wrong. You can turn on to render both sides, keyword face culling to find out. If it renders with back-face rendering then the normals of the model are an issue
        • Is the model visible, but the model is rendered black and the background is blue?
          • either direction of the light is wrong (very likely)
          • I suggest to change the shader so there is always some minimum amount of light, so called ambient light. Then each object gets a minimum lightning, even though there is a bad angle with respect to the light source like intensity = max(dot(n, l), ambience); in the vertex shader with ambience as a parameter and n the normalized normal of the object and l the normalized light direction. In the fragment shader I used gl_FragColor = vec4(intensity*tc.r,intensity*tc.g,intensity*tc.b,tc.a); with tc being a vec4 texture coordinate. In this way the object always has some light
          • or some bug in the shader code (could not spot a problem there at first sight so, but who knows?) Well I used dot product of model normal to light direction for this, in your code there seems to be the cross product.
      • The texture is not used / accepted / assigned right to the model or the vector is returning only one pixel location which is all black
    • Is there an error in the shader code? Compilation error which is logged as an exception?

      • fix it ;)

I guess the problem is a mixture of using the cross product and wrong light direction (I had the same problem in my model at the beginning)

Edit One more comment to dot product: the dot product is what you need for finding out the intensity. The geometric definition of it dot(a,b)=length(a)*length(b)*cos(alpha) where alpha is the angle between a and b.

  • If the model normal is the same direction as the light direction then you want full intensity.

  • If the model normal is in orthogonal direction (90 degrees) as the light direction then you want 0 intensity (or ambience intensity)

  • If the model normal is 60 degrees in the direction of the light direction then you want half intensity (or ambience intensity)

etc

Edit 2 - because the dot product can have negative results now the max(dot(a,b),0) would clip this off (for opposite direction). For quick ambience you can change this to max(dot(a,b),0.3)

Summary:

  • Use dot product for calculating intensity.

  • Use ambient light for keeping some light, even if the angle with respect to the light source is bad.

Question:

So, I'm trying to load (Wavefront) OBJ models in Java. Currently it loads vertex positions properly but texture coords are messed up:

This is what I see in the engine:

This is what I see in blender:

My current loading code is here:

 private void loadOBJ(String filename)
    {
    ArrayList<Vector3f> verts = new ArrayList<Vector3f>();
    ArrayList<Vector3f> norms = new ArrayList<Vector3f>();
    ArrayList<Vector2f> uvs = new ArrayList<Vector2f>();
    ArrayList<Integer> ints = new ArrayList<Integer>(); //Indices
    ArrayList<Integer> nints = new ArrayList<Integer>(); //Normal indices
    ArrayList<Integer> tints = new ArrayList<Integer>(); //Texture coord indices


    try {
        BufferedReader reader = new BufferedReader(new FileReader(filename));
        String line;

        while ((line = reader.readLine()) != null) {
            String[] tokens = line.split(" ");


            if(tokens[0].startsWith("vn"))
            {
                norms.add(new Vector3f(Float.parseFloat(tokens[1]), 
                                       Float.parseFloat(tokens[2]), 
                                       Float.parseFloat(tokens[3])));   
            }
            else if(tokens[0].startsWith("vt"))
            {
                uvs.add(new Vector2f(Float.parseFloat(tokens[1]), 
                                     Float.parseFloat(tokens[2])));
            }
            else if(tokens[0].startsWith("v")) 
            {
                verts.add(new Vector3f(Float.parseFloat(tokens[1]), 
                                       Float.parseFloat(tokens[2]), 
                                       Float.parseFloat(tokens[3])));
            }
            else if(tokens[0].startsWith("f"))
            {
                ints.add(Integer.parseInt(tokens[1].split("/")[0]) - 1);
                ints.add(Integer.parseInt(tokens[2].split("/")[0]) - 1);
                ints.add(Integer.parseInt(tokens[3].split("/")[0]) - 1);

                tints.add(Integer.parseInt(tokens[1].split("/")[1]) - 1);
                tints.add(Integer.parseInt(tokens[2].split("/")[1]) - 1);
                tints.add(Integer.parseInt(tokens[3].split("/")[1]) - 1);

                nints.add(Integer.parseInt(tokens[1].split("/")[2]) - 1);
                nints.add(Integer.parseInt(tokens[2].split("/")[2]) - 1);
                nints.add(Integer.parseInt(tokens[3].split("/")[2]) - 1);

                if(tokens.length > 4) //For quads
                {
                    ints.add(Integer.parseInt(tokens[1].split("/")[0]) - 1);
                    ints.add(Integer.parseInt(tokens[4].split("/")[0]) - 1);
                    ints.add(Integer.parseInt(tokens[2].split("/")[0]) - 1);

                    tints.add(Integer.parseInt(tokens[1].split("/")[1]) - 1);
                    tints.add(Integer.parseInt(tokens[4].split("/")[1]) - 1);
                    tints.add(Integer.parseInt(tokens[2].split("/")[1]) - 1);

                    nints.add(Integer.parseInt(tokens[1].split("/")[2]) - 1);
                    nints.add(Integer.parseInt(tokens[4].split("/")[2]) - 1);
                    nints.add(Integer.parseInt(tokens[2].split("/")[2]) - 1);
                }
            }

        }
        reader.close();


    } catch (IOException e) {
        System.err.println("Could not read file.");
        e.printStackTrace();
        System.exit(-1);
    }

    //Now convert the loaded data to internal format: VertexData[] that con tains positions, uvs, and normals, and int[] that has indices

    vertices = new VertexData[verts.size()];
    indices = new int[ints.size()];

    for(int i = 0; i < ints.size(); i++)
    {
        indices[i] = ints.get(i);
        int j = ints.get(i);
        int k = tints.get(i);
        int q = nints.get(i);

        vertices[j] = new VertexData();

        vertices[j].setXYZ(verts.get(j).x, verts.get(j).y, verts.get(j).z);
        vertices[j].setST(uvs.get(k).x, uvs.get(k).y);
        vertices[j].setNormal(norms.get(q).x, norms.get(q).y, norms.get(q).z);
    }

}

As you can see from the pictures current code failes to properly load uvs, but I can't figure out whats wrong. Help?


Answer:

Your code does not correctly generate the vertex data suitable for the GL. As you know, in the obj format, there is an separate array for vertex position, normals, texcoords and so on. And faces are formed by independetly indexing into them. In the GL, a vertex is the set of all of its attributes, and you can't use separate indices per attribute (at least not directly; modern GL is flexible enough to allow you to implement that level of indirection in the vertex shader, but that would be another story).

Your code maybe attempts at fixing this, but the solution is wrong. You simply use the vertex index from the OBJ file as the global index, and write your vertex containing all of the attributes using the other indices no matter, if the same vertex index might be used with a different combination of normals or texcoords - you simply overwrite it with the last such combination.

THe coorect approach would creating a separate vertex for each unique (vertex,normal,texcoord) combination that occurs. As a result, the number of vertices will of course increase, and the index value can't be directly reused for the element array, either.