5

I'm currently trying to make a Wavefront (.obj) file loader for an OpenGL project. The method I'm currently using goes line-by-line and separates the vertex positions, texture positions and normal positions in vectors (std::vectors) and I'm storing their indices (vertex, texture and normal indices) in three separate vectors (from the 'f' lines of the file, for each face).

I'm having trouble sorting the vector full of texture coordinates based on the texture indices. I'm able to render the vertices in the correct positions because my 'loader' class calls for the indices, but I can't figure out how to sort the texture coordinates in any way, so the textures look offset on some triangles as a result.

Image of cube with offset textures:

img

Image of texture (.png), how it should be on each face:

img

EDIT: Here is a link to both the .obj file and .mtl file. Google Drive.

Here is my OBJLoader.cpp file:

    rawObj.open(filePath); // Open file

    while (!rawObj.eof()) {
        getline(rawObj, line); // Read line

        // Read values from each line 
        // starting with a 'v' for 
        // the vertex positions with
        // a custom function (gets the word in a line
        // at position i)

        if (strWord(line, 1) == "v") {   
            for (int i = 2; i <= 4; i++) {
                std::string temp;
                temp = strWord(line, i);
                vertexStrings.push_back(temp);
            }

        // Same for texture positions

        } else if (strWord(line, 1) == "vt") {     
            for (int i = 2; i <= 3; i++) {
                std::string temp;
                temp = strWord(line, i);
                textureStrings.push_back(temp);
            }

        // Same for normal positions

        } else if (strWord(line, 1) == "vn") {     // normals
            for (int i = 2; i <= 4; i++) {
                std::string temp;
                temp = strWord(line, i);
                normalStrings.push_back(temp);
            }

        // Separate each of the three vertices and then separate 
        // each vertex into its vertex index, texture index and
        // normal index

        } else if (strWord(line, 1) == "f") {      // faces (indices)
            std::string temp;

            for (int i = 2; i <= 4; i++) {
                temp = strWord(line, i);
                chunks.push_back(temp);

                k = std::stoi(strFaces(temp, 1));
                vertexIndices.push_back(k-1);

                l = std::stoi(strFaces(temp, 2));
                textureIndices.push_back(l-1);

                m = std::stoi(strFaces(temp, 3));
                normalIndices.push_back(m-1);

            }

        }
    }

    // Convert from string to float

    for (auto &s : vertexStrings) {
        std::stringstream parser(s);
        float x = 0;

        parser >> x;

        vertices.push_back(x);
    }

    for (auto &s : textureStrings) {
        std::stringstream parser(s);
        float x = 0;

        parser >> x;

        texCoords.push_back(x);
    }

    // Y coords are from top left instead of bottom left
    for (int i = 0; i < texCoords.size(); i++) {
        if (i % 2 != 0)
            texCoords[i] = 1 - texCoords[i];
    }

    // Passes vertex positions, vertex indices and texture coordinates 
    // to loader class
    return loader.loadToVao(vertices, vertexIndices, texCoords);
}

I've tried inserting the values (vector.insert) from texCoords[textureIndices[i]] in a loop but that didn't work and made the output worse. I tried a simple:

tempVec[i] = texCoords[textureIndices[i]] 

in a for loop but that didn't work either.

I've gone through the whole project and I determined that the sorting is the cause of the issue, because when I plug in hard-coded values for the cube it works perfectly and the textures aren't offset at all. (The OpenGL commands / image loader are working as they should.)

Ultimately, is there another way to sort the texCoords based on the textureIndices?

