// obj2anib: a tool that turns Wavefront .obj into animated vertex/index binary files.
//
// Not endian-proof! Assume little endian because that's what OpenGL uses.
//
// 64-bit file header
// ANIBnnnn (where nnnn is int32_t number of frames)
//
// 64-bit frame header
// Where x is (sizeof(AnibVertex) * num_verts) and y is (sizeof(Index) * num_inds)
// Layout: [4 bytes num_verts][4 bytes num_inds][x bytes verts][y bytes inds]
//
// Compile with something like:
// g++ -std=c++14 -Wall -Wextra -Wfatal-errors -O3 -o obj2bin obj2bin.cpp
#include <stdint.h>
#include <stdio.h>
#include <string.h>
#include <cmath>
#include <map>
#include <string>
#include <utility>
#include <vector>
struct AnibVertex {
float Position[3];
float Normal[3];
float UV[2];
};
typedef uint32_t Index;
void NormalizeVertices( std::vector<AnibVertex> &verts ){
for( size_t i = 0; i < verts.size(); i++ ){
float* n = verts[i].Normal;
float l = std::sqrt( n[0] * n[0] + n[1] * n[1] + n[2] * n[2] );
if( l != 0.0f && l != 1.0f ){
verts[i].Normal[0] /= l;
verts[i].Normal[1] /= l;
verts[i].Normal[2] /= l;
}
}
}
// This function takes a Wavefront OBJ as input and appends a binary block to outVec.
// The OBJ must be triangulated before input. This function cannot deal with quads.
void AppendOBJ( const std::string &inFile, std::vector<unsigned char> &outVec ){
std::vector<AnibVertex> verts;
std::vector<Index> inds;
FILE *file = fopen( inFile.c_str(), "rb" );
if( !file ){
fprintf( stderr, "Failed to open %s\n", inFile.c_str() );
return;
}
std::string text = "";
char buf[4096];
while( size_t len = fread( buf, 1, sizeof( buf ), file ) ){
text += std::string( buf, len );
}
fclose( file );
size_t line = 0;
struct float3 { float x, y, z; };
struct float2 { float u, v; };
struct obdex { int v1, vt1, vn1, v2, vt2, vn2, v3, vt3, vn3; };
std::vector<float3> obj_v, obj_vn;
std::vector<float2> obj_vt;
std::vector<obdex> obj_f;
size_t head = 0, tail = 0;
do{
line++;
tail = text.find_first_of( "\n", head );
if( tail == std::string::npos ) tail = text.size();
if( text[head] != '#' ){
std::string ln = text.substr( head, tail - head );
std::vector<std::string> s;
size_t i1 = 0, i2 = 0;
do{
i2 = ln.find_first_of( " \r/", i1 );
if( i2 == std::string::npos ) i2 = ln.size();
std::string value = ln.substr( i1, i2 - i1 );
if( value.length() > 0 ) s.push_back( value );
i1 = i2 + 1;
}while( i1 < ln.size() );
if( s.size() > 0 ){
if( s[0] == "v" && s.size() >= 4 ){
obj_v.push_back( { std::stof(s[1]), std::stof(s[2]), std::stof(s[3]) } );
}else if( s[0] == "vt" && s.size() >= 3 ){
// Flip the vertical texture coordinate.
obj_vt.push_back( { std::stof(s[1]), 1.0f - std::stof(s[2]) } );
}else if( s[0] == "vn" && s.size() >= 4 ){
obj_vn.push_back( { std::stof(s[1]), std::stof(s[2]), std::stof(s[3]) } );
}else if( s[0] == "f" && s.size() >= 10 ){
obj_f.push_back( {
std::stoi(s[1]),
std::stoi(s[2]),
std::stoi(s[3]),
std::stoi(s[4]),
std::stoi(s[5]),
std::stoi(s[6]),
std::stoi(s[7]),
std::stoi(s[8]),
std::stoi(s[9])
} );
}
}
}
head = tail + 1;
}while( head < text.size() );
// Reduce the mesh to an efficient vertex/index representation.
// https://community.khronos.org/t/obj-texture-coordinates-arent-mapped-correctly/69926
std::map<std::string,std::pair<AnibVertex,Index>> vertmap;
Index idx = 0, next_idx = 0;
for( obdex f : obj_f ){
const int lookup_v[] = { f.v1, f.v2, f.v3 };
const int lookup_vt[] = { f.vt1, f.vt2, f.vt3 };
const int lookup_vn[] = { f.vn1, f.vn2, f.vn3 };
for( int i = 0; i < 3; i++ ){
int v = lookup_v[i] - 1;
int vt = lookup_vt[i] - 1;
int vn = lookup_vn[i] - 1;
int key[] = { v, vt, vn };
std::string skey( (const char*)key, sizeof(int) * 3 );
if( vertmap.find( skey ) == vertmap.end() ){
// create vertex from position/normal/texcoord
// store new vertex in vertices with key and next_vertex
vertmap[skey] = { {
{ obj_v[v].x, obj_v[v].y, obj_v[v].z },
{ obj_vn[vn].x, obj_vn[vn].y, obj_vn[vn].z },
{ obj_vt[vt].u, obj_vt[vt].v }
}, next_idx };
idx = next_idx;
next_idx++;
}else{
idx = vertmap[skey].second;
}
// Save idx in index buffer.
inds.push_back( idx );
}
}
// Order verts by index number.
verts.resize( vertmap.size() );
for( const auto &el : vertmap ){
verts[el.second.second] = el.second.first;
}
NormalizeVertices( verts );
// Write verts and inds to a buffer.
size_t memsize = sizeof(int32_t) * 2 + sizeof(AnibVertex) * verts.size() + sizeof(Index) * inds.size();
unsigned char *memory = (unsigned char*)malloc( memsize );
((int32_t*)memory)[0] = (int32_t)verts.size();
((int32_t*)memory)[1] = (int32_t)inds.size();
memcpy( memory + sizeof(int32_t) * 2, verts.data(), sizeof(AnibVertex) * verts.size() );
memcpy( memory + sizeof(int32_t) * 2 + sizeof(AnibVertex) * verts.size(), inds.data(), sizeof(Index) * inds.size() );
// Write the buffer to outVec.
size_t vecEnd = outVec.size();
outVec.resize(vecEnd + memsize);
memcpy((void*)&(outVec.data()[vecEnd]), memory, memsize);
free( memory );
}
int main( int argc, char* argv[] ){
if( argc > 1 ){
std::vector<unsigned char> outVec;
outVec.resize(8);
// Write header.
char header[8] = "ANIB";
((int32_t*)header)[1] = argc - 1;
memcpy(outVec.data(), header, 8);
// Write frames.
for( int i = 1; i < argc; i++ ){
std::string inFile = argv[i];
AppendOBJ(inFile, outVec);
}
// Write data to a file.
std::string outFile = "out.anib";
FILE *file = fopen(outFile.c_str(), "wb");
if(!file){
fprintf(stderr, "Failed to open %s for writing\n", outFile.c_str());
return 2;
}
fwrite(outVec.data(), outVec.size(), 1, file);
fclose(file);
printf("%s\n", outFile.c_str());
return 0;
}else{
fprintf( stderr, "Expects 1 or more input files.\n" );
if( argc == 1 ) printf( "Example:\n%s *.obj\n", argv[0] );
return 1;
}
}