Advertisement

Importing .OBJ model to OpenGL

Started by September 26, 2017 11:35 AM
10 comments, last by alx119 7 years, 1 month ago

Hello everyone,

I would like to import .OBJ models to OpenGL. I tried to follow a tutorial using assimp, but I didn't enjoyed this library so much. :D
Is it possible to import models with textures, materials etc, without using libraries like assimp ? I am asking this question because I decided to write it by myself.
Also if this is possible, what classes should I need ? I was thinking a class Mesh(containing, vertices, textures, materials), a class  Model that contains multiple meshes. Do I need something else ? 

If anyone could help me through this, I would be very grateful. :P
Thank you very much!
 

You need a class that has VertexArrayObject(s), and VertexBufferObject(s), and the textures, and materials. You can still store multiple Mesh classes inside of a Model class. There is no real need to store the vertices after you've given their data to the VBOs. So just store them temporarily in a LoadModel function, then delete the vertex data after the VAO, and VBO are created and filled out.

Personally, I'd use something other than .obj because it doesn't contain all the needed data(armature and bones), and I don't know if its just me, or the way Blender exported it, but my model's indices were completely messed up. Collada .dae exported everything needed, and the indices were in the right order, so I used that to get the data and store it in a smaller custom file type. If you use Collada, then tinyXML can make parsing it easier.

Either way you're going to need to write a parser for the file type. Look up the specifications, .obj isn't hard to learn, collada .dae is more complicated.

For loading images to use as textures, you can use SOIL(Simple OpenGL Image Library), it makes it really easy, and supports common image types used for textures.

Also, obj files don't contain the materials themselves, but they're typically exported with a .mtl file that contains the actual material data.

Advertisement

Hi,

Thank you very much for your advice. I wanted to ask in the topic about the parsing as well. How can I make the parsing more easy? Do I need another programming language like...LISP ? Or I can just use c++ ? What do you suggest ? :P
I will give it a try, first with .obj and if will not work, I will try with Collada. 
And one last question to see if I understood correctly :P. Let's just say that I have the vertices with the VBO:

float vertices [] = {-0.5f, -0.5f, 0.0f,
                                  0.5f, -0.5f, 0.0f,
                                0.0f, 0.5f, 0.0f };
unsigned int VBO; 
glGenBuffers(1, &VBO);
glBindBuffer(GL_ARRAY_BUFFER, VBO);
glBufferData(GL_ARRAY_BUFFER, sizeof(vertices), vertices, GL_STATIC_DRAW).

After this code, the vertices array can be deleted and they are kept in the VBO ? :P 
Thank you very much and I'm looking forward for your help! :P 
 

So today you can read .obj, and if you have to support another format? Apart from being a lesson in how to do this on your own, why would you want this responsibility.  If its education then by all means I recommend doing it, other than that find the right tool for the job. What exactly was the issue with Assimp? It could have simply being the tutorial you used and not Assimp itself. You should be able to just grab the Assimp documentation and use the library to do whatever you need which is just the basic loading. I've been using Assimp to load models for a few years now with no problem. Also be aware that Assimp does NOT load textures you will also need another library for that or roll your own. As for organization, offering suggestion without use case makes it hard to offer help. It would definitely help to abstract a mesh and all its contents. But again,  use case would dictate organization.

You can use c++. There are a few ways of doing it. An easy way is to use std::ifstream to open the file, then while not end of file get each line, and do whatever you need to based off of the first character of the current line for the .obj file.

No, you didn't really tell it what the data is, it has the data, but it doesn't know how to use it.

It is stored with the VAO, not the VBO. The VBO just acts a a buffer to pass data to the VAO, once its there it stays on the graphics card until glDeleteVertexArrays(1, &VAO); is called, or until VAO is released by the containing class(Model/Mesh) being destroyed. I've tested this, the VBO can go out of scope and the model will still render, but VAO cannot.


float vertices [] = {
  -0.5f, -0.5f, 0.0f,
  0.5f, -0.5f, 0.0f,
  0.0f, 0.5f, 0.0f };
float textureCoords [] = {
  0.0f, 0.0f,
  0.0f, 1.0f,
  1.0f, 0.0f,};
float normals [] = {
  0.0f, 0.0f, 1.0f,
  0.0f, 0.0f, 1.0f,
  0.0f, 0.0f, 1.0f };

unsigned int VAO;
unsigned int VBO_verts;
unsigned int VBO_textureCoords;
unsigned int VBO_normals;

glGenVertexArrays(1, &VAO); 
// This generates the VertexAttributeObject
glGenBuffers(1, &VBO);
glBindBuffer(GL_ARRAY_BUFFER, VBO_verts);
glBufferData(GL_ARRAY_BUFFER, sizeof(vertices), vertices, GL_STATIC_DRAW); 
// This fills the buffer with data, but doesn't tell it what it is

