OpenGL 1.5: Loading Wavefront .obj models

Started by Andrew_3ds, November 25, 2014, 20:26:58

Previous topic - Next topic

Andrew_3ds

I made a .obj loader to load 3d models, and I have got the normal & vertex pointers to work fine, but OpenGL doesn't have a 'faces' pointer function. I don't know how to use faces and my model looks all glitched.

Image of the model:


Here is the objloader class:
public class ObjLoader {
    static List<Vector3f> vertices = new ArrayList<Vector3f>();
    static List<Vector3f> normals = new ArrayList<Vector3f>();
    static List<Vector3f> faces = new ArrayList<Vector3f>();
    static List<Vector3f> facesnormal = new ArrayList<Vector3f>();
    
    public static StaticModel loadObj(String loc) {
        int linecounter = 0;
        String line;
        
        try {
            BufferedReader r = new BufferedReader(new FileReader(loc));
            while((line = r.readLine()) != null) {
                linecounter++;
                line = line.trim();
                if(line.startsWith("v ")) {
                    String[] splitLine = new String[4];
                    splitLine = line.split(" ");
                    vertices.add(new Vector3f(Float.valueOf(splitLine[1]), Float.valueOf(splitLine[2]), Float.valueOf(splitLine[3])));
                }
                if(line.startsWith("vn ")) {
                    String[] splitLine = new String[4];
                    splitLine = line.split(" ");
                    normals.add(new Vector3f(Float.valueOf(splitLine[1]), Float.valueOf(splitLine[2]), Float.valueOf(splitLine[3])));
                }
                if(line.startsWith("f ")) {
                    String[] splitLine = new String[4];
                    splitLine = line.split(" ");
                    String[] f1 = splitLine[1].split("/");
                    String[] f2 = splitLine[2].split("/");
                    String[] f3 = splitLine[3].split("/");
                    faces.add(new Vector3f(Float.valueOf(f1[0]), Float.valueOf(f1[1]), Float.valueOf(f1[2])));
                    facesnormal.add(new Vector3f(Float.valueOf(f3[0]), Float.valueOf(f3[1]), Float.valueOf(f3[2])));
                }
            }
            
            r.close();
            System.out.println("Loaded "+r.toString()+"; "+linecounter+" lines");
            
            float[] VERTEX_ARRAY = new float[vertices.size()*3];
            for(int i =0; i < VERTEX_ARRAY.length; i+=3) {
                float x = vertices.get(i/3).x;
                float y = vertices.get(i/3).y;
                float z = vertices.get(i/3).z;
                
                VERTEX_ARRAY[i] = x;
                VERTEX_ARRAY[i+1] = y;
                VERTEX_ARRAY[i+2] = z;
            }
            
            float[] NORMALS_ARRAY = new float[normals.size()*3];
            for(int i =0; i < NORMALS_ARRAY.length; i+=3) {
                float x = normals.get(i/3).x;
                float y = normals.get(i/3).y;
                float z = normals.get(i/3).z;
                
                NORMALS_ARRAY[i] = x;
                NORMALS_ARRAY[i+1] = y;
                NORMALS_ARRAY[i+2] = z;
            }
            
            System.out.println(Arrays.toString(VERTEX_ARRAY));
            return new StaticModel(VERTEX_ARRAY, NORMALS_ARRAY);
        } catch (IOException ex) {
            Logger.getLogger(ObjLoader.class.getName()).log(Level.SEVERE, null, ex);
        }
        
        return null;
    }
}


StaticModel class:
public class StaticModel {
    int vbo;
    int normalVbo;
    int i = GL_ARRAY_BUFFER;
    
    int vertexAmount;
    int vertexSize = 3;
    int normalAmount;
    int normalSize = 3;
    
    FloatBuffer vertexData;
    FloatBuffer normalData;
    
    float x;
    float y;
    float z;

    public StaticModel(float[] vertices, float[] normals) {
        vbo = glGenBuffers();
        normalVbo = glGenBuffers();
        
        vertexAmount = vertices.length/vertexSize;
        vertexData = asFloatBuffer(vertices);
        
        normalAmount = normals.length/normalSize;
        normalData = asFloatBuffer(normals);
        
        glBindBuffer(i, vbo);
        glBufferData(i, vertexData, GL_STATIC_DRAW);
        glBindBuffer(i, 0);
        
        glBindBuffer(i, normalVbo);
        glBufferData(i, normalData, GL_STATIC_DRAW);
        glBindBuffer(i, 0);
    }
    
    public void draw() {
        glBindBuffer(i, vbo);
        glVertexPointer(vertexSize, GL_FLOAT, 0, 0l);
        
        glBindBuffer(i, normalVbo);
        glNormalPointer(GL_FLOAT, 0, 0l);
        
        glEnableClientState(GL_VERTEX_ARRAY);
        glPushMatrix();
            glLoadIdentity();
            glTranslatef(x,y,z);
            glScalef(10,10,10);
            glDrawArrays(GL_TRIANGLES, 0, vertexAmount);
        glPopMatrix();
        glDisableClientState(GL_VERTEX_ARRAY);
        
        glBindBuffer(i, 0);
    }
    
    public void delete() {
        glDeleteBuffers(vbo);
        glDeleteBuffers(normalVbo);
        System.out.println("Deleted model #"+vbo);
    }
}

Kai

Hi Andrew_3ds,

I am afraid, the Wavefront obj model format does not work like this.
The vertex attributes, like 'v', 'vn' and 'vt' are not in the order in which you need to render them with OpenGL, but they instead are just enumerated to exist.

The faces 'f' then indexes (with 1-based index!) into this 'v' and 'vn' and 'vt' list.