Stefan Ivanovski
  • 117
  • 2
  • 10
  • 1
    If there are different indices for vertex coordinates and texture coordinates, then vertex positions have to be "duplicated". The vertex coordinate and its attributes (like texture coordinate) form a data reocord. You can imagine a 3D vertex coordinate and a 2D texture coordinate as a single 5D coordinate. See [Rendering meshes with multiple indices](https://stackoverflow.com/questions/11148567/rendering-meshes-with-multiple-indices/11148568#11148568). – Rabbid76 Aug 06 '18 at 14:03
  • @Rabbid76 Yes, and that's how they're stored in the Vertex Array Object. The positions (x, y, z) followed by the texture coordinates (x, y). But I'm not exactly sure why the vertex positions need to be duplicated. (For a bit more context, I have made sure that while creating the .obj file, each edge is split) – Stefan Ivanovski Aug 06 '18 at 14:13
  • *"But I'm not exactly sure why the vertex positions need to be duplicated."* Because you have 3 sets of indices `vertexIndices`, `textureIndices` and `normalIndices`. You have to combine them to 1 set of indices, which refers to 3 arrays of equal length, with the vertex coordinates, texture coordinates and normal vectors. – Rabbid76 Aug 06 '18 at 14:58
  • 1
    would be nice to share the model too (obj,mtl,texture)... – Spektre Aug 06 '18 at 15:05
  • 1
    @Spektre Edited with link to .mtl and .obj files. Texture is already in the post. – Stefan Ivanovski Aug 06 '18 at 15:51
  • @Rabbid76 I understand now. Thanks for the info. Although, the problem is specifically that I have the texture coordinates, and the texture indices, with no knowledge of how I would go about generating a new array of texture coordinates based on those indices. – Stefan Ivanovski Aug 06 '18 at 15:53
  • @Rabbid76 Because the texture coordinates need to be passed 'as is', unlike the vertex positions which can be passed in an array buffer and the indices of those vertices can be passed in an index buffer. – Stefan Ivanovski Aug 06 '18 at 16:19
  • @StefanIvanovski No, you are misunderstanding. Nothing can be passed "as is". You have to create completely new arrays of vertex coordinates, texture coordinates and indices. The order of vertex coordinates and texture coordinates from the *.obj* file is useless for drawing with OpenGL directly. It is just to keep the file as short as possible. – Rabbid76 Aug 06 '18 at 17:15

2 Answers2

3

If there are different indexes for vertex coordinates and texture coordinates, then the vertex positions must be "duplicated".
The vertex coordinate and its attributes (like texture coordinate) form a tuple. Each vertex coordinate must have its own texture coordinates and attributes. You can think of a 3D vertex coordinate and a 2D texture coordinate as a single 5D coordinate.
See Rendering meshes with multiple indices.

Let's assume that you have a .obj file like this:

v -1 -1 -1
v  1 -1 -1
v -1  1 -1
v  1  1 -1
v -1 -1  1
v  1 -1  1
v -1  1  1
v  1  1  1 

vt 0 0
vt 0 1
vt 1 0
vt 1 1

vn -1  0  0 
vn  0 -1  0
vn  0  0 -1
vn  1  0  0
vn  0  1  0
vn  0  0  1

f 3/1/1 1/2/1 5/4/1 7/3/1
f 1/1/2 2/2/2 3/4/2 6/3/2
f 3/1/3 4/2/3 2/4/3 1/3/3
f 2/1/4 4/2/4 8/4/4 6/3/4
f 4/1/5 3/2/5 7/4/5 8/3/5
f 5/1/6 6/2/6 8/4/6 7/3/6

From this you have to find all the combinations of vertex coordinate, texture texture coordinate and normal vector indices, which are used in the face specification:

 0 : 3/1/1 
 1 : 1/2/1
 2 : 5/4/1
 3 : 7/3/1
 4 : 1/1/2
 5 : 2/2/2
 6 : 3/4/2
 7 : 6/3/2
 8 : ...

Then you have to create a vertex coordinate, texture coordinate and normal vector array corresponding to the array of index combinations. The vertex coordinates and its attributes can either be combined in one array to data sets, or to three arrays with equal number of attributes:

 index   vx vy vz     u v     nx ny nz
 0 :     -1  1 -1     0 0     -1  0  0
 1 :     -1 -1 -1     0 1     -1  0  0
 2 :     -1 -1  1     1 1     -1  0  0
 3 :     -1  1  1     1 0     -1  0  0
 4 :     -1 -1 -1     0 0      0 -1  0
 5 :      1 -1 -1     0 1      0 -1  0
 6 :     -1  1 -1     1 1      0 -1  0
 7 :      1 -1  1     1 0      0 -1  0
 8 : ...

See the very simple c++ function, which can read an .obj file, like that you linked to. The function reads a file and writes the data to an element vector and an attribute vector.

Note, the function can be optimized and does not care about performance. For a small file (like cube3.obj which you liked to), that doesn't matter, but for a large file, especially the linear search in the index table, will have to be improved.

I just tried to give you an idea how to read an .obj file and how to create an element and attribute vector, which can be directly used to draw an mesh with the use of OpenGL.

#include <vector>
#include <array>
#include <string>
#include <fstream>
#include <strstream>
#include <algorithm>

bool load_obj( 
    const std::string          filename, 
    std::vector<unsigned int> &elements,
    std::vector<float>        &attributes )
{
    std::ifstream obj_stream( filename, std::ios::in );
    if( !obj_stream )
        return false;

    // parse the file, line by line
    static const std::string white_space = " \t\n\r";
    std::string token, indices, index;
    float value;
    std::vector<float> v, vt, vn;
    std::vector<std::array<unsigned int, 3>> f;
    for( std::string line; std::getline( obj_stream, line ); )
    {
        // find first non whispce characterr in line
        size_t start = line.find_first_not_of( white_space );
        if ( start == std::string::npos )
            continue;

        // read the first token
        std::istringstream line_stream( line.substr(start) );
        line_stream.exceptions( 0 );
        line_stream >> token;

        // ignore comment lines
        if ( token[0] == '#' )
            continue;

        // read the line
        if ( token == "v" ) // read vertex coordinate
        {
            while ( line_stream >> value )  
                v.push_back( value );
        }
        else if ( token == "vt" ) // read normal_vectors 
        {
            while ( line_stream >> value )
                vt.push_back( value );
        }
        else if ( token == "vn" )  // read normal_vectors 
        {
            while ( line_stream >> value )
                vn.push_back( value );
        }
        else if ( token == "f" )
        {
            // read faces
            while( line_stream >> indices )
            {
                std::array<unsigned int, 3> f3{ 0, 0, 0 };
                // parse indices
                for ( int j=0; j<3; ++ j )
                {
                    auto slash = indices.find( "/" );
                    f3[j] = std::stoi(indices.substr(0, slash), nullptr, 10);
                    if ( slash == std::string::npos )
                        break;
                    indices.erase(0, slash + 1);
                }
            
                // add index
                auto it = std::find( f.begin(), f.end(), f3 );
                elements.push_back( (unsigned int)(it - f.begin()) );
                if ( it == f.end() )
                    f.push_back( f3 );
            }
        }
    }

    // create array of attributes from the face indices
    for ( auto f3 : f )
    {
        if ( f3[0] > 0 )
        {
            auto iv = (f3[0] - 1) * 3;
            attributes.insert( attributes.end(), v.begin() + iv, v.begin() + iv + 3 );
        }

        if ( f3[1] > 0 )
        {
            auto ivt = (f3[1] - 1) * 2;
            attributes.insert( attributes.end(), vt.begin() + ivt, vt.begin() + ivt + 2 );
        }

        if ( f3[2] > 0 )
        {
            auto ivn = (f3[2] - 1) * 3;
            attributes.insert( attributes.end(), vn.begin() + ivn, vn.begin() + ivn + 3 );
        }
    }

    return true;
}
Rabbid76
  • 142,694
  • 23
  • 71
  • 112
2

I wanted to implement this (adding textures for obj file) into my engine for a long time and your Question got me the mood to actually do it :).

