2
votes

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 :(

2

2 Answers

2
votes

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);
        }
    }
}
0
votes

The problem is caused due to duplicating texture coordinates for one single vertex. This is due to the nature of texture seams in Blender, where the model had been created. One quick solution to this is to select all of the edges of the model where a texture seam is marked and then split (duplicate) the edges (Mesh --> Edge --> Edge split).