I've written a C++ Obj file loader that I can't get to work correctly. The problem is that while parsing a simple obj file like the following:
# 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
i can't understand the proper way to pass the normals to OpenGL. I always get results like this:
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);
}
How should I reorganize the normals such that they reflect the same order of the vertices?
Your problem is in the data structure. At least while loading an OBJ you need to load your faces into something like:
Then you if you want an array of normals (and probably texturecoords) that matches the vertices, you need to create a new array that matches. The OBJ format is optimized for storage, not rendering.
The added benefit from this two step process, is that you can remove the restriction on homogene faces, by splinting each non triangle into a triangle.
I know that this is an old question, but I had the same problem earlier today while I was building an Obj parser. There were two problems in my code that resulted in a lighting that is very similar to what you've shown: 1- The first was when I incorrectly used the index number(1,2,3,...,n) I got from the Obj file to reference the normal in my array. 2- The other problem was taking the vt values instead of the vn values.
And this is how you pass normal values to OpenGL when using glBegin/glEnd:
You set a normal value before drawing each vertex. Whenever you set a normal, all following vertices will be affected.