Texture mapping troubelshooting - Fish-In-A-Suit/Conquest GitHub Wiki
cube.obj
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
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
When the obj file is read up until the lines beginning with f
, the following occurs:
- construct a new Vector3f out of data of each of the lines starting with
f
and add it to vertices ArrayList - construct a new Vector2f out of data of each of the lines starting with
vt
and add it to textures ArrayList - construct a new Vector3f out of data of each of the lines starting with
vn
and add it to normals ArrayList
When the above is performed, the individual ArrayLists look like:
vertices:
(0) 1.000000 -1.000000 -1.000000
(1) 1.000000 -1.000000 1.000000
(2) -1.000000 -1.000000 1.000000
(3) -1.000000 -1.000000 -1.000000
(4) 1.000000 1.000000 -0.999999
(5) 0.999999 1.000000 1.000001
(6) -1.000000 1.000000 1.000000
(7) -1.000000 1.000000 -1.000000
textures:
(0) 0.2766 0.2633
(1) 0.5000 0.4867
(2) 0.2766 0.4867
(3) 0.7234 0.4867
(4) 0.9467 0.2633
(5) 0.9467 0.4867
(6) 0.0533 0.4867
(7) 0.0533 0.2633
(8) 0.2766 0.0400
(9) 0.5000 0.2633
(10) 0.0533 0.7100
(11) 0.7234 0.2633
(12) 0.0533 0.0400
(13) 0.2766 0.7100
the normals ArrayList is excluded since it's not necessary for texture mapping
// this is wrong, since you have to duplicate positional vectors for vertices and it's therefore too fast to define sizes of arrays for textures and normals based on how much positional vectors for vertices there are at this moment
Then, the size of texturesArray (the final array which holds ordered texture coordinates) and normalsArray are created based on the amount of vertices (which is given by their positions, which are held in vertices ArrayList):
int numVertices = vertices.size();
texturesArray = new float[numVertices*2];
normalsArray = new float[numVertices*3];
Lines starting with f
define so-called face elements. These are defined using vertex positional vector (VPV), texture and normal indices. In the case for this parser, all, a VPV, texture coordinate and normal vector are used to define a face element: f v1/vt1/vn1 v2/vt2/vn2 v3/vt3/vn3
. Based on this lines, data for VPVs, texture coordinates and normals is sorted from ArrayLists to corresponding arrays of corresponding type (float or int) and then passed to the constructor (for GameEntity) to create a new renderable model.
When the lines starting with f are read, each line is first split into a String array based on spacebar elements. For example, if line f 8/4/2 6/5/2 5/6/2
is split based on spacebar elements, the String array would comprise of the following strings: f
, 8/4/2
, 6/5/2
, 5/6/2
.
Then, each of the String-defined number-clusters (which defines attributes for one vertex of a face), is split into a separate String array based on slash elements. For example if String 8/4/2
is split based on slashes, then the resulting String array comprises of the following Strings: 8
, 4
, 2
. Then, this last String array which only contains numbers (actually indices, which point to which VPV, texture coordinate and normal vector should be used for the corresponding vertex of the corresponding face) is sent to the processVertex method.
The first part of the explanation of the following method is incomplete and wrong, since vertices aren't duplicated. Therefore, the same vertex is assigned different texture coordinates which results in the model texture being rendered wrongly.
processVertexprocessVertex(String[] vertexData, List indices, List textures, List normals, float[] textureArray, float[] normalsArray)
- vertexData: A String array specifying attributes for one vertex of a face
- indices: An ArrayList of indices, ie numbers that tell OpenGL how vertices should be connected with one another
- textures: An ArrayList of texture coordinates, which are not yet in correct order
- normals: An ArrayList of normal vectors, which are not yet in correct order
- textureArray: An empty float array which is to be populated with texture coordinates in correct order
- normalsArray: An empty float array which is to be populated with normal vector components in correct order
This method first defines which VPV is used for that vertex of the face out of the first String of vertexData (which in fact refers to the vertex positional vector of that face). Since OpenGL connects vertices by defining their position (and only after that their texture coordinate and normal vectors), this first number of vertexData is considered and index and should therefore be added to indices ArrayList:
int currentVertexPointer = Integer.parseInt(vertexData[0]) - 1;
indices.add(currentVertexPointer);
Note that 1 is substracted from the indices defining VPVs, texture coordinates and normal vectors, since indexing in wavefront file format starts with 1, whereas indexing in Java starts at 0.
Corresponding texture coordinates for the face are then "extracted" from the textures ArrayList (where all texture coordinates are stored as Vector2f-s) based on the second number of vertexData. This data has to be stored in textureArray (which is a float array, since this is what OpenGL uses to perform texture mapping). The index of textureArray where texture coordinates for a specific vertex should be stored are determined by the index of that vertex (which had been resolved from the first number of vertexData). However, since each vertex has two associated vertex coordinates, the index of the vertex has to be multiplied by two:
Vector2f currentTex = textures.get(Integer.parseInt(vertexData[1]) - 1);
textureArray[currentVertexPointer*2] = currentTex.x;
textureArray[currentVertexPointer*2 + 1] = 1.0f - currentTex.y;
Note that the second texture coordinate has to be substracted from one, due to how textures are stored/read (idk really) by OpenGL vs Blender (something to do with which corner of the image they are defined)
After going through all of the lines starting with f
, I've found out that the texture is mapped incorrectly on 4 of the 6 faces of the cube. After doing some research I've found out what causes the problem. All vertices are shared by several different faces, and each different face may have different texture coordinates. When data was being written to texturesArray, elements at several indexes have been overwritten with different texture coordinates several times. The solution is to check if vertex coordinates of a vertex in question match with the vertex coordinates that had been previously assigned to the same element in the texturesArray (note: if the indexes of the array are not yet even populated, then just add them there). If they don't match, then you need to create a new vertex and assign the vertex coordinates in question to this newly created vertex. This of course means that a new positional vector has to be created (which is just a duplicate of the positional vector of the vertex given by the index, ie first number in vertexData) and stored at the end of vertices ArrayList.
This brings about many new complications. Since the number (amount) of vertices is unknown prior to processing all of the face elements, due to the need to duplicate vertices in case of mismatching vertex coordinates, the sizes of texturesArray and normalsArray can't be defined prior to calling processVertex method, rendering them unavailable since arrays can't be used prior to having their size defined. This means that ArrayLists have to be used instead, where texture coordinates and normal vectors are stored in correct order. Then, after all of the face elements are processed, vertices properly duplicated, texture coordinates and normal vectors properly stored, all of these ArrayLists are transformed into arrays and these arrays are then sent off.
However, since indices cannot be used in the case of ArrayLists, you are basically screwed. Not totally. You can only add to an ArrayList. Therefore, you have to compare the current index for the vector with other indices so you can properly store data in these lists. Use a HashMap with position (index of the arraylist) as key and object as value
Following how indexes in texturesArray are populated:
index | value1 | value2 | value3 | value4 | value5 | value6 | change? |
---|---|---|---|---|---|---|---|
0 | 0.2766 | 0.2766 (third f-call) | 0.2766 (sixth f-call) | 0.2766 (14th f-call) | --- | --- | NO |
1 | 0.5133 | 0.5133 (third f-call) | 0.5133 (sixth f-call) | 0.5133 (14th f-call) | --- | --- | NO |
2 | 0.2766 | 0.2766 (third f-call) | 0.2766 (fourth f-call) | 0.2766 (seventh f-call) | 0.2766 (ninth f-call) | --- | NO |
3 | 0.7367 | 0.7367 (third f-call) | 0.7367 (fourth f-call) | 0.7367 (seventh f-call) | 0.7367 (ninth f-call) | --- | NO |
4 | 0.2766 | 0.5 (fifth f-call) | 0.5 (seventh f-call) | 0.2766 (twelveth f-call) | 0.5 (13th f-call) | --- | YES |
5 | 0.96 | 0.7367 (fifth f-call) | 0.7367 (seventh f-call) | 0.96 (twelveth f-call) | 0.7367 (13th f-call) | --- | YES |
6 | 0.5 | 0.5 (fifth f-call) | 0.5 (seventh f-call) | 0.2766 (14th f-call) | --- | --- | YES |
7 | 0.5133 | 0.5133 (fifth f-call) | 0.5133 (seventh f-call) | 0.29000002 (14th f-call) | --- | --- | YES |
8 | 0.9467 | 0.0533 (third f-call) | 0.0533 (sixth f-call) | 0.0533 (ninth f-call) | --- | --- | YES |
9 | 0.5133 | 0.5133 (third f-call) | 0.5133 (sixth f-call) | 0.5133 (ninth f-call) | --- | --- | NO |
10 | 0.9467 | 0.0533 (fourth f-call) | 0.9467 (eighth f-call) | 0.0533 (ninth f-call) | 0.0533 (twelveth f-call) | --- | YES |
11 | 0.7367 | 0.7367 (fourth f-call) | 0.7367 (eighth f-call) | 0.7367 (ninth f-call) | 0.7367 (twelveth f-call) | --- | NO |
12 | 0.7234 | 0.0533 (twelveth f-call) | 0.7234 (13th f-call) | --- | --- | --- | YES |
13 | 0.7367 | 0.96 (twelveth f-call) | 0.7367 (13th f-call) | --- | --- | --- | YES |
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) | YES |
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) | YES |
The above table displays the process of populating texturesArray during the time when polygonal face elements (ie lines beginning with f
) are read. It can clearly be seen that all indexes are populated more than once. Indexes 0, 1, 2, 3, 9 and 11 are populated with same texture coordinates, which is why two of the faces are rendered correctly. Indexes 4, 5, 6, 7, 8, 10, 12, 13, 14 and 15 are populated with different texture coordinates, which is why four faces are rendered incorrectly.
Currently, the final arrays look like the following:
verticesArray
index | element |
---|---|
0 | 1.0 |
1 | -1.0 |
2 | -1.0 |
3 | 1.0 |
4 | -1.0 |
5 | 1.0 |
6 | -1.0 |
7 | -1.0 |
8 | 1.0 |
9 | -1.0 |
10 | -1.0 |
11 | -1.0 |
12 | 1.0 |
13 | 1.0 |
14 | -0.999999 |
15 | 0.999999 |
16 | 1.0 |
17 | 1.000001 |
18 | -1.0 |
19 | 1.0 |
20 | 1.0 |
21 | -1.0 |
22 | 1.0 |
23 | -1.0 |
texturesArray
index | element |
---|---|
0 | 0.2766 |
1 | 0.5133 |
2 | 0.2766 |
3 | 0.7367 |
4 | 0.5 |
5 | 0.7367 |
6 | 0.2766 |
7 | 0.29000002 |
8 | 0.0533 |
9 | 0.5133 |
10 | -1.0 |
11 | -1.0 |
12 | 1.0 |
13 | 1.0 |
14 | -0.999999 |
15 | 0.999999 |
16 | 1.0 |
17 | 1.000001 |
18 | 0.0533 |
19 | 0.7367 |
20 | 0.7234 |
21 | 0.7367 |
22 | 0.0533 |
23 | 0.29000002 |
indicesArray
[1 3 0 7 5 4 4 1 0 5 2 1 2 7 3 0 7 4 1 2 3 7 6 5 4 5 1 5 6 2 2 6 7 0 3 7 ]
Since mode GL_TRIANGLES is used for rendering, OpenGL takes in indices in pairs of three for each "render cycle":
cycle | indices | success |
---|---|---|
1 | 1 3 0 |
INCORRECT |
2 | 7 5 4 |
CORRECT |
3 | 4 1 0 |
CORRECT |
4 | 5 2 1 |
INCORRECT |
5 | 2 7 3 |
INCORRECT |
6 | 0 7 4 |
INCORRECT |
7 | 1 2 3 |
INCORRECT |
8 | 7 6 5 |
CORRECT |
9 | 4 5 1 |
CORRECT |
10 | 5 6 2 |
INCORRECT |
11 | 2 6 7 |
INCORRECT |
12 | 0 3 7 |
INCORRECT |
For each index, it accesses the vertex positional vector (VPV) that is defined by that index. Furthermore, it also takes associated texture coordinates (index2 and index2+1). For the two faces that get rendered correctly, index pairs 7 5 4
, 4 1 0
, 7 6 5
and 4 5 1
are used to form triangles (4 triangles, two cube faces). However, if you look above, you might notice that indices 4, 5, 6 and 7 are assigned various different texture coordinates when loading the obj model --> whether various same texture coordinates or different ones are assigned is not important ???