The image you provided as texture looks more like a preview than a texture. Also the texture coordinates does not correspond to it as you can see in preview:

3D preview

If you look at the texture coordinates:

vt 0.736102 0.263898
vt 0.263898 0.736102
vt 0.263898 0.263898
vt 0.736102 0.263898
vt 0.263898 0.736102
vt 0.263898 0.263898
vt 0.736102 0.263898
vt 0.263898 0.736102
vt 0.263898 0.263898
vt 0.736102 0.263898
vt 0.263898 0.736102
vt 0.263898 0.263898
vt 0.736102 0.263898
vt 0.263898 0.736102
vt 0.263898 0.263898
vt 0.736102 0.736102
vt 0.736102 0.736102
vt 0.736102 0.736102
vt 0.736102 0.736102
vt 0.736102 0.736102 

there are just 2 numbers:

0.736102
0.263898

Which makes sense for axis aligned quad or square sub-image in the texture which isnot present in your texture. Also the number of texture points makes no sense 20 it should be just 4. Hence the confusions you got.

Anyway Rabbid76 is right you need to duplicate points ... Its relatively easy so:

  1. extract all positions, colors, texture points and normals

    from your obj file into separate tables. So parse lines starting with v,vt,vn and create 4 tables from it. Yes 4 as color is sometimes encoded in v as v x y z r g b as output from some 3D scanners.

    So you should have something like this:

    double ppos[]= // v
        {
        -1.000000, 1.000000, 1.000000,
        -1.000000,-1.000000,-1.000000,
        -1.000000,-1.000000, 1.000000,
        -1.000000, 1.000000,-1.000000,
         1.000000,-1.000000,-1.000000,
         1.000000, 1.000000,-1.000000,
         1.000000,-1.000000, 1.000000,
         1.000000, 1.000000, 1.000000,
         };
    double pcol[]= // v
        {
        };
    double ptxr[]= // vt
        {
        0.736102,0.263898,
        0.263898,0.736102,
        0.263898,0.263898,
        0.736102,0.263898,
        0.263898,0.736102,
        0.263898,0.263898,
        0.736102,0.263898,
        0.263898,0.736102,
        0.263898,0.263898,
        0.736102,0.263898,
        0.263898,0.736102,
        0.263898,0.263898,
        0.736102,0.263898,
        0.263898,0.736102,
        0.263898,0.263898,
        0.736102,0.736102,
        0.736102,0.736102,
        0.736102,0.736102,
        0.736102,0.736102,
        0.736102,0.736102,
        };
    double pnor[]=  // vn
        {
        -0.5774, 0.5774, 0.5774,
        -0.5774,-0.5774,-0.5774,
        -0.5774,-0.5774, 0.5774,
        -0.5774, 0.5774,-0.5774,
         0.5774,-0.5774,-0.5774,
         0.5774, 0.5774,-0.5774,
         0.5774,-0.5774, 0.5774,
         0.5774, 0.5774, 0.5774,
        };
    
  2. process faces f

    now you should handle the above tables as temp data and create real data for your mesh from scratch into new structure (or load it directly to VBOs). So what you need is to reindex all the f data to unique combinations of all the indexes present. To do that you need to keep track what you already got. For that I amusing this structure:

    class vertex
        {
    public:
        int pos,txr,nor;
        vertex(){}; vertex(vertex& a){ *this=a; }; ~vertex(){}; vertex* operator = (const vertex *a) { *this=*a; return this; }; /*vertex* operator = (const vertex &a) { ...copy... return this; };*/
        int operator == (vertex &a) { return (pos==a.pos)&&(txr==a.txr)&&(nor==a.nor); }
        int operator != (vertex &a) { return (pos!=a.pos)||(txr!=a.txr)||(nor!=a.nor); }
        };
    

    so create empty list of vertex now process first f line and extract indexes

    f 1/1/1 2/2/2 3/3/3
    

    so for each point (process just one at a time) in the face extract its ppos,ptxr,pnor index. Now check if it is already present in your final mesh data. If yes use its index instead. If not add new point to all tables your mesh have (pos,col,txr,nor) and use index of newly added point.

    When all points of a face was processed add the face with the reindexed indexes into your final mesh faces and process next f line.

