Object Loader in Opengl

How do I draw an OBJ file in OpenGL using tinyobjloader?

E: When I wrote this answer originally I had only worked with vertices and normals. I've figured out how to get materials and textures working, but don't have time to write that out at the moment. I will add that in when I have some time, but it's largely the same logic if you wanna poke around the tinyobj header yourselves in the meantime. :-)

I've learned a lot about TinyOBJLoader in the last day so I hope this helps someone in the future. Credit goes to this GitHub repository which uses TinyOBJLoader very clearly and cleanly in fileloader.cpp.

To summarise what I learned studying that code:

Shapes are of type shape_t. For a single model OBJ, the size of shapes is 1. I'm assuming OBJ files can contain multiple objects but I haven't used the file format much to know.

shape_t's have a member mesh of type mesh_t. This member stores the information parsed from the face rows of the OBJ. You can figure out the number of faces your object has by checking the size of the material_ids member.

The vertex, texture coordinate and normal indices of each face are stored in the indices member of the mesh. This is of type std::vector<index_t>. This is a flattened vector of indices. So for a model with triangulated faces f1, f2 ... fi, it stores v1, t1, n1, v2, t2, n2 ... vi, ti, ni. Remember that these indices correspond to the whole vertex, texture coordinate or normal. Personally I triangulated my model by importing into Blender and exporting it with triangulation turned on. TinyOBJ has its own triangulation algorithm you can turn on by setting the reader_config.triangulate flag.

I've only worked with the vertices and normals so far. Here's how I access and store them to be used in OpenGL:

  1. Convert the flat vertices and normal arrays into groups of 3, i.e. 3D vectors
for (size_t vec_start = 0; vec_start < attrib.vertices.size(); vec_start += 3) {
vertices.emplace_back(
attrib.vertices[vec_start],
attrib.vertices[vec_start + 1],
attrib.vertices[vec_start + 2]);
}

for (size_t norm_start = 0; norm_start < attrib.normals.size(); norm_start += 3) {
normals.emplace_back(
attrib.normals[norm_start],
attrib.normals[norm_start + 1],
attrib.normals[norm_start + 2]);
}

This way the index of the vertices and normals containers will correspond with the indices given by the face entries.


  1. Loop over every face, and store the vertex and normal indices in a separate object
for (auto shape = shapes.begin(); shape < shapes.end(); ++shape) {
const std::vector<tinyobj::index_t>& indices = shape->mesh.indices;
const std::vector<int>& material_ids = shape->mesh.material_ids;

for (size_t index = 0; index < material_ids.size(); ++index) {
// offset by 3 because values are grouped as vertex/normal/texture
triangles.push_back(Triangle(
{ indices[3 * index].vertex_index, indices[3 * index + 1].vertex_index, indices[3 * index + 2].vertex_index },
{ indices[3 * index].normal_index, indices[3 * index + 1].normal_index, indices[3 * index + 2].normal_index })
);
}
}

Drawing is then quite easy:

glBegin(GL_TRIANGLES);
for (auto triangle = triangles.begin(); triangle != triangles.end(); ++triangle) {
glNormal3f(normals[triangle->normals[0]].X, normals[triangle->normals[0]].Y, normals[triangle->normals[0]].Z);
glVertex3f(vertices[triangle->vertices[0]].X, vertices[triangle->vertices[0]].Y, vertices[triangle->vertices[0]].Z);

glNormal3f(normals[triangle->normals[1]].X, normals[triangle->normals[1]].Y, normals[triangle->normals[1]].Z);
glVertex3f(vertices[triangle->vertices[1]].X, vertices[triangle->vertices[1]].Y, vertices[triangle->vertices[1]].Z);

glNormal3f(normals[triangle->normals[2]].X, normals[triangle->normals[2]].Y, normals[triangle->normals[2]].Z);
glVertex3f(vertices[triangle->vertices[2]].X, vertices[triangle->vertices[2]].Y, vertices[triangle->vertices[2]].Z);
}
glEnd();

Sample Image

Loading .obj in OpenGL in C

Here is an example of WaveFront OBJ loader in C - http://kixor.net/dev/objloader/

How to load and display .obj file in Android with OpenGL-ES 2

I ended up writing a new parser, it can be used like this to build FloatBuffers to use in your Renderer:

ObjLoader objLoader = new ObjLoader(context, "Mug.obj");

numFaces = objLoader.numFaces;

// Initialize the buffers.
positions = ByteBuffer.allocateDirect(objLoader.positions.length * mBytesPerFloat)
.order(ByteOrder.nativeOrder()).asFloatBuffer();
positions.put(objLoader.positions).position(0);

normals = ByteBuffer.allocateDirect(objLoader.normals.length * mBytesPerFloat)
.order(ByteOrder.nativeOrder()).asFloatBuffer();
normals.put(objLoader.normals).position(0);

textureCoordinates = ByteBuffer.allocateDirect(objLoader.textureCoordinates.length * mBytesPerFloat)
.order(ByteOrder.nativeOrder()).asFloatBuffer();
textureCoordinates.put(objLoader.textureCoordinates).position(0);

and here's the parser:

