# parsing obj files

## Recommended Posts

Hi All,

does anyone know any good tutorials on parsing an object file into your program, specifically with regards to when a vertex has multiple normal values defined in the faces section?

Much appreciated,

Graham

##### Share on other sites

I do not think that a tutorial is needed for this because the file format is well known and described e.g. on Wikipedia https://en.wikipedia.org/wiki/Wavefront_.obj_file and there are also very good implementations out there you could take a look at https://github.com/syoyo/tinyobjloader

I already implemented multiple parsers and it wasnt absolutely difficult nor time consuming to understand the file format

##### Share on other sites
It seems my question was not specific enough.

As far as I can see there are 3 ways of converting an obj file to arrays of vertices and normals and I was wondering which Is the best / industry standard way of doing it.

E.g. For the following obj file

v 1.0 0.0 0.0
v 0.0 -0.5 -1.0
v -1.0 0.0 0.0
v 0.0 -0.5 1.0

vn 0.0 0.707106 0.707106
vn 0.0 0.707106 -0.707106

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

You could
Option 1: have the normals averaged as I currently do

Vertices = 1.0 0.0 0.0 0.0 -0.5 -1.0 -1.0 0.0 0.0 0.0 -0.5 1.0

Normals = 0.0 1.0 0.0 0.0 0.707106 -0.707106 0.0 1.0 0.0 0.0 0.707106 0.707106

Indices = 1 2 3 1 3 4

Or option 2: sort the data into the vertex order described by the faces section

Vertices = 1.0 0.0 0.0 0.0 -0.5 -1.0 -1.0 0.0 0.0 1.0 0.0 0.0 -1.0 0.0 0.0 0.0 -0.5 1.0

Normals = 0.0 0.707106 -0.707106 0.0 0.707106 -0.707106 0.0 0.707106 -0.707106 0.0 0.707106 0.707106 0.0 0.707106 0.707106 0.0 0.707106 0.707106

Indices = 1 2 3 4 5 6

Or option 3: start with the vertices in the order defined and add any with multiple normal values to the end

Vertices = 1.0 0.0 0.0 0.0 -0.5 -1.0 -1.0 0.0 0.0 0.0 -0.5 1.0 1.0 0.0 0.0 -1.0 0.0 0.0

Normals = 0.0 0.707106 -0.707106 0.0 0.707106 -0.707106 0.0 0.707106 -0.707106 0.0 0.707106 0.707106 0.0 0.707106 0.707107 0.0 0.707107 0.707106

Indices = 1 2 3 5 6 4

Option 1 is the current way I do it, option 2 seems like the easiest but option 3 seems like it could be the best in terms of data size but would be the hardest algorithm to implement.

Hopefully that makes my question more clear

##### Share on other sites

When you have vertex normals, you don't average them. Something like weighted averaging needed only when you calculating normals on your own, because there's no normals data in obj file.

Now you must construct all vertices with all corresponding data (just like that, combined value of position/texture_coord/normal), then build index, removing duplicate vertices, i.e verts that has all the same pos/tex_co/norm. In the end there might be more verts in your vertex buffer than there's 'v' directives in obj file, and that's normal.

The first thing you must accept - is that 'v' directive does not create your final vertex. It only describes position in space. Resulting vertex is a combination of all data from all v/vt/vn streams.

Face vertex index in 'v' stream will tell about source vertex identity (same index means physically same point, while different indices means unrelated verts even if those are in same position), but that's needed only when you reconstruct normals, so shouldn't bother you at the moment.

##### Share on other sites

You might misunderstand something while indices are for storing less data, .obj files define faces the original model was build from so this isnt something inside the file format itself but more a thing your game (engine) would process.

v 1.0 0.0 0.0
v 0.0 -0.5 -1.0
v -1.0 0.0 0.0
v 0.0 -0.5 1.0

vn 0.0 0.707106 0.707106
vn 0.0 0.707106 -0.707106

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

The resulting mesh would consist of

