creating vbo data from obj files

Started by
7 comments, last by Yours3!f 12 years, 11 months ago
Hi all,

lately I've been struggling with putting the loaded obj data into a vbo and I did succeed.
I wanted to try out the obj loader with a fairly large obj model (1.5Million polys), and it loaded it in 45s, which is quite good because Blender loaded it in approximately the same amount of time, however when I tried to put the vertex data into a vbo, for which I needed to convert the attributes:

I store my vertex data in a mesh object which contains:
std::vector< std::vector<float> > vertices;
std::vector< std::vector<float> > normals;
std::vector< std::vector<unsigned int> > faces;

that is n number of vertices with 3 components, m number of normals with 3 components, and o number of faces with 9 components (3 components per each poly --> (vertex, texture, normal) indices), just like it was arranged in the obj file.

in immediate mode I'd render this data like this:

void render::draw_meshes(const mesh& the_mesh)
{
glBegin(GL_TRIANGLES);
for (int c = 0; c < the_mesh.faces.size(); c++) //for each face render the 3 polys, for each poly render each attribute
{
glNormal3f(the_mesh.normals[the_mesh.faces[c][2] - 1][0],
the_mesh.normals[the_mesh.faces[c][2] - 1][1],
the_mesh.normals[the_mesh.faces[c][2] - 1][2]);
glVertex3f(the_mesh.vertices[the_mesh.faces[c][0] - 1][0],
the_mesh.vertices[the_mesh.faces[c][0] - 1][1],
the_mesh.vertices[the_mesh.faces[c][0] - 1][2]);

glNormal3f(the_mesh.normals[the_mesh.faces[c][5] - 1][0],
the_mesh.normals[the_mesh.faces[c][5] - 1][1],
the_mesh.normals[the_mesh.faces[c][5] - 1][2]);

glVertex3f(the_mesh.vertices[the_mesh.faces[c][3] - 1][0],
the_mesh.vertices[the_mesh.faces[c][3] - 1][1],
the_mesh.vertices[the_mesh.faces[c][3] - 1][2]);

glNormal3f(the_mesh.normals[the_mesh.faces[c][8] - 1][0],
the_mesh.normals[the_mesh.faces[c][8] - 1][1],
the_mesh.normals[the_mesh.faces[c][8] - 1][2]);
glVertex3f(the_mesh.vertices[the_mesh.faces[c][6] - 1][0],
the_mesh.vertices[the_mesh.faces[c][6] - 1][1],
the_mesh.vertices[the_mesh.faces[c][6] - 1][2]);
}
glEnd();
}


and this would be just nice, but the performance would be waaaaaaaaay too low.
So I convert my data like this:


void render::fill_vbos(mesh& the_mesh)
{
typedef GLfloat float_3[3]; //the 3 components

float_3 * tmp_vertices; //3 temporary arrays for data
float_3 * tmp_normals;
GLuint * tmp_faces;

float_3 verts[3]; //three attributes for each poly in a face
float_3 norms[3];

//here we actually dont do it the efficient way like it is done in the obj files (where we have like 700k vertices the same amount of normals and 1500k polys), but we have to convert it so that opengl understands our data
tmp_vertices = new float_3[the_mesh.faces.size() * 3]; //allocate some space for the data (number of faces * 3 = number of vertices)
tmp_normals = new float_3[the_mesh.faces.size() * 3];
tmp_faces = new uint[the_mesh.faces.size() * 3]; //allocate some space for the data (number of faces * 3 = numver of vertices all together to be drawn)

the_mesh.numverts = 0;
the_mesh.numindexes = 0;

for (int c = 0; c < the_mesh.faces.size(); c++) //for each face...
{
verts[0][0] = the_mesh.vertices[the_mesh.faces[c][0] - 1][0]; //...lets store (temporarily) the 3 polys the face contains
verts[0][1] = the_mesh.vertices[the_mesh.faces[c][0] - 1][1];
verts[0][2] = the_mesh.vertices[the_mesh.faces[c][0] - 1][2];

verts[1][0] = the_mesh.vertices[the_mesh.faces[c][3] - 1][0];
verts[1][1] = the_mesh.vertices[the_mesh.faces[c][3] - 1][1];
verts[1][2] = the_mesh.vertices[the_mesh.faces[c][3] - 1][2];

verts[2][0] = the_mesh.vertices[the_mesh.faces[c][6] - 1][0];
verts[2][1] = the_mesh.vertices[the_mesh.faces[c][6] - 1][1];
verts[2][2] = the_mesh.vertices[the_mesh.faces[c][6] - 1][2];

norms[0][0] = the_mesh.normals[the_mesh.faces[c][2] - 1][0];
norms[0][1] = the_mesh.normals[the_mesh.faces[c][2] - 1][1];
norms[0][2] = the_mesh.normals[the_mesh.faces[c][2] - 1][2];

norms[1][0] = the_mesh.normals[the_mesh.faces[c][5] - 1][0];
norms[1][1] = the_mesh.normals[the_mesh.faces[c][5] - 1][1];
norms[1][2] = the_mesh.normals[the_mesh.faces[c][5] - 1][2];

norms[2][0] = the_mesh.normals[the_mesh.faces[c][8] - 1][0];
norms[2][1] = the_mesh.normals[the_mesh.faces[c][8] - 1][1];
norms[2][2] = the_mesh.normals[the_mesh.faces[c][8] - 1][2];

for (int c3 = 0; c3 < 3; c3++) //for each 3 polys...
{
int c4;
for (c4 = 0; c4 < the_mesh.numverts; c4++) //...search through the vertices we stored in the new array so far, and if we find a match then only store its index
{
if (tmp_vertices[c4][0] == verts[c3][0] and
tmp_vertices[c4][1] == verts[c3][1] and
tmp_vertices[c4][2] == verts[c3][2] and

tmp_normals[c4][0] == norms[c3][0] and
tmp_normals[c4][1] == norms[c3][1] and
tmp_normals[c4][2] == norms[c3][2])
{
tmp_faces[the_mesh.numindexes] = c4;
the_mesh.numindexes++;
break;
}
}

if (c4 == the_mesh.numverts)
{
tmp_vertices[the_mesh.numverts][0] = verts[c3][0];
tmp_vertices[the_mesh.numverts][1] = verts[c3][1];
tmp_vertices[the_mesh.numverts][2] = verts[c3][2];

tmp_normals[the_mesh.numverts][0] = norms[c3][0];
tmp_normals[the_mesh.numverts][1] = norms[c3][1];
tmp_normals[the_mesh.numverts][2] = norms[c3][2];

tmp_faces[the_mesh.numindexes] = the_mesh.numverts;
the_mesh.numindexes++;
the_mesh.numverts++;
}
}
}

glGenBuffers(4, the_mesh.vbos); //copy our data into the vbos

// Copy data to video memory
// Vertex data
glBindBuffer(GL_ARRAY_BUFFER, the_mesh.vbos[the_mesh.VERTEX_VBO]);
glBufferData(GL_ARRAY_BUFFER, sizeof(GLfloat)*the_mesh.numverts*3, tmp_vertices, GL_STATIC_DRAW);

// Normal data
glBindBuffer(GL_ARRAY_BUFFER, the_mesh.vbos[the_mesh.NORMAL_VBO]);
glBufferData(GL_ARRAY_BUFFER, sizeof(GLfloat)*the_mesh.numverts*3, tmp_normals, GL_STATIC_DRAW);

// Texture coordinates
//glBindBuffer(GL_ARRAY_BUFFER, the_mesh.vbos[the_mesh.TEXTURE_VBO]);
//glBufferData(GL_ARRAY_BUFFER, sizeof(GLfloat)*the_mesh.numverts*2, tmp_texcoords, GL_STATIC_DRAW);

// Indexes
glBindBuffer(GL_ELEMENT_ARRAY_BUFFER, the_mesh.vbos[the_mesh.FACE_VBO]);
glBufferData(GL_ELEMENT_ARRAY_BUFFER, sizeof(GLuint)*the_mesh.numindexes, tmp_faces, GL_STATIC_DRAW);

delete [] tmp_vertices; //delete the temporary data
delete [] tmp_normals;
delete [] tmp_faces;

tmp_vertices = NULL;
tmp_normals = NULL;
tmp_faces = NULL;
}


(this is actually from opengl superbible)

this conversion makes opengl understand my data, however the loading time triples or worse, sometimes (for great amount of data) it doesn't even load. (that might be because it cant allocate enough space? dont know but my processor still runs at 100%)

so my question is: is there a way to reduce this conversion time, or even better somehow make opengl draw my data properly according to index data (that is without converting anything)?

Best regards,
Yours3!f
Advertisement
No, you can't draw the data by separate indexes. You must perform a conversion like you have done to draw obj formatted data in a vertex buffer.

I don't know the particulars of your algorithm, but I would expect loading a medium OBJ file to take on the order of a few seconds, using a well-known importer like Assimp. 45 seconds might not be unusual for one as large as yours.