Just for sure here is my Wavefront OBJ loader C++ class I am using in my engine (but it depends on the engine itself so you can not use it directly it is just to see the structure of code and how to encode this... as starting with this from scratch might be difficult).

//---------------------------------------------------------------------------
//--- Wavefront obj librrary ver: 2.11 --------------------------------------
//---------------------------------------------------------------------------
#ifndef _model_obj_h
#define _model_obj_h
//---------------------------------------------------------------------------
//---------------------------------------------------------------------------
//---------------------------------------------------------------------------
class model_obj
    {
public:
    class vertex
        {
    public:
        int pos,txr,nor;
        vertex(){}; vertex(vertex& a){ *this=a; }; ~vertex(){}; vertex* operator = (const vertex *a) { *this=*a; return this; }; /*vertex* operator = (const vertex &a) { ...copy... return this; };*/
        int operator == (vertex &a) { return (pos==a.pos)&&(txr==a.txr)&&(nor==a.nor); }
        int operator != (vertex &a) { return (pos!=a.pos)||(txr!=a.txr)||(nor!=a.nor); }
        };

    OpenGL_VAO obj;

    model_obj();
    ~model_obj();
    void reset();

    void load(AnsiString name);
    int  save(OpenGL_VAOs &vaos);
    };
//---------------------------------------------------------------------------
//---------------------------------------------------------------------------
//---------------------------------------------------------------------------
model_obj::model_obj()
    {
    reset();
    }
//---------------------------------------------------------------------------
model_obj::~model_obj()
    {
    reset();
    }
//---------------------------------------------------------------------------
void model_obj::reset()
    {
    obj.reset();
    }