//First Triangle
vec3 1.0 0.0 0.0 | vec3 0.0 0.707106 -0.707106
vec3 0.0 -0.5 -1.0 | vec3 0.0 0.707106 -0.707106
vec3 -1.0 0.0 0.0 | vec3 0.0 0.707106 -0.707106

//Second Triangle
vec3 1.0 0.0 0.0 | vec3 0.0 0.707106 0.707106
vec3 -1.0 0.0 0.0 | vec3 0.0 0.707106 0.707106
vec3 0.0 -0.5 1.0 | vec3 0.0 0.707106 0.707106

//in a 6 component Vertex Buffer 3 components Vertices and 3 components Normals

To get an Index Buffer you need to store each combination of values once and set an index at its position. In your case you have to store all 6 faces because they do not share any vertex.

How you do it is up to you, someone store them in a constant array so vertex positions first then normals, I prefer normaly storing vertices as is in the buffer so vertex | normals | vertex | normals. Both work as expected and there is no right way of indexing

##### Share on other sites

I used this code early on when I was understanding the format, its a good start.  It has some flaws, but its useable.  Assumes all models are only using 3 verts per face so you need to triangulate models first.

##### Share on other sites

As far as I can see there are 3 ways of converting an obj file to arrays of vertices and normals and I was wondering which Is the best / industry standard way of doing it.

If what you're asking is "what way does GameX parse .obj files?" then you should understand that GameX most likely doesn't even use .obj files at all.  Thing is, .obj is a fine enough format for tutorial material, but it's a horrible format for real-world usage; slow and error-prone to parse.  GameX most likely uses it's own proprietary binary model format, with it's own exporter(s) from common modelling program(s), and just loads the format directly into vertex buffers without any parsing at all.  So the best/industry standard way of parsing .obj files is actually: don't; use a better format instead.

##### Share on other sites

Thanks for the replies.

I see now that option 2 and 3 that I mentioned are just two different ways of doing the same thing and I knew option 1 was not the way to go.

I have a script which extracts the data from the .obj file and creates 3 text files one for the vertices in the correct order, one for the normals in the correct order and one for the indices, which I then read in in my game.

##### Share on other sites
#ifndef stringlistH
#define stringlistH

#include "FileHandling.h"
#include "const_vars.h"
#include <vector>
#ifndef WINDOWS_CMP
#include <android/log.h>
#endif
#include <fstream>
#include <sstream>
#include "DxcMath.h"
#include "string"
#include "stdlib.h"
#include <iostream>

#define APP_LOG2 "WNB_TXTLOG"

enum tFloatConversionRule { tFCDot, tFCcomma };

extern tFloatConversionRule FLOAT_CONVERSION;

#ifdef WINDOWS_CMP

inline float pstrtofloat(AnsiString num)
{
AnsiString temp = num;
if (FLOAT_CONVERSION == tFCDot)     //English rule (floats are written as 10.25)
temp = StringReplace(num,",",".", TReplaceFlags() << rfReplaceAll);
else      //Polish rule (floats are written as 10,25)
temp = StringReplace(num,".",",", TReplaceFlags() << rfReplaceAll);

return StrToFloat(temp);
}
#endif

#ifndef WINDOWS_CMP

inline AnsiString booltostring(bool hue)
{
if (hue) return "true"; else return "false";
}

extern std::vector<AnsiString> w_nawiasie[10];

inline AnsiString IntToStr(int i)
{
std::stringstream s; s;

s << i;

AnsiString converted(s.str());
return converted;
}

inline AnsiString FloatToStr(float i)
{
std::stringstream s; s;

s << i;

AnsiString converted(s.str());
return converted;
}

inline int Pos(AnsiString sub, AnsiString str)
{
std::size_t found = str.find(sub,0);
if (found!=textObject ::npos)
return int(found)+1;
else
return 0;
}

inline int pstrtoint(AnsiString num)
{
return ::atoi(num.c_str());
}

inline int StrToInt(AnsiString num)
{
return ::atoi(num.c_str());
}

