// BlockyFroggy
// Copyright © 2017 John Ryland.
// All rights reserved.
#include "ObjModel.h"
#include "Log.h"
#include <string>
#include <sstream>
#include <cstdio>
#include <cstdlib>
#include <functional>
#include "ResourceLoader.h"
#include "Utilities.h"
DECLARE_LOG_CONTEXT(OBJ)
/*
To test, compile with:
g++ -std=c++11 ObjModel.cpp -o test -D TEST
*/
/*
Two concepts that could be taken
- OOC - out of core - processing the data progressively - useful in case of large models and in a pipeline or converter
- in core - slurp the whole file in to memory, and then just work from RAM - more efficient - limited to what fits in RAM - not a streaming solution
Code is/was using files and doing line based reading and parsing so would be suited for OOC, but I think for what is actually needed
Now abstracted this, DataSource is the base class, 2 implementations, one directed towards OOC, other using resource loader.
ResourceLoader one is async
*/
std::string removeCRLF(std::string line)
{
if (line[line.size()-1] == '\n')
return line.substr(0, line.size()-1);
return line;
}
class DataSource
{
public:
DataSource() {}
virtual ~DataSource() {}
virtual void open(const char* fileName, std::function<bool(bool okay, DataSource& fd)> callback) = 0;
virtual bool getNextLine(std::string& str) = 0;
};
class FileDataSource : public DataSource
{
public:
FileDataSource() : fd(nullptr) {}
~FileDataSource() override {
if (fd)
fclose(fd);
}
void open(const char* fileName, std::function<bool(bool okay, DataSource& fd)> callback) override {
std::string f = "../../Data/";
f += fileName;
if (!(fd = fopen(f.c_str(), "rb")))
Log(LL_Error, "failed to open file");
callback(!!fd, *this);
}
bool getNextLine(std::string& str) override {
char buf[512];
bool res = fd && fgets(buf, 511, fd);
if (res)
str = removeCRLF(buf);
return res;
}
FILE* fd;
};
class ResourceLoaderDataSource : public DataSource
{
public:
ResourceLoaderDataSource() {}
~ResourceLoaderDataSource() override {
}
void open(const char* fileName, std::function<bool(bool okay, DataSource& fd)> callback) override {
m_offset = 0;
m_res = loadFileAsync(fileName, true, [callback](Resource* res) {
ResourceLoaderDataSource tmp;
tmp.m_offset = 0;
tmp.m_res = res;
callback(!!res, tmp);
});
}
bool getNextLine(std::string& str) override {
str = "";
if (m_res && m_res->isLoaded() && m_res->data.size()) {
ByteArray& buffer = m_res->data;
if (m_offset >= buffer.size()) return false;
std::string ret;
while (m_offset < buffer.size() && buffer[m_offset] != '\n') {
ret += buffer[m_offset];
m_offset++;
}
m_offset++;
str = removeCRLF(ret);
return true;
}
return false;
}
size_t m_offset;
Resource* m_res;
FILE* fd;
};
//using Loader = FileDataSource;
using Loader = ResourceLoaderDataSource;
bool readMaterial(Material& mat, std::string& line, bool& more, DataSource& fd)
{
std::vector<std::string> tokens = Utilities::split(line, ' ');
mat.materialName = tokens[1];
while (fd.getNextLine(line)) {
std::vector<std::string> tokens = Utilities::split(line, ' ');
if (tokens.size() >= 2) {
if (tokens[0] == "Ns") {
mat.Ns = atof(tokens[1].c_str());
} else if (tokens[0] == "Ni") {
mat.Ni = atof(tokens[1].c_str());
} else if (tokens[0] == "Ka") {
if (tokens.size() != 4)
return false;
for (size_t i = 0; i < 3; i++)
mat.Ka[i] = atof(tokens[i+1].c_str());
} else if (tokens[0] == "Kd") {
if (tokens.size() != 4)
return false;
for (size_t i = 0; i < 3; i++)
mat.Kd[i] = atof(tokens[i+1].c_str());
//mat.Kd = { atof(tokens[1].c_str()), atof(tokens[2].c_str()), atof(tokens[3].c_str()) };
} else if (tokens[0] == "map_Kd") {
if (tokens.size() != 2)
return false;
mat.map_Kd = tokens[1];
} else if (tokens[0] == "Ks") {
if (tokens.size() != 4)
return false;
for (size_t i = 0; i < 3; i++)
mat.Ks[i] = atof(tokens[i+1].c_str());
//mat.Ks = { atof(tokens[1].c_str()), atof(tokens[2].c_str()), atof(tokens[3].c_str()) };
} else if (tokens[0] == "d") {
mat.d = atof(tokens[1].c_str());
} else if (tokens[0] == "illum") {
mat.illum = atoi(tokens[1].c_str());
} else if (tokens[0][0] == '#') {
// Just a comment, skip
} else {
// Not our token, return from here with more set to true
more = true;
return true;
}
}
}
return true;
}
void readMaterialLibrary(ObjScene& outScene, const char* fileName)
{
Loader fd;
fd.open(fileName, [&outScene](bool okay, DataSource& fd)->bool
{
if (!okay)
return false;
std::string line;
while (fd.getNextLine(line)) {
bool more = true;
while (more) {
more = false;
std::vector<std::string> tokens = Utilities::split(line, ' ');
if (tokens.size()) {
if (tokens[0] == "newmtl") {
Material newMat;
Log(LL_Error, "reading material %s", tokens[1].c_str());
if (!readMaterial(newMat, line, more, fd)) {
return false;
}
outScene.materialLibrary[tokens[1]] = newMat;
} else if (tokens[0][0] == '#') {
// Just a comment
} else {
Log(LL_Error, "Unexpected token");
}
}
}
}
return true;
});
}
bool readObject(Object& obj, std::string& line, bool& more, DataSource& fd)
{
std::vector<std::string> tokens = Utilities::split(line, ' ');
obj.objectName = tokens[1];
while (fd.getNextLine(line)) {
std::vector<std::string> tokens = Utilities::split(line, ' ');
if (tokens.size()) {
if (tokens[0] == "usemtl") {
obj.materialName = tokens[1];
} else if (tokens[0] == "v") {
//Log(LL_Error, "doing vert");
if (tokens.size() != 4) {
Log(LL_Error, "bad vert size: %li", tokens.size());
return false;
}
obj.vertices.emplace_back(atof(tokens[1].c_str()), atof(tokens[2].c_str()), atof(tokens[3].c_str()));
} else if (tokens[0] == "vn") {
//Log(LL_Error, "doing vn");
if (tokens.size() != 4) {
Log(LL_Error, "bad normal size: %li", tokens.size());
return false;
}
obj.normals.emplace_back(atof(tokens[1].c_str()), atof(tokens[2].c_str()), atof(tokens[3].c_str()));
} else if (tokens[0] == "vt") {
//Log(LL_Error, "doing vt");
if (tokens.size() != 3) {
Log(LL_Error, "bad uv size: %li", tokens.size());
return false;
}
obj.uvs.emplace_back(atof(tokens[1].c_str()), atof(tokens[2].c_str()));
} else if (tokens[0] == "f") {
obj.faces.emplace_back();
Face &face = obj.faces.back();
for (size_t i = 1; i < tokens.size(); i++) {
std::vector<std::string> faceVert = Utilities::split(tokens[i], '/');
face.indices.emplace_back();
Face::FaceDetail& d = face.indices.back();
for (size_t i = 0; i < 3; i++)
if (faceVert.size() >= (i+1))
d.asArray[i] = atoi(faceVert[i].c_str());
}
} else if (tokens[0] == "s") {
if (tokens.size() != 2) {
Log(LL_Error, "bad smoothing size");
return false;
}
if (tokens[1] == "off" || tokens[1] == "0")
obj.smoothing = false;
else if (tokens[1] == "on" || tokens[1] == "1")
obj.smoothing = true;
else {
Log(LL_Error, "bad smoothing value");
return false;
}
} else if (tokens[0][0] == '#') {
// Just a comment, skip
} else {
Log(LL_Error, "other token: %s", tokens[0].c_str());
// Not our token, return from here with more set to true
more = true;
return true;
}
}
}
return true;
}
std::pair<std::string,std::string> splitFilename(const std::string& filePath)
{
std::size_t found = filePath.find_last_of("/\\");
return std::make_pair(filePath.substr(0,found), filePath.substr(found+1));
}
bool readObjFile(ObjScene& outScene, const char* fileName, std::function<void(bool okay)> callback)
{
std::string path = splitFilename(fileName).first;
Loader fd;
fd.open(fileName, [path, &outScene, callback](bool okay, DataSource& fd)->bool
{
if (!okay) {
callback(false);
return false;
}
std::string line;
while (fd.getNextLine(line)) {
bool more = true;
while (more) {
more = false;
std::vector<std::string> tokens = Utilities::split(line, ' ');
if (tokens.size()) {
if (tokens[0] == "mtllib") {
// std::string mtlFile = path + "/" + tokens[1];
std::string mtlFile = tokens[1];
Log(LL_Error, "reading material library %s", mtlFile.c_str());
readMaterialLibrary(outScene, mtlFile.c_str());
//Log(LL_Error, "OBJ", "error reading material library");
} else if (tokens[0] == "o") {
Log(LL_Error, "reading object %s", tokens[1].c_str());
outScene.objects.emplace_back();
if (!readObject(outScene.objects.back(), line, more, fd)) {
Log(LL_Error, "error while reading object");
callback(false);
return false;
}
// The memory could move, so pointer to vector item in map is bad idea
//outScene.objectMap[tokens[1]] = &outScene.objects.back();
} else if (tokens[0][0] == '#') {
// Just a comment
} else {
Log(LL_Error, "Unexpected token");
}
}
}
}
callback(true);
return true;
});
return true;
}
#ifdef TEST
int main(int argc, char* argv[])
{
if ( argc >= 2) {
Log(LL_Debug, "using argv[1] == %s as input file", argv[1]);
ObjScene scene;
if (readObjFile(scene, argv[1]))
Log(LL_Debug, "success");
else
Log(LL_Debug, "failed");
}
return 0;
}
#endif