//---------------------------------------------------------------------------
void model_obj::load(AnsiString name)
    {
    int   adr,siz,hnd;
    BYTE *dat;

    reset();
    siz=0;
    hnd=FileOpen(name,fmOpenRead);
    if (hnd<0) return;
    siz=FileSeek(hnd,0,2);
        FileSeek(hnd,0,0);
    dat=new BYTE[siz];
    if (dat==NULL) { FileClose(hnd); return; }
    FileRead(hnd,dat,siz);
    FileClose(hnd);

    AnsiString s,s0,t;
    int     a,i,j;
    double  alpha=1.0;
    List<double> f;
    List<int> pos,txr,nor;
    List<double> ppos,pcol,pnor,ptxr;   // OBJ parsed data
    vertex v;
    List<vertex> pv;

    f.allocate(6);

    ppos.num=0;
    pcol.num=0;
    pnor.num=0;
    ptxr.num=0;
    obj.reset();
//                              purpose,    location,                   type,datatype,datacomponents,pack_acc);
    obj.addVBO(_OpenGL_VBO_purpose_pos ,vbo_loc_pos ,        GL_ARRAY_BUFFER,GL_FLOAT,             3,     0.0001);
    obj.addVBO(_OpenGL_VBO_purpose_col ,vbo_loc_col ,        GL_ARRAY_BUFFER,GL_FLOAT,             4,     0.0001);
    obj.addVBO(_OpenGL_VBO_purpose_txr0,vbo_loc_txr0,        GL_ARRAY_BUFFER,GL_FLOAT,             2,     0.0001);
    obj.addVBO(_OpenGL_VBO_purpose_nor ,vbo_loc_nor ,        GL_ARRAY_BUFFER,GL_FLOAT,             3,     0.0001);
    obj.addVBO(_OpenGL_VBO_purpose_fac ,          -1,GL_ELEMENT_ARRAY_BUFFER,  GL_INT,             3,     0.0);
    obj.draw_mode=GL_TRIANGLES;
    obj.rep.reset();
    obj.filename=name;

    _progress_init(siz); int progress_cnt=0;
    for (adr=0;adr<siz;)
        {
        progress_cnt++; if (progress_cnt>=1024) { progress_cnt=0; _progress(adr); }

        s0=txt_load_lin(dat,siz,adr,true);
        a=1; s=str_load_str(s0,a,true);

        // clear temp vector in case of bug in obj file
        f.num=0; for (i=0;i<6;i++) f.dat[i]=0.0;

        if (s=="v")
            {
            f.num=0;
            for (;;)
                {
                s=str_load_str(s0,a,true);
                if ((s=="")||(!str_is_num(s))) break;
                f.add(str2num(s));
                }
            if (f.num>=3)
                {
                ppos.add(f[0]);
                ppos.add(f[1]);
                ppos.add(f[2]);
                }
            if (f.num==6)
                {
                pcol.add(f[3]);
                pcol.add(f[4]);
                pcol.add(f[5]);
                }
            }
        else if (s=="vn")
            {
            f.num=0;
            for (;;)
                {
                s=str_load_str(s0,a,true);
                if ((s=="")||(!str_is_num(s))) break;
                f.add(str2num(s));
                }
            pnor.add(f[0]);
            pnor.add(f[1]);
            pnor.add(f[2]);
            }
        else if (s=="vt")
            {
            f.num=0;
            for (;;)
                {
                s=str_load_str(s0,a,true);
                if ((s=="")||(!str_is_num(s))) break;
                f.add(str2num(s));
                }
            ptxr.add(f[0]);
            ptxr.add(f[1]);
            }
        else if (s=="f")
            {
            pos.num=0;
            txr.num=0;
            nor.num=0;
            for (;;)
                {
                s=str_load_str(s0,a,true); if (s=="") break;
                for (t="",i=1;i<=s.Length();i++) if (s[i]=='/') break; else t+=s[i]; if ((t!="")&&(str_is_num(t))) pos.add(str2int(t)-1);
                for (t="",i++;i<=s.Length();i++) if (s[i]=='/') break; else t+=s[i]; if ((t!="")&&(str_is_num(t))) txr.add(str2int(t)-1);
                for (t="",i++;i<=s.Length();i++) if (s[i]=='/') break; else t+=s[i]; if ((t!="")&&(str_is_num(t))) nor.add(str2int(t)-1);
                }
            // reindex and or duplicate vertexes if needed
            for (i=0;i<pos.num;i++)
                {
                // wanted vertex
                               v.pos=pos[i];
                if (txr.num>0) v.txr=txr[i]; else v.txr=-1;
                if (nor.num>0) v.nor=nor[i]; else v.nor=-1;
                // is present in VBO?
                for (j=0;j<pv.num;j++)
                 if (v==pv[j])
                  { pos[i]=j; j=-1; break; }
                // if not add it
                if (j>=0)
                    {
                    j=v.pos; j=j+j+j;   if (pcol.num>0) obj.addpntcol(ppos[j+0],ppos[j+1],ppos[j+2],pcol[j+0],pcol[j+1],pcol[j+2],alpha);
                                         else           obj.addpnt   (ppos[j+0],ppos[j+1],ppos[j+2]);
                    j=v.nor; j=j+j+j;   if (v.nor>=0)   obj.addnor   (pnor[j+0],pnor[j+1],pnor[j+2]);
                    j=v.txr; j=j+j;     if (v.txr>=0)   obj.addtxr   (ptxr[j+0],ptxr[j+1]);
                    pos[i]=pv.num; pv.add(v);
                    }
                }
            for (i=2;i<pos.num;i++) obj.addface(pos[0],pos[i-1],pos[i]);
            }
        }
    _progress_done();
    delete[] dat;


    }
//---------------------------------------------------------------------------
int model_obj::save(OpenGL_VAOs &vaos)
    {
    int vaoix0=-1;
    OpenGL_VBO *vn=obj.getVBO(_OpenGL_VBO_purpose_nor );
    if (vn->data.num==0) obj.nor_compute();
    vaos.vao=obj;
    vaoix0=vaos.add(obj);
    return vaoix0;
    }
//---------------------------------------------------------------------------
//---------------------------------------------------------------------------
//---------------------------------------------------------------------------
#endif
//---------------------------------------------------------------------------

It does not use the *.mtl file yet (I hardcoded the texture for the preview).

PS. if I use this as texture:

texture

The result looks like this:

preview

I use a lot of mine own stuff here so some explanations:


str_load_str(s,i,true) returns string representing first valid word from index i in string s. The true means just that i is updated with new position in s.
str_load_lin(s,i,true) returns string representing line (till CR or LF or CRLF or LFCR) from index i in string s. The true means just that i is updated with new position after that line.
txt_load_... is the same but instead of reading from string it reads form BYTE* or CHAR* if you want.

Beware AnsiString is indexed form 1 and BYTE*,CHAR* from 0.

I also use mine dynamic list template so:


List<double> xxx; is the same as double xxx[];
xxx.add(5); adds 5 to end of the list
xxx[7] access array element (safe)
xxx.dat[7] access array element (unsafe but fast direct access)
xxx.num is the actual used size of the array
xxx.reset() clears the array and set xxx.num=0
xxx.allocate(100) preallocate space for 100 items

Here the updated faster reindex code with textures from mtl file (otherstuff is ignored and only single object/texture is supported for now):

//---------------------------------------------------------------------------
//--- Wavefront obj librrary ver: 2.11 --------------------------------------
//---------------------------------------------------------------------------
#ifndef _model_obj_h
#define _model_obj_h
//---------------------------------------------------------------------------
class model_obj
    {
public:

    class vertex
        {
    public:
        int pos,txr,nor;
        vertex(){}; vertex(vertex& a){ *this=a; }; ~vertex(){}; vertex* operator = (const vertex *a) { *this=*a; return this; }; /*vertex* operator = (const vertex &a) { ...copy... return this; };*/
        int operator == (vertex &a) { return (pos==a.pos)&&(txr==a.txr)&&(nor==a.nor); }
        int operator != (vertex &a) { return (pos!=a.pos)||(txr!=a.txr)||(nor!=a.nor); }
        int operator <  (vertex &a)
            {
            if (pos>a.pos) return 0;
            if (pos<a.pos) return 1;
            if (txr>a.txr) return 0;
            if (txr<a.txr) return 1;
            if (nor<a.nor) return 1;
            return 0;
            }
        void ld(int p,int t,int n) { pos=p; txr=t; nor=n; }
        };

    class vertexes
        {
    public:
        List<vertex> pv;    // vertexes in order
        List<int> ix;       // inex sort ASC for faster access
        int m;              // power of 2 >= ix.num
        vertexes(){}; vertexes(vertexes& a){ *this=a; }; ~vertexes(){}; vertexes* operator = (const vertexes *a) { *this=*a; return this; }; /*vertexes* operator = (const vertexes &a) { ...copy... return this; };*/
        void reset() { m=0; pv.num=0; ix.num=0; }
        bool get(int &idx,vertex &v)        // find idx so pv[idx]<=v and return if new vertex was added
            {
            int i,j;
            // handle first point
            if (ix.num<=0)
                {
                m=1;
                idx=0;
                pv.add(v);
                ix.add(0);
                return true;
                }
            // bin search closest idx
            for (j=0,i=m;i;i>>=1)
                {
                j|=i;
                if (j>=ix.num) { j^=i; continue; }
                if (v<pv.dat[ix.dat[j]]) j^=i;
                }
            // stop if match found
            idx=ix.dat[j];
            if (v==pv.dat[idx]) return false;
            // add new index,vertex if not
            idx=pv.num; pv.add(v); j++;
            if (j>=ix.num) ix.add(idx);
             else ix.ins(j,idx);
            if (ix.num>=m+m) m<<=1;
            return true;
            }
        };

    struct material
        {
        AnsiString nam,txr;
        material(){}; material(material& a){ *this=a; }; ~material(){}; material* operator = (const material *a) { *this=*a; return this; }; /*material* operator = (const material &a) { ...copy... return this; };*/
        };

    List<material> mat;
    OpenGL_VAO obj;

    model_obj();
    ~model_obj();
    void reset();

    void load(AnsiString name);
    int  save(OpenGL_VAOs &vaos);
    };
//---------------------------------------------------------------------------
//---------------------------------------------------------------------------
//---------------------------------------------------------------------------
model_obj::model_obj()
    {
    reset();
    }
//---------------------------------------------------------------------------
model_obj::~model_obj()
    {
    reset();
    }
//---------------------------------------------------------------------------
void model_obj::reset()
    {
    obj.reset();
    mat.reset();
    }