public final class ObjLoader {

public final int numFaces;

public final float[] normals;
public final float[] textureCoordinates;
public final float[] positions;

public ObjLoader(Context context, String file) {

Vector<Float> vertices = new Vector<>();
Vector<Float> normals = new Vector<>();
Vector<Float> textures = new Vector<>();
Vector<String> faces = new Vector<>();

BufferedReader reader = null;
try {
InputStreamReader in = new InputStreamReader(context.getAssets().open(file));
reader = new BufferedReader(in);

// read file until EOF
String line;
while ((line = reader.readLine()) != null) {
String[] parts = line.split(" ");
switch (parts[0]) {
case "v":
// vertices
vertices.add(Float.valueOf(parts[1]));
vertices.add(Float.valueOf(parts[2]));
vertices.add(Float.valueOf(parts[3]));
break;
case "vt":
// textures
textures.add(Float.valueOf(parts[1]));
textures.add(Float.valueOf(parts[2]));
break;
case "vn":
// normals
normals.add(Float.valueOf(parts[1]));
normals.add(Float.valueOf(parts[2]));
normals.add(Float.valueOf(parts[3]));
break;
case "f":
// faces: vertex/texture/normal
faces.add(parts[1]);
faces.add(parts[2]);
faces.add(parts[3]);
break;
}
}
} catch (IOException e) {
// cannot load or read file
} finally {
if (reader != null) {
try {
reader.close();
} catch (IOException e) {
//log the exception
}
}
}

numFaces = faces.size();
this.normals = new float[numFaces * 3];
textureCoordinates = new float[numFaces * 2];
positions = new float[numFaces * 3];
int positionIndex = 0;
int normalIndex = 0;
int textureIndex = 0;
for (String face : faces) {
String[] parts = face.split("/");

int index = 3 * (Short.valueOf(parts[0]) - 1);
positions[positionIndex++] = vertices.get(index++);
positions[positionIndex++] = vertices.get(index++);
positions[positionIndex++] = vertices.get(index);

index = 2 * (Short.valueOf(parts[1]) - 1);
textureCoordinates[normalIndex++] = textures.get(index++);
// NOTE: Bitmap gets y-inverted
textureCoordinates[normalIndex++] = 1 - textures.get(index);

index = 3 * (Short.valueOf(parts[2]) - 1);
this.normals[textureIndex++] = normals.get(index++);
this.normals[textureIndex++] = normals.get(index++);
this.normals[textureIndex++] = normals.get(index);
}
}
}

obj loader not displaying correctly in c++ OpenGL

I'm baffled as to why but the OBJ format gives vertex attribute indices starting at 1, and not 0. I.e. f 1//2 refers to the first vertex and second normal. Your debug output doesn't show a zero in indices so I'm guessing the loader hasn't accounted for this.

A valid vertex index starts from 1 and matches the corresponding vertex elements of a previously defined vertex list. Each face can contain three or more vertices. [wikipedia]

Hopefully the fix is as easy as: faces[tempSize] = *i - 1;

[EDIT]

Your draw call specifies 14 vertices, but you have 12 triangles so should be drawing 36. amountOfFaces sounds like it should be tempIndices.size()/3.

Your vectors should be references to avoid a copy, and it will be way faster to populate faces etc. with a single memcpy. E.g: memcpy(faces, &tempIndices.front(), sizeof(GLint) * tempIndices.size())

[EDIT]

Oh, right, of course. Now I feel silly for not noticing. Faces indexes the vertices, so they can be reused (saves memory and extra processing). Instead of glDrawArrays you want this:

glDrawElements(GL_TRIANGLES, 36, GL_UNSIGNED_INT, faces); //replate 36 for things you draw that aren't cubes

If you wanted to use glDrawArrays, you would have to rearrange your vertex arrays so each vertex appears in the order given by faces, including all the duplicates. For example your new position array would be [0,0,0, 1,1,0 1,0,0 ...] being vectors 0,6,4 forming the first triangle. std::vectors make this kind of rearranging easy to code.

One last thing. The normals array isn't as big as the position array, but OpenGL doesn't support mixed indexing. So you'll have to duplicate some of the normals so every vertex has both a position and normal.

How to load obj file to PyOpenGL

OpenGL has different current matrices, see glMatrixMode. Each vertex coordinate is transformed by the model view matrix and the projection matrix.

I recommend to set the projection matrix to the current GL_PROJECTION and the view matrix to the current GL_MODELVIEW:

if __name__ == "__main__":
# [...]

glMatrixMode(GL_PROJECTION) # <---- specify projection matrix
gluPerspective(90, (display[0]/display[1]), 0.1, 100)

glMatrixMode(GL_MODELVIEW) # <---- specify model view matrix
glTranslatef(0.0, 0.0, -5)

Anyway you have to remove the model transformation, because the model transformation moves the object out of the Viewing frustum and causes that the model is clipped:

if __name__ == "__main__":
# [...]

while True:
# [...]

glClear(GL_COLOR_BUFFER_BIT|GL_DEPTH_BUFFER_BIT)

# draw model
glPushMatrix()
#glTranslatef(10, 10, 10) <--- DELETE
model.render()
glPopMatrix()

Note, the Wavefront OBJ loader requires a .obj and a .mtl file to load the model correctly.

Custom OpenGL OBJ model loader indices calculation not working

After a long 4 days of testing stuff I found out that I could instead convert the vectors of v3s to vectors of GLfloats in a for loop to add all of them, and add the indices for each i.

std::vector<GLfloat> testv;
std::vector<GLfloat> testu;
std::vector<GLfloat> testn;
for (size_t i = 0; i < m_vertices.size(); i++)
{
testv.push_back(m_vertices[i].x);
testv.push_back(m_vertices[i].y);
testv.push_back(m_vertices[i].z);

testu.push_back(m_uvs[i].x);
testu.push_back(m_uvs[i].y);

testn.push_back(m_normals[i].x);
testn.push_back(m_normals[i].y);
testn.push_back(m_normals[i].z);
testindices.push_back(i);
}

This seems to work on at least 3 triangulated models I used, don't know if it is the best way though.



Related Topics



Leave a reply



Submit