If you want the ultimate fastest solution, you should not use OBJ directly in your engine. A good idea may be to parse the OBJ into some kind of machine readable opengl-formatted binary data stream in a separate pre-build step, and then load this data at runtime instead of the obj. You could probably cut down the load times by 95% using such a system.
[size=2]My Projects:
[size=2]Portfolio Map for Android - Free Visual Portfolio Tracker
[size=2]Electron Flux for Android - Free Puzzle/Logic Game
I've been working on exactly the same thing lately and I went at it with a totally different approach. Maybe its me and the fact that its almost 10pm here but your method strikes me as really redundant. Would it be possible to actually see your loading code?

I use the following:

class ObjVertex
{
public:
float x, y, z;
};

class ObjTexCoord
{
public:
float u, v;
};

class ObjNormal
{
public:
float x, y, z;
};

class ModelData
{
public:
std::string MaterialLib;
ObjVertex* VertexData;
ObjTexCoord* TexCoordData;
ObjNormal* NormalData;

int NormalCount;
int TexCoordCount;
int VertexCount;
int FaceCount;

GLint VBOVertexOffset;
GLint VBONormalOffset;
GLint VBOTexCoordOffset;

ModelData();
~ModelData();
};


I'll use this and make 2 objects.

ModelData TempModel, NewModel;

All the vertex, texcoord and normal data gets loaded into TempModel. Then, as we get to the face information we parse all the coordinates from the TempModel into our NewModel in the correct orders. Once that's done we're good to go. Nothing needs to be converted so you can just use your VertexData, TexCoordData and NormalData arrays in your glBufferData() calls. If VBOs are not supported on the given machine the arrays can be used for glVertexArrayData() (I think thats the right command...?).

One thing that can be really costly performance wise is not reserving space in your vectors. .push_back() is really expensive otherwise. Find out exactly how much data you're going to be loading into your vectors and reserve the space before hand.
thanks both of you for the replies!

Would it be possible to actually see your loading code?[/quote]

yeah here it is:
docs.google.com

--> you can use it in the following manner:
mesh lamp;
load_obj lamp_load;
lamp_load.load_obj_file("lamp.obj", lamp);

thats it three lines :)

Then, as we get to the face information we parse all the coordinates from the TempModel into our NewModel in the correct orders[/quote]

could you please show me how this reorganizing is done?

If you want the ultimate fastest solution, you should not use OBJ directly in your engine. A good idea may be to parse the OBJ into some kind of machine readable opengl-formatted binary data stream in a separate pre-build step, and then load this data at runtime instead of the obj. You could probably cut down the load times by 95% using such a system.[/quote]

that came to my mind first when I tried to load a huge model...
When looking at the OBJ file format you'll see that vertex attributes are attached to vertices by using indices. Assuming that the DCC inclusive exporter has worked well (so you don't want to find the minimal mesh yourself) then the association between an attribute index and its attribute value should be sufficient. Hence you need not compare every vertex component-wise, but can compare indices (at the costs of temporarily storing the indices, of course). So instead of


if (tmp_vertices[c4][0] == verts[c3][0] and
tmp_vertices[c4][1] == verts[c3][1] and
tmp_vertices[c4][2] == verts[c3][2] and

tmp_normals[c4][0] == norms[c3][0] and
tmp_normals[c4][1] == norms[c3][1] and
tmp_normals[c4][2] == norms[c3][2]) ...

you'd have something like


if (tmp_posIndices[c4] == newPosIndex and
tmp_normalIndices[c4] == newNormalIndex) ...

what would be at least much more cache friendly. If you have more attributes than those 2, some kind of hashing over the indices is a possibility, too.

thanks both of you for the replies!

could you please show me how this reorganizing is done?



I go through the entire file line by line loading all the coordinate info into the appropriate part of my TempModel object. Once I get to the face information I do the following.

sscanf_s(Buffer.c_str(), "f %d/%d/%d %d/%d/%d %d/%d/%d", &v1, &t1, &n1, &v2, &t2, &n2, &v3, &t3, &n3);

NewModel->VertexData[Index] = TempModel->VertexData[v1-1]; NewModel->VertexCount++;
NewModel->TexCoordData[Index] = TempModel->TexCoordData[t1-1]; NewModel->TexCoordCount++;
NewModel->NormalData[Index] = TempModel->NormalData[n1-1]; NewModel->NormalCount++;

Index++;

NewModel->VertexData[Index] = TempModel->VertexData[v2-1]; NewModel->VertexCount++;
NewModel->TexCoordData[Index] = TempModel->TexCoordData[t2-1]; NewModel->TexCoordCount++;
NewModel->NormalData[Index] = TempModel->NormalData[n2-1]; NewModel->NormalCount++;