Also take special notice that those face indices can also be negative, in which case -1 means the last specified vertex before encountering the 'f' element.

What you also need to take into account is that faces need not be triangles, but arbitrary polygons. So, when loading an obj model, you should first check, whether it has properly been triangulated.

If you consider using an index buffer, also have in mind that the 'f' element allows all three vertex attributes to be indexed in any arbitrary order, which would forbid to use an OpenGL index buffer straight away from the data. You must reindex the data to do that, instead.

Andrew_3ds

Quote from: Kai on November 25, 2014, 20:37:31
Hi Andrew_3ds,

I am afraid, the Wavefront obj model format does not work like this.
The vertex attributes, like 'v', 'vn' and 'vt' are not in the order in which you need to render them with OpenGL, but they instead are just enumerated to exist.

The faces 'f' then indexes (with 1-based index!) into this 'v' and 'vn' and 'vt' list.

Also take special notice that those face indices can also be negative, in which case -1 means the last specified vertex before encountering the 'f' element.

What you also need to take into account is that faces need not be triangles, but arbitrary polygons. So, when loading an obj model, you should first check, whether it has properly been triangulated.

If you consider using an index buffer, also have in mind that the 'f' element allows all three vertex attributes to be indexed in any arbitrary order, which would forbid to use an OpenGL index buffer straight away from the data. You must reindex the data to do that, instead.

Okay I understand now. So then how do I use the f data to put them in correct order?

Kai

First, you need to obviously keep a list of all encountered vertex attributes (separate list per attribute type).
Then, when parsing a 'f' element, you extract the actual vertex data from your remembered vertex attribute list at the respective index provided by the 'f' element.
This way, you build your vertex, normal (and optionally texture) buffer data.

Andrew_3ds

Quote from: Kai on November 25, 2014, 20:53:19
First, you need to obviously keep a list of all encountered vertex attributes (separate list per attribute type).
Then, when parsing a 'f' element, you extract the actual vertex data from your remembered vertex attribute list at the respective index provided by the 'f' element.
This way, you build your vertex, normal (and optionally texture) buffer data.

Okay I understand what you mean, but I am having a hard time trying to get it to work in my code...
I tried using a for loop to loop through the faces arraylist, then putting the vertex in slot "i" from my vertex arraylist into my array, but I don't think I'm doing it right

float[] VERTEX_ARRAY = new float[vertices.size()*3];
            int index = 0;
            System.out.println(faces.size());
            for(int i = 0; i < faces.size() / 3; i++) {
                VERTEX_ARRAY[index] = faces.get(i).x;
                VERTEX_ARRAY[index+1] = faces.get(i).y;
                VERTEX_ARRAY[index+2] = faces.get(i).z;
                index+=3;
            }
            
            float[] NORMAL_ARRAY = new float[normals.size()*3];
            System.out.println(facesnormal.size());
            for(int i = 0; i < facesnormal.size() / 3; i++) {
                NORMAL_ARRAY[index] = facesnormal.get(i).x;
                NORMAL_ARRAY[index+1] = facesnormal.get(i+1).y;
                NORMAL_ARRAY[index+2] = facesnormal.get(i+2).z;
                index+=3;
            }

Kai

Okay, I see. The only thing that counts is the 'f' elements. Only those tell you something about what actual polygons exist in your model. So, you would not allocate your VERTEX_ARRAY based on the number of vertices found, but on the number of faces.

Another thing: You must parse the faces as they occur and not after you parsed all vertices! This ensures that you also can cope with negative face indices. So, do it this way:

pseudocode:

vs = []
vns = []
vb = [] - this is your vertex buffer
nb = [] - this is your normal buffer
for each line in your obj file:
  if line is 'v' element
    store v in vs
  if line is 'vn' element
    store vn in vns
  if line is 'f' element
    fv0 = value of 1st element of 1st f-subelement
    if (fv0 is negative)
      fv0 += vs.length
    else
      fv0 -= 1
    v0 = vs[fv0]
    v1 and v2 accordingly
    store v0, v1, v2 in vb
    proceed accordingly with normals and store them in nb


Did the code above from scretch, though. That's why its not Java and it might contain syntax errors :-)

Now, you have in your 'vb' all vertices in the right order and in 'nb' all normals and can render them.

Hope, that helps. :-)

Andrew_3ds

EDIT: Nevermind! I figured it out finally! Thank you!

VERTEX_ARRAY = new float[faces.size() * 9];
            int index = 0;
            for(int i = 0; i < faces.size(); i++) {
                VERTEX_ARRAY[index] = vertices.get((int) faces.get(i).x-1).x;
                VERTEX_ARRAY[index+1] = vertices.get((int) faces.get(i).x-1).y;
                VERTEX_ARRAY[index+2] = vertices.get((int) faces.get(i).x-1).z; 
                
                VERTEX_ARRAY[index+3] = vertices.get((int) faces.get(i).y-1).x;
                VERTEX_ARRAY[index+4] = vertices.get((int) faces.get(i).y-1).y;
                VERTEX_ARRAY[index+5] = vertices.get((int) faces.get(i).y-1).z; 
                
                VERTEX_ARRAY[index+6] = vertices.get((int) faces.get(i).z-1).x;
                VERTEX_ARRAY[index+7] = vertices.get((int) faces.get(i).z-1).y;
                VERTEX_ARRAY[index+8] = vertices.get((int) faces.get(i).z-1).z; 
                
                index +=9;
            }


Repeated that for normals, and it worked :)

Kai

I'm glad it works for you.

Though, we should probably rename this thread to "How to load Wavefront OBJ" or something like that and probably move it to "General Java Game Development", to help other people find this when they search for it in the forum, and since it has little to do with the OpenGL API.