Skip to content

Instantly share code, notes, and snippets.

@rioki
Created January 26, 2018 18:36
Show Gist options
  • Star 0 You must be signed in to star a gist
  • Fork 0 You must be signed in to fork a gist
  • Save rioki/e3e4c37ebee2df4c183285739a46dcc1 to your computer and use it in GitHub Desktop.
Save rioki/e3e4c37ebee2df4c183285739a46dcc1 to your computer and use it in GitHub Desktop.
OBJ Parser
//
// OBJ Parser
//
// Copyright (c) 2014 Sean Farrell
//
// Permission is hereby granted, free of charge, to any person obtaining a copy
// of this software and associated documentation files (the "Software"), to deal
// in the Software without restriction, including without limitation the rights
// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
// copies of the Software, and to permit persons to whom the Software is
// furnished to do so, subject to the following conditions:
//
// The above copyright notice and this permission notice shall be included in all
// copies or substantial portions of the Software.
//
// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
// SOFTWARE.
//
#include "ObjParser.h"
#include <sstream>
#include <iterator>
#include <map>
namespace
{
inline
std::ostream& operator << (std::ostream& os, const std::vector<std::string>& values)
{
for (unsigned int i = 0; i < values.size(); i++)
{
os << values[i];
if (i == values.size() - 2)
{
os << " or ";
}
else if (i != values.size() - 1)
{
os << ", ";
}
}
return os;
}
}
ObjParser::ObjParser()
: line(0), token(NO_TOKEN), next_token(NO_TOKEN) {}
ObjParser::~ObjParser() {}
const std::vector<rgm::vec3>& ObjParser::get_vertices() const
{
return vertices;
}
const std::vector<rgm::vec3>& ObjParser::get_normals() const
{
return normals;
}
const std::vector<rgm::vec2>& ObjParser::get_texcoords() const
{
return texcoords;
}
const std::vector<std::vector<rgm::ivec3>>& ObjParser::get_faces() const
{
return faces;
}
void ObjParser::parse(const std::string& f)
{
file = f;
input.open(file);
if (! input.good())
{
std::stringstream buff;
buff << "Failed to open " << file << " for reading.";
throw std::runtime_error(buff.str());
}
// prime the next_token
get_next_token();
while (next_token != END_OF_FILE)
{
parse_line();
}
}
void ObjParser::get_next_token()
{
// skip whitepsaces
std::string v;
TokenType t = lex_token(v);
while (t == WHITESPACE || t == NEWLINE || t == COMMENT)
{
if (t == NEWLINE)
{
line++;
}
v = "";
t = lex_token(v);
}
token = next_token;
value = next_value;
next_token = t;
next_value = v;
}
ObjParser::TokenType ObjParser::lex_token(std::string& value)
{
int c = input.get();
switch (c)
{
case ' ': case '\t': case '\v':
value.push_back(c);
return lex_whitespace(value);
case '\n': case '\r':
value.push_back(c);
return lex_newline(value);
case '#':
value.push_back(c);
return lex_comment(value);
case '/':
value = "/";
return SLASH;
case '\\':
value = "\\";
return BSLASH;
case '.':
value = ".";
return DOT;
case '+': case '-': case '0': case '1': case '2': case '3':
case '4': case '5': case '6': case '7': case '8': case '9':
value.push_back(c);
return lex_number(value);
case 'a': case 'b': case 'c': case 'd': case 'e': case 'f':
case 'g': case 'h': case 'i': case 'j': case 'k': case 'l':
case 'm': case 'n': case 'o': case 'p': case 'q': case 'r':
case 's': case 't': case 'u': case 'v': case 'w': case 'x':
case 'y': case 'z':
case 'A': case 'B': case 'C': case 'D': case 'E': case 'F':
case 'G': case 'H': case 'I': case 'J': case 'K': case 'L':
case 'M': case 'N': case 'O': case 'P': case 'Q': case 'R':
case 'S': case 'T': case 'U': case 'V': case 'W': case 'X':
case 'Y': case 'Z':
case '_':
value.push_back(c);
return lex_identifier(value);
case EOF:
return END_OF_FILE;
default:
{
value.push_back(c);
std::stringstream buff;
buff << "Unexpected character " << value << ".";
throw std::runtime_error(buff.str());
}
}
}
ObjParser::TokenType ObjParser::lex_whitespace(std::string& value)
{
int c = input.get();
while (true)
{
switch (c)
{
case ' ': case '\t': case '\v':
value.push_back(c);
break;
default:
input.unget();
return WHITESPACE;
}
c = input.get();
}
}
ObjParser::TokenType ObjParser::lex_newline(std::string& value)
{
int c = input.get();
switch (c)
{
case '\n': case '\r':
if (c != value[0])
{
// \r\n or \n\r
value.push_back(c);
}
else
{
// treat \n \n as two newline, obviously
input.unget();
}
return NEWLINE;
default:
input.unget();
return NEWLINE;
}
}
ObjParser::TokenType ObjParser::lex_identifier(std::string& value)
{
int c = input.get();
while (true)
{
switch (c)
{
case '0': case '1': case '2': case '3': case '4':
case '5': case '6': case '7': case '8': case '9':
case 'a': case 'b': case 'c': case 'd': case 'e': case 'f':
case 'g': case 'h': case 'i': case 'j': case 'k': case 'l':
case 'm': case 'n': case 'o': case 'p': case 'q': case 'r':
case 's': case 't': case 'u': case 'v': case 'w': case 'x':
case 'y': case 'z':
case 'A': case 'B': case 'C': case 'D': case 'E': case 'F':
case 'G': case 'H': case 'I': case 'J': case 'K': case 'L':
case 'M': case 'N': case 'O': case 'P': case 'Q': case 'R':
case 'S': case 'T': case 'U': case 'V': case 'W': case 'X':
case 'Y': case 'Z':
case '_':
value.push_back(c);
break;
default:
input.unget();
return IDENTIFIER;
}
c = input.get();
}
}
ObjParser::TokenType ObjParser::lex_number(std::string& value)
{
// NOTE: we don't actually validate if it makes a valid number here
// NOTE: the e-notation is not implemented, is that actually valid in PLY?
int c = input.get();
while (true)
{
switch (c)
{
case '+': case '-': case '.':
case '0': case '1': case '2': case '3': case '4':
case '5': case '6': case '7': case '8': case '9':
value.push_back(c);
break;
default:
input.unget();
return NUMBER;
}
c = input.get();
}
}
ObjParser::TokenType ObjParser::lex_comment(std::string& value)
{
int c = input.get();
while (c != '\n' && c != '\r' && c != EOF)
{
c = input.get();
value.push_back(c);
}
input.unget();
return COMMENT;
}
void ObjParser::parse_keyword(const std::string& keyword)
{
get_next_token();
if (token != IDENTIFIER || value != keyword)
{
std::stringstream buff;
buff << file << "(" << line << "): Expected " << keyword << " but got " << value << ".";
throw std::runtime_error(buff.str());
}
}
unsigned int ObjParser::parse_keyword(const std::vector<std::string>& keywords)
{
get_next_token();
auto i = std::find(keywords.begin(), keywords.end(), value);
if (token != IDENTIFIER || i == keywords.end())
{
std::stringstream buff;
buff << file << "(" << line << "): Expected " << keywords << " but got " << value << ".";
throw std::runtime_error(buff.str());
}
return std::distance(keywords.begin(), i);
}
std::string ObjParser::parse_identifier()
{
get_next_token();
if (token != IDENTIFIER)
{
std::stringstream buff;
buff << file << "(" << line << "): Expected identifier but got " << value << ".";
throw std::runtime_error(buff.str());
}
return value;
}
std::string ObjParser::parse_identifier_or_number()
{
get_next_token();
if (token != IDENTIFIER && token != NUMBER)
{
std::stringstream buff;
buff << file << "(" << line << "): Expected identifier or number but got " << value << ".";
throw std::runtime_error(buff.str());
}
return value;
}
float ObjParser::parse_float()
{
get_next_token();
if (token == NUMBER)
{
// TODO use endptr to check if the entiere string was read
double dv = strtod(&value[0], NULL);
return (float)dv;
}
else
{
std::stringstream buff;
buff << file << "(" << line << "): Expected number but got " << value << ".";
throw std::runtime_error(buff.str());
}
}
unsigned long ObjParser::parse_integer()
{
get_next_token();
if (token == NUMBER)
{
// TODO use endptr to check if the entiere string was read
unsigned long ulv = strtoul(&value[0], NULL, 10);
return ulv;
}
else
{
std::stringstream buff;
buff << file << "(" << line << "): Expected number but got " << value << ".";
throw std::runtime_error(buff.str());
}
}
std::string ObjParser::parse_filename()
{
// this is not correctly implemented; it could be a fully qualified path with spaces and all that jazz
std::string file;
while (next_token == IDENTIFIER ||
next_token == SLASH ||
next_token == BSLASH ||
next_token == DOT)
{
get_next_token();
file += value;
if (value == "mtl")
{
return file;
}
}
return file;
}
std::vector<std::string> keywords = [] () -> std::vector<std::string> {
std::vector<std::string> result;
result.push_back("v");
result.push_back("vt");
result.push_back("vn");
result.push_back("vp");
result.push_back("f");
result.push_back("o");
result.push_back("g");
result.push_back("s");
result.push_back("mtllib");
result.push_back("usemtl");
return result;
}();
enum Keyword
{
V,
VT,
VN,
VP,
F,
O,
G,
S,
MTLLIB,
USEMTL
};
void ObjParser::parse_line()
{
unsigned int keyword = parse_keyword(keywords);
switch (keyword)
{
case V:
parse_vertex();
break;
case VT:
parse_texcoord();
break;
case VN:
parse_normal();
break;
case VP:
parse_parmeter();
break;
case F:
parse_face();
break;
case O:
parse_object();
break;
case G:
parse_group();
break;
case S:
parse_smothing();
break;
case MTLLIB:
parse_mtllib();
break;
case USEMTL:
parse_usemtl();
break;
default:
throw std::logic_error("Unknown line keyword!");
}
}
void ObjParser::parse_vertex()
{
float x = parse_float();
float y = parse_float();
float z = parse_float();
float w = 0.0f;
if (next_token == NUMBER)
{
w = parse_float();
}
vertices.push_back(rgm::vec3(x, y, z));
}
void ObjParser::parse_texcoord()
{
float u = parse_float();
float v = parse_float();
float w = 0.0f;
if (next_token == NUMBER)
{
w = parse_float();
}
texcoords.push_back(rgm::vec2(u, v));
}
void ObjParser::parse_normal()
{
float x = parse_float();
float y = parse_float();
float z = parse_float();
normals.push_back(rgm::vec3(x, y, z));
}
void ObjParser::parse_parmeter()
{
float u = parse_float();
float v = 0.0f;
if (next_token == NUMBER)
{
v = parse_float();
}
float w = 0.0f;
if (next_token == NUMBER)
{
w = parse_float();
}
// discard value
}
void ObjParser::parse_face()
{
std::vector<rgm::ivec3> points;
while (next_token == NUMBER)
{
rgm::ivec3 p = parse_face_point();
points.push_back(p);
}
faces.push_back(points);
}
rgm::ivec3 ObjParser::parse_face_point()
{
int v = -1;
int t = -1;
int n = -1;
v = parse_integer();
if (next_token == SLASH)
{
get_next_token();
}
if (next_token == NUMBER)
{
t = parse_integer();
}
if (next_token == SLASH)
{
get_next_token();
if (next_token == NUMBER)
{
n = parse_integer();
}
}
return rgm::ivec3(v, t, n);
}
void ObjParser::parse_mtllib()
{
std::string file = parse_filename();
}
void ObjParser::parse_usemtl()
{
std::string id = parse_identifier();
}
void ObjParser::parse_object()
{
std::string id = parse_identifier();
}
void ObjParser::parse_group()
{
std::string id = parse_identifier();
}
void ObjParser::parse_smothing()
{
// s 1
// s on
// s off
std::string id = parse_identifier_or_number();
}
//
// OBJ Parser
//
// Copyright (c) 2014 Sean Farrell
//
// Permission is hereby granted, free of charge, to any person obtaining a copy
// of this software and associated documentation files (the "Software"), to deal
// in the Software without restriction, including without limitation the rights
// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
// copies of the Software, and to permit persons to whom the Software is
// furnished to do so, subject to the following conditions:
//
// The above copyright notice and this permission notice shall be included in all
// copies or substantial portions of the Software.
//
// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
// SOFTWARE.
//
#ifndef _OBJ_PARSER_H_
#define _OBJ_PARSER_H_
#include <string>
#include <fstream>
#include <vector>
#include <tuple>
#include <rgm/rgm.h>
class ObjParser
{
public:
ObjParser();
~ObjParser();
const std::vector<rgm::vec3>& get_vertices() const;
const std::vector<rgm::vec3>& get_normals() const;
const std::vector<rgm::vec2>& get_texcoords() const;
const std::vector<std::vector<rgm::ivec3>>& get_faces() const;
void parse(const std::string& file);
private:
enum TokenType
{
NO_TOKEN,
WHITESPACE,
NEWLINE,
COMMENT,
IDENTIFIER,
NUMBER,
SLASH,
DOT,
BSLASH,
END_OF_FILE
};
std::ifstream input;
std::string file;
unsigned int line;
TokenType token;
TokenType next_token;
std::string value;
std::string next_value;
std::vector<rgm::vec3> vertices;
std::vector<rgm::vec3> normals;
std::vector<rgm::vec2> texcoords;
std::vector<std::vector<rgm::ivec3>> faces;
void get_next_token();
TokenType lex_token(std::string& value);
TokenType lex_whitespace(std::string& value);
TokenType lex_newline(std::string& value);
TokenType lex_comment(std::string& value);
TokenType lex_identifier(std::string& value);
TokenType lex_number(std::string& value);
void parse_keyword(const std::string& keyword);
unsigned int parse_keyword(const std::vector<std::string>& keywords);
std::string parse_identifier();
std::string parse_identifier_or_number();
float parse_float();
unsigned long parse_integer();
std::string parse_filename();
void parse_line();
void parse_vertex();
void parse_texcoord();
void parse_normal();
void parse_parmeter();
void parse_face();
void parse_mtllib();
void parse_usemtl();
void parse_object();
void parse_group();
void parse_smothing();
rgm::ivec3 parse_face_point();
};
#endif
// This code takes the OBJ contents and normalizes, for consuption in OpenGL.
// That is each vertex is a unique set of vertex, normal and texture coordinate.
ObjParser parser;
parser.parse(file);
auto v = parser.get_vertices();
auto n = parser.get_normals();
auto t = parser.get_texcoords();
auto f = parser.get_faces();
vertices.clear();
normals.clear();
texcoords.clear();
std::map<rgm::ivec3, size_t, iv3less> index_mapping;
for (auto face : f)
{
std::vector<size_t> idx;
for (rgm::ivec3 fv : face)
{
auto i = index_mapping.find(fv);
if (i != index_mapping.end())
{
idx.push_back(i->second);
}
else
{
size_t vi = vertices.size();
vertices.push_back(v[fv[0] - 1]);
normals.push_back(n[fv[2] - 1]);
rgm::vec2 tc = t[fv[1] - 1];
tc[1] = 1 - tc[1];
texcoords.push_back(tc);
index_mapping[fv] = vi;
idx.push_back(vi);
}
}
for (unsigned int i = 2; i < idx.size(); i++)
{
add_face(idx[0], idx[i - 1], idx[i]);
}
}
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment