如何根据Wavefront(.obj)文件中提供的纹理索引对纹理位置进行排序?

时间:2018-08-06 13:00:15

标签: c++ opengl stdvector wavefront

我目前正在尝试为OpenGL项目制作Wavefront(.obj)文件加载器。我目前使用的方法是逐行进行的,将向量(std :: vectors)中的顶点位置,纹理位置和法线位置分开,并将它们的索引(顶点,纹理和法线索引)存储在三个单独的位置向量(从文件的“ f”行开始,针对每个面)。

我无法根据纹理索引对充满纹理坐标的向量进行排序。我可以在正确的位置渲染顶点,因为我的“加载程序”类需要索引,但我无法弄清楚如何以任何方式对纹理坐标进行排序,因此纹理看起来像是在某些三角形上偏移结果。

具有偏移纹理的多维数据集的图像:

img

纹理(.png)的图像,其在每个面上的显示方式:

img

编辑:这是指向.obj文件和.mtl文件的链接。 Google Drive.

这是我的OBJLoader.cpp文件:

    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);
}

我尝试将texCoords [textureIndices [i]]中的值(vector.insert)插入循环中,但这没有用,并且使输出更糟。我尝试了一个简单的方法:

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

在for循环中,但这也不起作用。

我遍历了整个项目,并确定排序是问题的原因,因为当我插入多维数据集的硬编码值时,它可以完美工作并且纹理根本不会偏移。 (OpenGL命令/图像加载器可以正常工作。)

最终,还有另一种方法可以基于textureIndices对texCoords进行排序吗?

2 个答案:

答案 0 :(得分:3)

如果顶点坐标和纹理坐标的索引不同,则必须“复制”顶点位置。
顶点坐标及其属性(如纹理坐标)形成数据记录。您可以将3D顶点坐标和2D纹理坐标想象为单个5D坐标。
See Rendering meshes with multiple indices

让我们假设您有一个 .obj 文件,如下所示:

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

由此,您必须找到在面部规范中使用的顶点坐标,纹理纹理坐标和法向矢量索引的所有组合:

 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 : ...

然后,您必须创建与索引组合数组相对应的顶点坐标,纹理坐标和法线矢量数组。 顶点坐标及其属性可以在一个数组中组合为数据集,也可以组合为三个具有相等数量属性的数组:

 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 : ...

请参阅非常简单的c ++函数,该函数可以读取链接到的 .obj 文件。 该函数读取文件并将数据写入元素向量和属性向量。

请注意,该功能可以优化并且不关心性能。 对于小文件(例如您喜欢的 cube3.obj ),这并不重要,但是对于大文件, 特别是索引表中的线性搜索,将需要改进。

我只是想给您一个想法,如何读取 .obj 文件以及如何创建元素和属性矢量,可以使用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;
}

答案 1 :(得分:2)

我很想在引擎中实现此功能(为obj文件添加纹理),而您的问题让我很想实际执行此操作:)。

作为纹理提供的图像看起来更像是预览而不是纹理。此外,纹理坐标与预览中所见不符:

3D preview

如果您查看纹理坐标:

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 

只有两个数字:

0.736102
0.263898

这对于纹理中不存在的,轴对齐的四边形或正方形子图像有意义。同样,纹理点的数量没有意义20,应该仅仅是4。因此,您会感到困惑。

无论如何, Rabbid76 是正确的,您需要复制点……相对来说比较容易:

  1. 提取所有位置,颜色,纹理点和法线

    从您的obj文件中

    到单独的表中。因此,分析以v,vt,vn开头的行,并从中创建4个表。是4,因为某些3D扫描仪的输出有时会在v中将颜色编码为v x y z r g b

    所以你应该有这样的东西:

    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. 加工面f

    现在您应该将上述表格作为临时数据处理,并为网格从头开始为新结构创建真实数据(或将其直接加载到VBO)。因此,您需要将所有f数据重新索引为存在的所有索引的唯一组合。为此,您需要跟踪已经获得的信息。为此,我在使用这种结构:

    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); }
        };
    

    因此创建vertex的空列表,现在处理第一条f行并提取索引

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

    因此,针对面部中的每个点(一次只处理一个),提取其ppos,ptxr,pnor索引。现在检查它是否已经存在于最终网格数据中。如果是,请改用其索引。如果未将新点添加到所有表中,则网格具有(pos,col,txr,nor)并使用新添加点的索引。

    处理完一个面的所有点后,将带有重新索引索引的面添加到最终的网格面中,并处理下一行f

可以肯定的是,这是我在引擎中使用的 Wavefront OBJ 加载程序C ++类(但是它取决于引擎本身,因此您不能直接使用它,只是为了查看它的结构)代码以及如何对此进行编码……从头开始可能很困难。

//---------------------------------------------------------------------------
//--- 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
//---------------------------------------------------------------------------

它尚未使用*.mtl文件(我将纹理硬编码用于预览)。

PS。(如果我将其用作纹理:

texture

结果如下:

preview

我在这里使用了很多我自己的东西,所以有一些解释:


str_load_str(s,i,true)返回字符串i中代表索引s中第一个有效单词的字符串。 true表示i已更新为s中的新位置。
str_load_lin(s,i,true)返回表示字符串{{中从索引CR开始的行(直到LFCRLFLFCRi的字符串) 1}}。 true表示s在该行之后被更新为新位置。
i是相同的,但是如果需要的话,它会读取txt_load_...BYTE*的形式,而不是从字符串中读取。

当心CHAR*AnsiString的{​​{1}}和1的索引。

我也使用我的动态列表模板,所以:


BYTE*,CHAR*0相同
List<double> xxx;double xxx[];添加到列表的末尾
xxx.add(5);访问数组元素(安全)
5访问数组元素(不安全但快速的直接访问)
xxx[7]是数组的实际使用大小
xxx.dat[7]清除数组并设置xxx.num
xxx.reset()xxx.num=0个项目预分配空间

这里是来自mtl文件的具有纹理的更新的更快的重新索引代码(其他内容已被忽略,目前仅支持单个对象/纹理):

xxx.allocate(100)

应用添加的材料(现在仅是纹理和材料名称),我更改了重新索引,以便对文本进行索引排序,并使用二进制搜索按需获取顶点索引。 100K faces Standford dragon (3.4MByte)的加载时间为3.7秒:

dragon