inline AnsiString StringReplace(AnsiString str, AnsiString substr, AnsiString with)
{
AnsiString s = str;
while (Pos(substr, s) > 0)
{
int pos = Pos(substr, s)-1;
s.erase(pos,substr.length());
s.insert(pos, with);
}
return s;
}

//inline float pstrtofloat(AnsiString num)

inline float pstrtofloat(AnsiString num)//
{
AnsiString temp = num;

if (FLOAT_CONVERSION == tFCDot)
temp = StringReplace(num,",",".");
else
temp = StringReplace(num,".",",");
return float(	::atof(temp.c_str())	);
}

//

inline AnsiString stddelete(AnsiString str, int pos, int len) //this is for textObject  only because i will call only Pos()-1 from it
{
AnsiString s = str;
s.erase(pos, len);
return s;
}

extern AnsiString LowerCase(AnsiString str);
extern AnsiString UpperCase(AnsiString str);

inline int numberofchars(AnsiString line, AnsiString charstr)
{
AnsiString line2;
int result = 0;
line2 = line;
result = 0;

while ( Pos(LowerCase(charstr), LowerCase(line2)) > 0 )
{
line2 = stddelete( line2, 0, Pos(LowerCase(charstr), LowerCase(line2)) + TextLength(charstr) - 1 );
result = result + 1;
}

return result;
}

inline AnsiString get_text_between2(AnsiString b1, AnsiString b2, AnsiString original_str)
{

if (Pos(b1,original_str) == 0) return original_str;
if (Pos(b2,original_str) == 0) return original_str;

//float, 2, 3); ahue ahue
AnsiString temp1 = stddelete(original_str, 0, Pos(b1,original_str) + b1.length() - 1 );
int k = Pos(b2,temp1) - 1;
AnsiString temp2 = stddelete(temp1, k, temp1.length() - k);
return temp2;
}

inline AnsiString get_before_char(AnsiString text, AnsiString sign, bool casesensitive)
{
AnsiString s = text;
AnsiString tmp;
if (casesensitive == false)
tmp = stddelete(s,Pos(LowerCase(sign),LowerCase(s))-1,s.length()-Pos(LowerCase(sign),LowerCase(s))+1);
else
tmp = stddelete(s,Pos(sign,s)-1,s.length()-Pos(sign,s)+1);

return tmp;
}

inline AnsiString get_after_char(AnsiString text, AnsiString sign, bool casesensitive)
{
AnsiString s = text;
AnsiString tmp;
if (casesensitive == false)
tmp = stddelete(s,0,Pos(LowerCase(sign),LowerCase(s)));
else
tmp = stddelete(s,0,Pos(sign,s));

return tmp;
}

inline AnsiString get_after_char2(AnsiString text, AnsiString sign, bool casesensitive)
{
AnsiString s = text;
AnsiString tmp;
if (casesensitive == false)
tmp = stddelete(s,0,Pos(LowerCase(sign),LowerCase(s))+sign.length()-1);
else
tmp = stddelete(s,0,Pos(sign,s)+sign.length()-1);

return tmp;
}

inline void get_all_in_nawias(AnsiString ainput, AnsiString aznak, int index)
{
textObject  input = ainput;
textObject  delimiter = aznak;

w_nawiasie[index].clear();

AnsiString pikok;

std::size_t  start = 0U;
std::size_t end = input.find(delimiter);

while (end != textObject ::npos) {

pikok =  input.substr(start, end - start);
w_nawiasie[index].push_back(pikok);

start = end + delimiter.length();
end = input.find(delimiter, start);
}
pikok =  input.substr(start, end);
w_nawiasie[index].push_back(pikok);

}

inline AnsiString ExtractFileName(AnsiString pikok)
{
get_all_in_nawias(pikok,"/",0);
return w_nawiasie[0][ w_nawiasie[0].size()-1 ];
}

inline AnsiString ExtractFilePath(AnsiString pikok)
{
get_all_in_nawias(pikok,"/",0);
if (w_nawiasie[0].size() <= 0) return "";
AnsiString ahue = "";
for (int i=0; i < w_nawiasie[0].size()-1; i++)
ahue = ahue + w_nawiasie[0][i]+"/";

return ahue;
}