//---------------------------------------------------------------------------
void model_obj::load(AnsiString name)
    {
    AnsiString path=ExtractFilePath(name);
    int   adr,siz,hnd;
    BYTE *dat;

    reset();
    siz=0;
    hnd=FileOpen(name,fmOpenRead);
    if (hnd<0) return;
    siz=FileSeek(hnd,0,2);
        FileSeek(hnd,0,0);
    dat=new BYTE[siz];
    if (dat==NULL) { FileClose(hnd); return; }
    FileRead(hnd,dat,siz);
    FileClose(hnd);

    AnsiString s,s0,t;
    int     a,i,j;
    double  alpha=1.0;
    List<double> f;
    List<int> pos,txr,nor;
    List<double> ppos,pcol,pnor,ptxr;   // OBJ parsed data
    vertex v;
    vertexes pver;
    material m0,*m=NULL;

    f.allocate(6);
    pver.reset();

    ppos.num=0;
    pcol.num=0;
    pnor.num=0;
    ptxr.num=0;
    obj.reset();
//                              purpose,    location,                   type,datatype,datacomponents,pack_acc);
    obj.addVBO(_OpenGL_VBO_purpose_pos ,vbo_loc_pos ,        GL_ARRAY_BUFFER,GL_FLOAT,             3,     0.0001);
    obj.addVBO(_OpenGL_VBO_purpose_col ,vbo_loc_col ,        GL_ARRAY_BUFFER,GL_FLOAT,             4,     0.0001);
    obj.addVBO(_OpenGL_VBO_purpose_txr0,vbo_loc_txr0,        GL_ARRAY_BUFFER,GL_FLOAT,             2,     0.0001);
    obj.addVBO(_OpenGL_VBO_purpose_nor ,vbo_loc_nor ,        GL_ARRAY_BUFFER,GL_FLOAT,             3,     0.0001);
    obj.addVBO(_OpenGL_VBO_purpose_fac ,          -1,GL_ELEMENT_ARRAY_BUFFER,  GL_INT,             3,     0.0);
    obj.draw_mode=GL_TRIANGLES;
    obj.rep.reset();
    obj.filename=name;

    _progress_init(siz); int progress_cnt=0;
    for (adr=0;adr<siz;)
        {
        progress_cnt++; if (progress_cnt>=1024) { progress_cnt=0; _progress(adr); }

        s0=txt_load_lin(dat,siz,adr,true);
        a=1; s=str_load_str(s0,a,true);

        // clear temp vector in case of bug in obj file
        f.num=0; for (i=0;i<6;i++) f.dat[i]=0.0;

        if (s=="v")
            {
            f.num=0;
            for (;;)
                {
                s=str_load_str(s0,a,true);
                if ((s=="")||(!str_is_num(s))) break;
                f.add(str2num(s));
                }
            if (f.num>=3)
                {
                ppos.add(f[0]);
                ppos.add(f[1]);
                ppos.add(f[2]);
                }
            if (f.num==6)
                {
                pcol.add(f[3]);
                pcol.add(f[4]);
                pcol.add(f[5]);
                }
            }
        else if (s=="vn")
            {
            f.num=0;
            for (;;)
                {
                s=str_load_str(s0,a,true);
                if ((s=="")||(!str_is_num(s))) break;
                f.add(str2num(s));
                }
            pnor.add(f[0]);
            pnor.add(f[1]);
            pnor.add(f[2]);
            }
        else if (s=="vt")
            {
            f.num=0;
            for (;;)
                {
                s=str_load_str(s0,a,true);
                if ((s=="")||(!str_is_num(s))) break;
                f.add(str2num(s));
                }
            ptxr.add(f[0]);
            ptxr.add(f[1]);
            }
        else if (s=="f")
            {
            pos.num=0;
            txr.num=0;
            nor.num=0;
            for (;;)
                {
                s=str_load_str(s0,a,true); if (s=="") break;
                for (t="",i=1;i<=s.Length();i++) if (s[i]=='/') break; else t+=s[i]; if ((t!="")&&(str_is_num(t))) pos.add(str2int(t)-1);
                for (t="",i++;i<=s.Length();i++) if (s[i]=='/') break; else t+=s[i]; if ((t!="")&&(str_is_num(t))) txr.add(str2int(t)-1);
                for (t="",i++;i<=s.Length();i++) if (s[i]=='/') break; else t+=s[i]; if ((t!="")&&(str_is_num(t))) nor.add(str2int(t)-1);
                }
            // reindex and or duplicate vertexes if needed
            for (i=0;i<pos.num;i++)
                {
                // wanted vertex
                               v.pos=pos[i];
                if (txr.num>0) v.txr=txr[i]; else v.txr=-1;
                if (nor.num>0) v.nor=nor[i]; else v.nor=-1;
                if (pver.get(pos[i],v)) // is present in VBO? if not add it
                    {
                    j=v.pos; j=j+j+j;   if (pcol.num>0) obj.addpntcol(ppos[j+0],ppos[j+1],ppos[j+2],pcol[j+0],pcol[j+1],pcol[j+2],alpha);
                                         else           obj.addpnt   (ppos[j+0],ppos[j+1],ppos[j+2]);
                    j=v.nor; j=j+j+j;   if (v.nor>=0)   obj.addnor   (pnor[j+0],pnor[j+1],pnor[j+2]);
                    j=v.txr; j=j+j;     if (v.txr>=0)   obj.addtxr   (ptxr[j+0],ptxr[j+1]);
                    }
                }
            for (i=2;i<pos.num;i++) obj.addface(pos[0],pos[i-1],pos[i]);
            }
        else if (s=="mtllib")
            {
            AnsiString s1;
            int   adr,siz,hnd;
            BYTE *dat;
            // extract mtl filename
            s=str_load_str(s0,a,true);
            s+=str_load_lin(s0,a,true);
            // load it to memory
            siz=0;
            hnd=FileOpen(path+s,fmOpenRead);
            if (hnd<0) continue;
            siz=FileSeek(hnd,0,2);
                FileSeek(hnd,0,0);
            dat=new BYTE[siz];
            if (dat==NULL) { FileClose(hnd); continue; }
            FileRead(hnd,dat,siz);
            FileClose(hnd);
            // extract textures and stuff
            m=&m0;
            for (adr=0;adr<siz;)
                {
                s1=txt_load_lin(dat,siz,adr,true);
                a=1; s=str_load_str(s1,a,true);
                if (s=="newmtl")
                    {
                    s=str_load_str(s1,a,true);
                    s+=str_load_lin(s1,a,true);
                    mat.add();
                    m=&mat[mat.num-1];
                    m->nam=s;
                    m->txr="";
                    }
                else if (s=="map_Kd")
                    {
                    s=str_load_str(s1,a,true);
                    s+=str_load_lin(s1,a,true);
                    m->txr=s;
                    }
                }
            delete[] dat;
            m=NULL;
            }
        else if (s=="usemtl")
            {
            // extract material name
            s=str_load_str(s0,a,true);
            s+=str_load_lin(s0,a,true);
            // find it in table
            for (m=mat.dat,i=0;i<mat.num;i++,m++)
             if (m->nam==s) { i=-1; break; }
            if (i>=0) m=NULL;
            }
        }
    // textures
    for (i=0;i<mat.num;i++)
     if (mat[i].txr!="")
        {
        OpenGL_VAO::_TXR txr;
        txr.ix=-1;
        txr.unit=txr_unit_map;
        txr.filename=mat[i].txr;
        txr.txrtype=GL_TEXTURE_2D;
        txr.repeat=GL_REPEAT;
        obj.txr.add(txr);
        }

    _progress_done();
    delete[] dat;
    }