Index++;

NewModel->VertexData[Index] = TempModel->VertexData[v3-1]; NewModel->VertexCount++;
NewModel->TexCoordData[Index] = TempModel->TexCoordData[t3-1]; NewModel->TexCoordCount++;
NewModel->NormalData[Index] = TempModel->NormalData[n3-1]; NewModel->NormalCount++;

Index++;


It's not pretty but it works. I'm sure there's a better way to do it but that's what I have so far.
thanks both of you I'm going to try those out, I just don't really have time right now, school you know, end of year...

best regards,
Yours3!f
hi,

I've implemented your solution Ahl, and here it is:

filling up the temporary arrays & copying data to gpu:

void render::fill_vbos(mesh& the_mesh)
{
typedef GLfloat float_3[3];

float_3 * tmp_vertices;
float_3 * tmp_normals;
GLuint * tmp_faces;

float_3 verts[3];
float_3 norms[3];

tmp_vertices = new float_3[the_mesh.faces.size() * 3];
tmp_normals = new float_3[the_mesh.faces.size() * 3];
tmp_faces = new uint[the_mesh.faces.size() * 3];

unsigned int num_index = -1;

for (int c = 0; c < the_mesh.faces.size(); c++)
{
for (int i = 0; i < 3; i++)
{
num_index++;
for(int j = 0; j < 3; j++)
{
tmp_vertices[num_index][j] = the_mesh.vertices[the_mesh.faces[c][i * 3] - 1][j];
tmp_normals[num_index][j] = the_mesh.normals[the_mesh.faces[c][i * 3 + 2] - 1][j];
}
tmp_faces[c * 3 + i] = num_index;
}
}

the_mesh.numindexes = num_index + 1;

glGenBuffers(4, the_mesh.vbos);

// Copy data to video memory
// Vertex data
glBindBuffer(GL_ARRAY_BUFFER, the_mesh.vbos[the_mesh.VERTEX_VBO]);
glBufferData(GL_ARRAY_BUFFER, sizeof(GLfloat)*the_mesh.numindexes*3, tmp_vertices, GL_STATIC_DRAW);

// Normal data
glBindBuffer(GL_ARRAY_BUFFER, the_mesh.vbos[the_mesh.NORMAL_VBO]);
glBufferData(GL_ARRAY_BUFFER, sizeof(GLfloat)*the_mesh.numindexes*3, tmp_normals, GL_STATIC_DRAW);

// Indexes
glBindBuffer(GL_ELEMENT_ARRAY_BUFFER, the_mesh.vbos[the_mesh.FACE_VBO]);
glBufferData(GL_ELEMENT_ARRAY_BUFFER, sizeof(GLuint)*the_mesh.numindexes, tmp_faces, GL_STATIC_DRAW);

delete [] tmp_vertices;
delete [] tmp_normals;
delete [] tmp_faces;

tmp_vertices = NULL;
tmp_normals = NULL;
tmp_faces = NULL;
}


drawing the data:

void render::draw_meshes_vbo(const mesh& the_mesh)
{
glEnableClientState(GL_VERTEX_ARRAY);
glEnableClientState(GL_NORMAL_ARRAY);

// Here's where the data is now
glBindBuffer(GL_ARRAY_BUFFER, the_mesh.vbos[the_mesh.VERTEX_VBO]);
glVertexPointer(3, GL_FLOAT,0, 0);

// Normal data
glBindBuffer(GL_ARRAY_BUFFER, the_mesh.vbos[the_mesh.NORMAL_VBO]);
glNormalPointer(GL_FLOAT, 0, 0);

// Indexes
glBindBuffer(GL_ELEMENT_ARRAY_BUFFER, the_mesh.vbos[the_mesh.FACE_VBO]);
glDrawElements(GL_TRIANGLES, the_mesh.numindexes, GL_UNSIGNED_INT, 0);

glDisableClientState(GL_VERTEX_ARRAY);
glDisableClientState(GL_NORMAL_ARRAY);
}


so far so good: 20ms improvement on a small mesh...

EDIT: for the 1.5M mesh I get a nice 48 seconds loading time which is only 3 seconds worse than immediate mode, and now the framerate is bw 25 and 30, and not bw 3-5

I've got a question:
Since the vertices are in order there is no need for indices, so how do I draw my data without the indices?

best regards,
Yours3!f
Eventually I found out that glDrawArrays() do exactly what I need (drawing without indices), so now I use them, and I added some VAO tricks to the whole rendering, and it is now extremely convenient to use.

This topic is closed to new replies.

Advertisement