inline AnsiString get_filename_ext(AnsiString pikok)
{
AnsiString filename = ExtractFileName(pikok);
return get_after_char(filename, ".", false);
}

inline AnsiString change_filename_ext(AnsiString pikok, AnsiString next) //push .extension important with dot ex. ".tga"
{
AnsiString path 	= ExtractFilePath(pikok);
AnsiString filename = ExtractFileName(pikok);
AnsiString fname	= get_before_char(filename,".", false);

return path+fname+next;

}

#ifdef WINDOWS_CMP
struct TStringList
#endif

#ifndef WINDOWS_CMP
struct TStringList
#endif
{

int Count;
std::vector<AnsiString> Strings;

{
AnsiString p = text;
Strings.push_back(text);
Count = Count + 1;
}

AnsiString GetText()
{
AnsiString res = "";
int i;
for (i=0; i < Count; i++)
res = res + Strings[i] + "\n";
return res;
}

{
std::vector<AnsiString> tmp;
tmp.clear();
for (int i=0; i < atline; i++)
tmp.push_back(Strings[i]);

AnsiString prefix;
AnsiString suffix;

if (Strings[atline].length() > 0)
{
prefix = stddelete(Strings[atline], atpos, 100000);
suffix = stddelete(Strings[atline], 0, atpos);

tmp.push_back(prefix);
tmp.push_back(suffix);
} else
{
tmp.push_back("");
tmp.push_back("");
}

for (int i=atline+1; i < Count; i++)
tmp.push_back(Strings[i]);

Strings.clear();

Count = Count + 1;
for (int i=0; i < Count; i++)
Strings.push_back(tmp[i]);

tmp.clear();
}

void Clear()
{
Count = 0;
Strings.clear();
}

#ifdef WINDOWS_CMP
AStringList()
#endif

#ifndef WINDOWS_CMP
TStringList()
#endif
{
Clear();
}

#ifdef WINDOWS_CMP
~AStringList()
#endif

#ifndef WINDOWS_CMP
~TStringList()
#endif
{
Clear();
}

#define STRLST "WNB_LOG"
{

std::ifstream file(fname.c_str());
if (file.good() == false) {
file.close();
__android_log_print(ANDROID_LOG_VERBOSE, STRLST, "!Not good text file, aborting", 1+1);
return;
}

AnsiString str;

Count = 0;
if (Strings.size() > 0) Strings.clear();
while (std::getline(file, str))
{
Strings.push_back(str);
__android_log_print(ANDROID_LOG_VERBOSE, STRLST, str.c_str(), 1+1);
Count = Count + 1;
}

file.close();

for (int i=0; i < Count; i++)
{
AnsiString str = Strings[i];
char lastChar = str.at( str.length() - 1 );

if ( (lastChar == '\r') || (lastChar == '\n') )
{
//		AnsiString p = "deleted newline: "+str;
//		__android_log_print(ANDROID_LOG_VERBOSE, APP_LOG2, p.c_str(), 1+1);

str.erase(str.length()-1);
Strings[i] = str;
}
}
}

//textObject  p;
AnsiString pc;

void SaveToFile(AnsiString fname)
{
pc = GetText();
std::ofstream outfile (fname.c_str(),std::ofstream::binary);
int len = pc.length();
char * buff = new char[ len ];
memcpy(buff, pc.c_str(), sizeof(char) * len);
outfile.write (buff, len);
outfile.close();
}

};

#endif

extern void  init_string_float_conversion_rule();

inline AnsiString POINT_TO_TEXT(t3dpoint<float> p)
{
return "X "+FloatToStr(p.x) + " Y "+FloatToStr(p.y)+" Z "+FloatToStr(p.z);
}

inline AnsiString iPOINT_TO_TEXT(t3dpoint<int> p)
{
return "X "+IntToStr(p.x) + " Y "+IntToStr(p.y)+" Z "+IntToStr(p.z);
}

inline int booltoint(bool k)
{
if (k) return 1; else return 0;
}