glBindVertexArray(VAO); // Set current VAO to use
glEnableVertexAttribArray(0);	// Enable whatever the first element is, typically vertex positions
// The order of the elements MUST match the shader's order, otherwise it'll use the wrong pieces of data from the VAO/VBO

// This line tells it how to split the data into something useful.
glVertexAttribPointer(0, // index to use, the same number we just enabled for vertex positions
                      3, // the size of the element
                      GL_FLOAT, // GL_FLOAT tells it the type(GLfloat)
                      GL_FALSE, // Is it already normalized, honestly I don't know what that means with vertex data so I leave it false
                      0, // the stride to the beginning of the next vertex element(textureCoods, which aren't in this array)
                      (GLvoid*)(NULL + (0 * sizeof(GLfloat))); // Position to start from

glBindBuffer(GL_ARRAY_BUFFER, VBO_textureCoords);
glBufferData(GL_ARRAY_BUFFER, sizeof(textureCoords), textureCoords, GL_STATIC_DRAW); 

glEnableVertexAttribArray(1);	// Enable whatever the second element is, normal/texture coords/color/whatever

glVertexAttribPointer(1, // Second element, were going to use texture coords
                      2, // the size of the element, only 2 floats this time
                      GL_FLOAT, GL_FALSE, 0, (GLvoid*)0);

glBindBuffer(GL_ARRAY_BUFFER, VBO_normals);
glBufferData(GL_ARRAY_BUFFER, sizeof(normals), normals, GL_STATIC_DRAW); 

glEnableVertexAttribArray(2);	// Enable whatever the third element is, normal/texture coords/color/whatever

glVertexAttribPointer(2, // Third element, were going to use normals
                      3, GL_FLOAT, GL_FALSE, 0, (GLvoid*)0);

glBindVertexArray(0); // Set current VAO to nothing first, so that the VBOs won't accidently get released or over written
glBindBuffer(GL_ARRAY_BUFFER, 0);

You can also use an interleaved data, hopefully I didn't make any mistakes with the above or below code:


float vertexData_Interleaved[] = {
	-0.5f, -0.5f, 0.0f, 0.0f, 0.0f, 0.0f, 0.0f, 1.0f,
	0.5f, -0.5f, 0.0f, 0.0f, 1.0f, 0.0f, 0.0f, 1.0f,
	0.0f, 0.5f, 0.0f, 1.0f, 0.0f, 0.0f, 0.0f, 1.0f };

unsigned int VAO;
unsigned int VBO;

glGenVertexArrays(1, &VAO);

glGenBuffers(1, &VBO);
glBindBuffer(GL_ARRAY_BUFFER, VBO);
glBufferData(GL_ARRAY_BUFFER, sizeof(vertices), vertices, GL_STATIC_DRAW);

glBindVertexArray(VAO);
glEnableVertexAttribArray(0);
glEnableVertexAttribArray(1);
glEnableVertexAttribArray(2);

glVertexAttribPointer(0, 3, GL_FLOAT, GL_FALSE, 0, (GLvoid*)0);
glVertexAttribPointer(1, 2, GL_FLOAT, GL_FALSE,
	8 * sizeof(GLfloat), // Vertex position size(3 floats) + texture coord size(2 floats) + normals size(3 floats) 
	(GLvoid*)(NULL + (3 * sizeof(GLfloat))); // Position to start from

glVertexAttribPointer(2, 3, GL_FLOAT, GL_FALSE,
	8 * sizeof(GLfloat), // Vertex position size(3 floats) + texture coord size(2 floats) + normals size(3 floats)
	(GLvoid*)(NULL + (5 * sizeof(GLfloat))); // Position to start from

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

After this you can delete the vertex data array(s).

@cgrant: I tried to follow once, the tutorials with assimp from learnopengl.com and I encountered an error that I couldn't solve it.
Also I didn't understand so much what was with the root nodes and all the things referring to assimp, so I told myself that it would be challenging to create my own model importer. :P
Also, I will try first with .obj files, just for learning purpose and maybe in the future, I will try with other formats. :P

@YxjmirThank you for all the informations. I've worked before with VAO's and VBO's, and was slightly ok, but I think I understood more what they do, and not how they do. :P
 

Advertisement

I've been looking at different file types to store things more efficiently, and I came across glft, and glb(binary glft file) which is just a specification for a file that can be loaded directly into opengl buffers using a JSON file to determine which chunks of data are which, and how big the chunk is, etc.. This seems like it would be an efficient format, and you could even use assimp to load the file and then transfer the data into the glft file format.

Here's a link for it: https://github.com/KhronosGroup/glTF/blob/master/specification/2.0/README.md#motivation

Also, as a side note, I wanted to clarify that when I said the VBO can go out of scope and it still render as long as the VAO is still in scope, this may not be a good practice to follow, I just meant you need to have the VAO stored in the class otherwise it won't know how to use the data (if it even still has it). This was a problem I had when I first started using opengl, and it took me a while to figure out what was wrong.

Hi,

I came back to this topic because I didn't write an model importer yet, because I had something else to do. :P

So today I will start to write it, but first I have a few questions. :P
I was thinking to make a vector for vertices, one for textures, and one for normals and after to interleave the data in a single vector. My object has around ~290 vertices, ~596 vt, and ~570 vn. So one of the question is:

1.How can I store all of this in vectors ? or in a single vector if I want to use interleaved data ? How much space can a vector support ?

Another question. Let's say I have some faces:
f 1/3/1 2/4/2 13/23/3 12/24/4
f 1/3/6 3/34/5 14/35/6 13/23/3

2.So I have 2 faces, both of them have the first vertex 1, with the texture coordonate no. 3, but have different normals, 1 and 6.
So how can I write this in my interleaved data ?

3. If my spaceship model uses 2 textures, how can I know which texture to use in shaders ? How can I find the coordonates for each texture ?

So for the moment, these are my questions. :P 
Hope somebody can help me to clarify the parsing of the .obj file.

Thank you! 

 

46 minutes ago, alx119 said:

I was thinking to make a vector for vertices, one for textures, and one for normals and after to interleave the data in a single vector. My object has around ~290 vertices, ~596 vt, and ~570 vn.

The primary issue is that OBJ files allows you to share the individual attributes; for example, the first vertex of the two faces you showed is 1/3/1 and 1/3/6, so the two faces share position and texture coordinate, which also happens to be different indices (first vertex and third texture coordinate), but use different normal (first vs. sixth). But, OpenGL does not allow that; you can only have one single index to select all attributes of a vertex.

You have to rearrange the indices to make sure that all triplets uses the same indices; for example 1/1/1, 2/2/2 and 3/3/3 to make a triangle of the first, second and third vertex. You cannot have mixed indices within a triplet. The important thing to keep in mind that a vertex in OpenGL is a collection of all its attributes. The vertex is not just it's position. Basically, this is your vertex (vec2 and vec3 represent your favourite 2- and 3-dimensional vectors):


struct Vertex {
    vec3 p;   // position
    vec2 t;   // texture coordinate
    vec3 n;   // normal
};

If two vertices have different normals, then the two vertices are entirely different. You then need two different instances of this structure with the set of positions and normals duplicated.

You need to translate your OBJ file's split index structure into OpenGL's shared index structure. If you are learning this and don't feel comfortable doing that, then I suggest you forget about indices and just use a flat non-indexed vertex array instead.

Assuming that you have individual lists of each position, texture coordinate and normal attribute from the OBJ file, as well as a list of faces and corresponding sets of indices, you can flatten the arrays by simply iterating over the faces and build a new vertex array from scratch.


struct Index {
	int position_index;
	int texture_index;
	int normal_index;
}

struct Face {
	Index indices[3];
}

std::vector<Face> faces;
std::vector<vec3> position;
std::vector<vec2> texture;
std::vector<vec3> normal;
std::vector<Vertex> object;

for(auto &&f : faces) {
    for(auto &&i : f.indices) {
        Vertex v;
        v.p = position[i.position_index];
        v.t = texture[i.texture_index];
        v.n = normal[i.normal_index];
        object.push_back(v);
    }
}

The vector object now contains the list of vertices forming triangles as specified by the OBJ-file. You can pass this vector, or it's data rather, directly to OpenGL as an interleaved array. No index arrays needed. It is assumed that you have populated position, texture, normal and faces from the OBJ file already.

7 hours ago, alx119 said:

1.How can I store all of this in vectors ? or in a single vector if I want to use interleaved data ? How much space can a vector support ?

Just use std::vector<float> and push_back to put the vertex, uv, then normal, and use my example of interleaved VBO. You shouldn't have to worry about the amount of space a vector can support, I don't remember what the number is but it's pretty high.

 

7 hours ago, alx119 said:

3. If my spaceship model uses 2 textures, how can I know which texture to use in shaders ? How can I find the coordonates for each texture ?

You would use both, the uv coordinates will range from 0.0 - 1.0 regardless of the size of the texture. (sometimes it will be out of this range, but it will just repeat starting at the opposite end, or be clipped depending on the texture settings)


// You could check to make sure that the glGetUniformLocation() returns a valid location, and 
// display an error, or write it to file or something.
glUniform1i(glGetUniformLocation("texture_0"), 0);
glUniform1i(glGetUniformLocation("texture_1"), 1);

glActiveTexture(GL_TEXTURE0);
glBindTexture(GL_TEXTURE_2D, texture0_id);

glActiveTexture(GL_TEXTURE1);
glBindTexture(GL_TEXTURE_2D, texture1_id);

your shader should have texture_0, and texture_1 as two uniform sampler2D, or as a array but then you'd have to modify my example a little bit.

This topic is closed to new replies.

Advertisement