理解Wavefront Obj中的法线索引

5
我已经编写了一个C++ Obj文件加载器,但无法正确运行。问题在于解析以下简单的obj文件时:
# Blender v2.62 (sub 0) OBJ File: ''
# www.blender.org
mtllib cube.mtl
o Cube
v 1.000000 -1.000000 -1.000000
v 1.000000 -1.000000 1.000000
v -1.000000 -1.000000 1.000000
v -1.000000 -1.000000 -1.000000
v 1.000000 1.000000 -0.999999
v 0.999999 1.000000 1.000001
v -1.000000 1.000000 1.000000
v -1.000000 1.000000 -1.000000
vn 0.000000 0.000000 -1.000000
vn -1.000000 -0.000000 -0.000000
vn -0.000000 -0.000000 1.000000
vn 1.000000 -0.000000 0.000000
vn 1.000000 0.000000 0.000001
vn -0.000000 1.000000 0.000000
vn 0.000000 -1.000000 0.000000
usemtl Material
s off
f 5//1 1//1 4//1
f 5//1 4//1 8//1
f 3//2 7//2 8//2
f 3//2 8//2 4//2
f 2//3 6//3 3//3
f 6//3 7//3 3//3
f 1//4 5//4 2//4
f 5//5 6//5 2//5
f 5//6 8//6 6//6
f 8//6 7//6 6//6
f 1//7 2//7 3//7
f 1//7 3//7 4//7

我无法理解正确的将法线传递给OpenGL的方法。我总是得到像这样的结果:enter image description here

ObjLoader.h

#include <Eigen/Core>

class ObjLoader
{
public:
    ObjLoader();
    bool load(const std::string &filename);

    void draw();
private:
    bool loadFace(const std::string &line,int lineNumber);


    std::vector< Eigen::Vector3d> verticesCoord,verticesNormals;
    std::vector< Eigen::Vector2d> textureCoords;
    std::vector<GLuint> vertexIndices,normalIndices,textureIndices;
    Eigen::Vector3d calculateNormal( const Eigen::Vector3d &coord1, const Eigen::Vector3d &coord2, const Eigen::Vector3d &coord3 );
    std::string mtlFile;
    unsigned int nVerticesPerFace;

};

ObjLoader.cpp

#include <iostream>
#include <fstream>
#include <string>
#include <sstream>
#include <Eigen/Core>
#include "ObjLoader.h"

using namespace std;
using namespace Eigen;

ObjLoader::ObjLoader()
{

}

bool ObjLoader::load(const string &filename)
{
    ifstream is(filename.c_str());
    if (is.is_open())
    {
    cerr << "File " + filename + " loaded successfully" << endl;
    }

    std::vector<Vector3d> temporaryNormals; // a vector to contain the normals as they are read from the obj

    string line;
    unsigned int lineNumber=0;
    while ( getline(is,line) )
    {
    lineNumber++;
    if ( line.empty() || line.at(0)=='#' )
        continue;
    if ( line.substr(0,6)=="mtllib")
    {
        this->mtlFile = line.substr(7,line.size()-1);
        cerr <<  "MTLIB support file= " << mtlFile << endl;
    }
    stringstream stream(line);
    char identifier ;
    stream >> std::skipws >> identifier;

    char specifier;
    stream >> specifier;
    if (specifier != 't' && specifier != 'n' && specifier!='p' )
    {
        stream.seekg(0);
        specifier=0;
    }

    switch ( identifier )
    {
    case 'v': //is a vertex specification
    {
        switch ( specifier ) // if there is a space then is a simple vertex coordinates
        {
        case 0:
        {
            char tmp; stream >> tmp;
            Eigen::Vector3d vertex(0.0,0.0,0.0);
            stream >>  vertex[0] >> vertex[1] >> vertex[2];
            this->verticesCoord.push_back(vertex);
        }
            break;
        case 't':
        {
            Eigen::Vector2d textures(0,0);
            stream >> textures[0] >> textures[1];
            this->textureCoords.push_back(textures);
        }
            break;
        case 'n':
        {
            Eigen::Vector3d vertexNormal(0,0,0);
            stream >>  vertexNormal[0] >> vertexNormal[1] >> vertexNormal[2];
            temporaryNormals.push_back(vertexNormal);
        }
            break;
        }
    }
        break;
    case 'f': // is a face specification
    {
        this->loadFace(line,lineNumber);
    }
        break;
    }
    }

    // Rearrange the normals
    verticesNormals.resize(temporaryNormals.size(),Vector3d(1,1,1));
    for(unsigned int i=0;i<vertexIndices.size();i++)
    {
    GLuint nI = normalIndices.at(i);
    GLuint vI = vertexIndices.at(i);
    if(nI!=vI)
    {
        this->verticesNormals.at(vI) = temporaryNormals.at(nI);
        std::cerr<< "Normal index doesn't match vertex index: " << vertexIndices[i] << " " << normalIndices[i] << std::endl;
    }
    else
    {
        this->verticesNormals.at(vI) = temporaryNormals.at(vI);
    }

    cerr << "Vertices=" << this->verticesCoord.size() << endl;
    cerr << "Normals=" << this->verticesNormals.size() << endl;
    cerr  << "Textures=" << this->textureCoords.size() << endl;
    cerr << "NVertices per face= " << this->nVerticesPerFace << endl;
    cerr << "Faces= " << this->vertexIndices.size()/nVerticesPerFace << endl;

    return 0;
    }
}

bool BothAreSpaces(char lhs, char rhs)
{
    return (lhs == rhs) && (lhs == ' ');
}