inline bool stringtobool(AnsiString hue)
{
AnsiString s = LowerCase(hue);
if (s == "0") return false;
if (s == "1") return true;

if (s == "false") return false;
if (s == "true") return true;
return false;
}

#ifdef WINDOWS_CMP
#include "string_unit.hpp"
typedef TStringList AStringList;

inline void AddLineAtPos(TStringList * s, int atline, int atpos)
{
s->Strings[atline].Insert("\n", atpos);
}
#endif

#endif

#include "stringlist.h"
#include <algorithm>    // std::transform

tFloatConversionRule FLOAT_CONVERSION;

void  init_string_float_conversion_rule()
{
AnsiString f;
float p = 10.250f;
f = FloatToStr(p);
#ifndef WINDOWS_CMP
if (Pos(".",f) > 0)
#endif

#ifdef WINDOWS_CMP
if (WinPosRx(".",f) > 0)
#endif
FLOAT_CONVERSION = tFCDot; else FLOAT_CONVERSION = tFCcomma;

}
#ifndef WINDOWS_CMP

std::vector<AnsiString> w_nawiasie[10];

AnsiString LowerCase(AnsiString str)
{
AnsiString s = str;
std::transform(s.begin(), s.end(), s.begin(), ::tolower);
return s;
}

AnsiString UpperCase(AnsiString str)
{
AnsiString s = str;
std::transform(s.begin(), s.end(), s.begin(), ::toupper);
return s;
}

#endif

--------------------------------------------------------------------------------------------

inline t3dpoint<float> OBJ_LINE_TO_T3dPOINT(AnsiString s)
{
t3dpoint<float> result;

get_all_in_nawias(s, " ",4);
result.x = pstrtofloat(w_nawiasie[4][1]);
result.y = pstrtofloat(w_nawiasie[4][2]);
result.z = pstrtofloat(w_nawiasie[4][3]);
//ShowMessage(showmsg3flt(result));

return result;
}

inline textpoint OBJ_LINE_TO_TEXTPOINT(AnsiString s)
{
textpoint result;

get_all_in_nawias(s, " ",4);
result.x = pstrtofloat(w_nawiasie[4][1]);
result.y = pstrtofloat(w_nawiasie[4][2]);

return result;
}