//---------------------------------------------------------------------------
int model_obj::save(OpenGL_VAOs &vaos)
    {
    int vaoix0=-1,i;
    OpenGL_VBO *vn=obj.getVBO(_OpenGL_VBO_purpose_nor );
    if (vn) if (vn->data.num==0) obj.nor_compute();
    vaos.vao=obj;

    vaoix0=vaos.add(obj);
    return vaoix0;
    }
//---------------------------------------------------------------------------
//---------------------------------------------------------------------------
//---------------------------------------------------------------------------
#endif
//---------------------------------------------------------------------------

Appart the added materials (just texture and material name for now) I changed the re-indexing so the vertextes are index sorted and binary search is used to obtain vertex index on demand. With this 100K faces Standford dragon (3.4MByte) is loaded in 3.7sec:

dragon

Spektre
  • 41,942
  • 8
  • 91
  • 312
  • Thanks a lot! I'll see if I can implement my own version based on the answers so far. It's really appreciated :) EDIT: and yes, the texture is zoomed a bit so the texture coords aren't 1 and 0 for that reason. – Stefan Ivanovski Aug 07 '18 at 10:34
  • 1
    @StefanIvanovski btw I just finished updating my loader with mtl file parser and also had to rewrite the re-indexing `vertex` to index sorted list to boost loading speeds as the naive `O(n^2)` reindex is too slow for huge models If you want I can post updated code but its not as simple as the one I posted and could be much hard to understand (So I struggle if to add it or not ...) – Spektre Aug 07 '18 at 12:31
  • 1
    @StefanIvanovski well I decided to add also the new code (luckily it still fits the SO limit). I also added some description on stuff I am using to help understand what it does... PS the important stuff (reindexing) is happening in `else if (s=="f")` conditioned part of code. – Spektre Aug 07 '18 at 13:02
  • Thank you once again! I'll make sure to review the code again and wrap my head around what's happening :) – Stefan Ivanovski Aug 07 '18 at 14:26
  • 1
    @StefanIvanovski imgur is finaly working again so I could finally add the dragon preview and also found the link where to obtain the 100K dragon obj model so you have the same mesh for testing of speed (however that mesh does not require re-indexing its just for speed comparison) – Spektre Aug 08 '18 at 14:53