bool ObjLoader::loadFace(const string &_line, int lineNumber)
{
    std::string line = _line;
    std::string::iterator new_end = std::unique(line.begin(), line.end(), BothAreSpaces);
    line.erase(new_end, line.end());

    stringstream stream(line),countVerticesStream(line);
    string val;
    stream >> val;
    if (val!="f")
    {
    string lineString= static_cast<ostringstream*>( &(ostringstream() << lineNumber) )->str();
    throw std::logic_error("Error loading face at line " + lineString);
    }

    // Count the number of vertices per face by counting the /
    int nVertices = 0;
    while ( countVerticesStream.good() )
    {
    if (countVerticesStream.get()==' ' && countVerticesStream.good())
        nVertices++;
    }

    if ( nVerticesPerFace !=0 && nVerticesPerFace != nVertices )
    {
    string lineString= static_cast<ostringstream*>( &(ostringstream() << lineNumber) )->str();
    throw std::logic_error("Can't support non uniform faces definitions. You must use the same number of vertices for every faces. Check line "+lineString);
    }
    this->nVerticesPerFace = nVertices;

    GLuint indexPosition = 0, indexTexture = 0, indexNormal = 0;

    // Compute the normal
    Vector3d faceVertices[nVerticesPerFace];
    for ( unsigned int iFace = 0; iFace < nVerticesPerFace; iFace++ )
    {
    stream >> indexPosition;
    faceVertices[iFace] = verticesCoord.at(indexPosition-1);
    if( '/' == stream.peek() )
    {
        stream.ignore();
        if( '/' != stream.peek() )
        {
            stream >> indexTexture;
        }
        if( '/' == stream.peek() )
        {
            stream.ignore();
            // Optional vertex normal
            stream >> indexNormal;
        }
    }
    this->vertexIndices.push_back(indexPosition-1); // that's because Obj format starts counting from 1
    this->textureIndices.push_back(indexTexture-1); // that's because Obj format starts counting from 1
    this->normalIndices.push_back(indexNormal-1); // that's because Obj format starts counting from 1

    }
}

void ObjLoader::draw()
{
    double *pVerticesCoords = &this->verticesCoord.at(0)[0];
    glEnableClientState(GL_VERTEX_ARRAY);
    glVertexPointer(3,GL_DOUBLE, 0,pVerticesCoords);
    glDrawArrays(GL_POINTS, 0, this->verticesCoord.size());
    glDisableClientState(GL_VERTEX_ARRAY);

    GLint coordsPerVertex=3;
    GLint stride=0; //  Our coords are tightly packed into their arrays so we set this to 0
    //double *pVerticesCoords = &this->verticesCoord.at(0)[0];
    double *pNormalCoords = &this->verticesNormals.at(0)[0];

    glEnableClientState(GL_NORMAL_ARRAY);
    glEnableClientState(GL_VERTEX_ARRAY);

    glNormalPointer(GL_DOUBLE, 0, pNormalCoords); // Normal pointer to normal array
    glVertexPointer(coordsPerVertex,GL_DOUBLE, stride,pVerticesCoords);

    switch ( nVerticesPerFace )
    {
    case 3:
    glDrawElements(GL_TRIANGLES, vertexIndices.size(), GL_UNSIGNED_INT, this->vertexIndices.data());
    break;
    case 4:
    glDrawElements(GL_QUADS, vertexIndices.size(), GL_UNSIGNED_INT, this->vertexIndices.data());
    break;
    default:
    glDrawElements(GL_POLYGON, vertexIndices.size(), GL_UNSIGNED_INT, this->vertexIndices.data());
    }
    glDisableClientState(GL_VERTEX_ARRAY);
    glDisableClientState(GL_NORMAL_ARRAY);

}

我应该如何重新组织法线,以使其反映顶点的相同顺序?

展示你的问题真的需要那么多代码吗?乍一看,我认为不需要。你能否请缩小你提供的代码范围,这样我们就可以更好地理解你的问题,从而给出更好的答案。 - Mark Garcia
我提供了这段代码,以便为您提供一个可编译的示例。问题(我认为)是在加载文件并将法线按照遇到的顺序推入std::vector<Vector3d> normals时。我想提供具有相同索引的法线和顶点,但不知道如何实现。 - linello
2个回答

4

你的问题在于数据结构。至少在加载OBJ时,你需要将面部信息加载到类似以下的内容中:

struct Vertex
{
    unsigned int vertex;
    unsigned int normal;
    unsigned int texturecoord;
};

struct Face
{
    // not dynamic, but you get the idea.
    Vertex vertexes[N];
};

如果你想要一个与顶点匹配的法线数组(可能还有纹理坐标),那么你需要创建一个相匹配的新数组。OBJ格式的优化是为了存储而不是渲染。

这个两步操作的附加好处是,通过将每个非三角形分裂成三角形,可以消除同质面的限制。


谢谢,但是如何使用“顶点”数据结构来绘制坐标、法线和纹理呢?我的意思是,索引不会混乱吗? - linello
你需要计算一组新的数组,以匹配所需的VBO结构。加载步骤应该完全解耦渲染步骤。未来可能需要加载其他格式,具有其他特点,请勿使渲染依赖于加载代码。 - rioki

-1

我知道这是一个老问题,但今天在构建Obj解析器时我遇到了与你所展示的非常相似的照明问题:

1- 当我错误地使用从Obj文件获取的索引号(1、2、3、...、n)来引用我的数组中的法线时,就会出现第一个问题。

2- 另一个问题是使用vt值而不是vn值。

以下是在使用glBegin/glEnd时如何将法线值传递给OpenGL:

glNormal3f(....);
glVertex3f(....);

在绘制每个顶点之前,您需要设置一个普通值。每当您设置一个普通值时,所有后续的顶点都会受到影响。


立即模式?滚出去 - RecursiveExceptionException

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接