{
int vcnt = 0;
int fcnt = 0;

int i;

TStringList * k = new TStringList();

TStringList * obj = new TStringList();

TStringList * VE = new TStringList();
TStringList * VT = new TStringList();
TStringList * VN = new TStringList();
AnsiString s;
int r;
for (r = 0; r < k->Count; r++) {
s = k->Strings[r];
if ( (Pos("v ",LowerCase(s)) > 0) && (numberofchars(s,".") == 3) )  vcnt = vcnt + 1;
if  (Pos("f ",LowerCase(s)) > 0)  fcnt = fcnt + 1;
//		  if  ( (Pos("usemtl",LowerCase(s)) > 0) && Pos("#",LowerCase(s)) == 0)  MATERIAL_FILE->Add("NEW_MATERIAL_ON_FACE("+IntToStr(fcnt+1)+");");

}

vcnt = -1;
fcnt = -1;

// POBRANIE LISTY VERTEXOW DO JEDNORODNEJ LISTY
for (r = 0; r < k->Count; r++)
{
s = k->Strings[r];

if ( (Pos("v ",LowerCase(s)) > 0) && (numberofchars(s,".") == 3) )
{
vcnt = vcnt + 1;
}

if  (Pos("f ",LowerCase(s)) > 0)
fcnt = fcnt + 1;

}

//III PASS zczytanie informacji o face'ach i przypozadkowanie z VBO_N vertexow do VBO_V

AnsiString Vertex1, TexCoord1, NormalCoord1;
AnsiString Vertex2, TexCoord2, NormalCoord2;
AnsiString Vertex3, TexCoord3, NormalCoord3;
fcnt = 0;
this->header.LENGTH = fcnt * 3; //cause they are triangles
this->AOS 			= new TTGLVertex<T,float,float>[ this->header.LENGTH ];
this->FaceLength	= fcnt;
this->Matrixarr 	= new tmatrixtype[ this->FaceLength ];
this->VBO_BE 		= new tvbofacenfo[ this->FaceLength ];
this->Textureindex  = new int[ this->FaceLength ];

for (int i=0; i < this->FaceLength; i++)	Textureindex[i] = 0;

for (r = 0; r < k->Count; r++)
{
s = k->Strings[r];

if (Pos("v ",LowerCase(s)) > 0)

if (Pos("vt ",LowerCase(s)) > 0)

if (Pos("vn ",LowerCase(s)) > 0)
}

if (VT->Count != VE->Count)
if (VN->Count != VE->Count)

fcnt = -1;
vcnt = 0;
for (r = 0; r < k->Count; r++)
{
s = k->Strings[r];

if  (Pos("f ",LowerCase(s)) > 0)
{ //to jest face
fcnt = fcnt + 1;

get_all_in_nawias(s, " ",4);

Vertex1 = get_before_char(w_nawiasie[4][1],"/",false);
Vertex2 = get_before_char(w_nawiasie[4][2],"/",false);
Vertex3 = get_before_char(w_nawiasie[4][3],"/",false);

get_all_in_nawias(w_nawiasie[4][1], "/", 0);

TexCoord1 = get_before_char(w_nawiasie[0][1],"/",false);

NormalCoord1 = get_before_char(w_nawiasie[4][1],"/",false);
NormalCoord1 = get_before_char(w_nawiasie[4][2],"/",false);
NormalCoord1 = get_before_char(w_nawiasie[4][3],"/",false);

int a,b,c;
a=pstrtoint(Vertex1);
b=pstrtoint(Vertex2);
c=pstrtoint(Vertex3);

if ( (a  <= VE->Count) && (b   <= VE->Count) &&
(c   <= VE->Count) )
{

this->Matrixarr[fcnt] = mtTriangles;
this->VBO_BE[fcnt].INDEX_START = fcnt * 3;
this->VBO_BE[fcnt].length 		= 3;

this->AOS[ vcnt + 0 ].v = OBJ_LINE_TO_T3dPOINT(	VE->Strings[a-1] );
this->AOS[ vcnt + 0 ].t = OBJ_LINE_TO_TEXTPOINT(	VT->Strings[a-1] );
this->AOS[ vcnt + 0 ].n = OBJ_LINE_TO_T3dPOINT(	VN->Strings[a-1] );

this->AOS[ vcnt + 1 ].v = OBJ_LINE_TO_T3dPOINT(	VE->Strings[b-1] );
this->AOS[ vcnt + 1 ].t = OBJ_LINE_TO_TEXTPOINT(	VT->Strings[b-1] );
this->AOS[ vcnt + 1 ].n = OBJ_LINE_TO_T3dPOINT(	VN->Strings[b-1] );

this->AOS[ vcnt + 2 ].v = OBJ_LINE_TO_T3dPOINT(	VE->Strings[c-1] );
this->AOS[ vcnt + 2 ].t = OBJ_LINE_TO_TEXTPOINT(	VT->Strings[c-1] );
this->AOS[ vcnt + 2 ].n = OBJ_LINE_TO_T3dPOINT(	VN->Strings[c-1] );
vcnt = vcnt + 3;

}

}

}

}

Milkshape could export again obj to some simplified standard wavefront obj, so code above will import it correctly

##### Share on other sites

If you need to get some job done though, here's a library that will save you precious time: http://assimp.sourceforge.net/

Cheers.

##### Share on other sites

Ok so I have an algorithm that imports .obj files which works fine but it was very blocky, like my model in blender.

I applied smoothing in blender and when I imported the new .obj file into my game the vertices were mostly ok but there were some major spikes and vertices in random positions.

is there a different approach between an object file with s off and with s 1?

Graham

## Create an account

Register a new account

• ### Forum Statistics

• Total Topics
628383
• Total Posts
2982384

• 10
• 9
• 15
• 24
• 11