#include #include #include #include #include #include #include #include typedef struct Lexeme { const char* str; size_t length; } Lexeme; typedef struct Lexer { const char* buffer; // Input buffer. size_t size; // Buffer size. size_t next; // Points to the next, unconsumed character. Lexeme lexeme; // Current lexeme. } Lexer; static void LexerMake(const char* data, size_t size, Lexer* lexer) { assert(data); assert(lexer); lexer->buffer = data; lexer->size = size; } static bool End(const Lexer* lexer) { assert(lexer); assert(lexer->next <= lexer->size); return lexer->next == lexer->size; } static bool HasNext(const Lexer* lexer) { assert(lexer); return lexer->next < lexer->size; } static void Advance(Lexer* lexer) { assert(lexer); assert(HasNext(lexer)); lexer->next++; } static char Next(const Lexer* lexer) { assert(lexer); assert(HasNext(lexer)); return lexer->buffer[lexer->next]; } static const char* NextPtr(const Lexer* lexer) { assert(lexer); assert(HasNext(lexer)); return &lexer->buffer[lexer->next]; } // Get the pointer to the next character, or one past the last character of the // buffer (the "end" of the buffer). static const char* NextOrEndPtr(const Lexer* lexer) { assert(lexer); assert(HasNext(lexer) || End(lexer)); return &lexer->buffer[lexer->next]; } static void SkipChar(Lexer* lexer) { assert(lexer); if (HasNext(lexer)) { Advance(lexer); } } static void SkipLine(Lexer* lexer) { assert(lexer); // Advance until we find a newline character. while (HasNext(lexer) && (Next(lexer) != '\n')) Advance(lexer); // Skip the newline character. SkipChar(lexer); } static bool IsWhiteSpace(char c) { return (c == ' ') || (c == '\n'); } static void SkipWhiteSpace(Lexer* lexer) { assert(lexer); while (HasNext(lexer) && IsWhiteSpace(Next(lexer))) Advance(lexer); } static void ReadUntilWhiteSpace(Lexer* lexer) { assert(lexer); while (HasNext(lexer) && !IsWhiteSpace(Next(lexer))) Advance(lexer); } static bool NextLexeme(Lexer* lexer) { assert(lexer); SkipWhiteSpace(lexer); if (HasNext(lexer)) { lexer->lexeme.str = NextPtr(lexer); ReadUntilWhiteSpace(lexer); // Find the end of the lexeme. lexer->lexeme.length = NextOrEndPtr(lexer) - lexer->lexeme.str; } else { lexer->lexeme.str = nullptr; lexer->lexeme.length = 0; } return lexer->lexeme.length > 0; } static bool ParseFloat(const Lexeme* lex, float* out) { assert(lex); assert(out); assert(errno == 0); *out = (float)strtod(lex->str, nullptr); return errno == 0; } static inline bool IsLexeme(const Lexer* lexer, const char* expected) { assert(lexer); assert(expected); return strncmp(lexer->lexeme.str, expected, lexer->lexeme.length) == 0; } // Reasonable limits for the parser implementation. // The model spec does not impose a limit on tris, but vertex attributes are // indexed by uint16_t. #define MAX_TRIS 65536 #define MAX_VERTS 65536 // Temporary storage for model data. A Model can be outputted from this. typedef struct ModelData { uint32_t numTris; uint32_t numPositions; uint32_t numNormals; uint32_t numTexcoords; mdTri tris [MAX_TRIS]; mdVec3 positions[MAX_VERTS]; mdVec3 normals [MAX_VERTS]; mdVec2 texcoords[MAX_VERTS]; } ModelData; static bool ParseObj(Lexer* lexer, ModelData* modelData) { assert(lexer); assert(modelData); #define PRINT(STR) printf("%s%.*s\n", STR, (int)lexer->lexeme.length, lexer->lexeme.str) #define LEX(STR) IsLexeme(lexer, STR) #define NEXT_LEXEME() { if (!NextLexeme(lexer)) break; else PRINT("~ "); } #define NEXT_FLOAT(PTR) { NEXT_LEXEME(); if (!ParseFloat(&lexer->lexeme, PTR)) break; } bool consumeNext = true; for (;;) { if (consumeNext) { NEXT_LEXEME(); } consumeNext = true; if (LEX("#")) { SkipLine(lexer); } else if (LEX("mtllib")) { NEXT_LEXEME(); // material file PRINT("> material: "); } else if (LEX("o")) { NEXT_LEXEME(); // object name PRINT("> object: "); } else if (LEX("v")) { float x, y, z; NEXT_FLOAT(&x); NEXT_FLOAT(&y); NEXT_FLOAT(&z); modelData->positions[modelData->numPositions++] = (mdVec3){x, y, z}; printf("> position: %.2f, %.2f, %.2f\n", x, y, z); } else if (LEX("vn")) { float x, y, z; NEXT_FLOAT(&x); NEXT_FLOAT(&y); NEXT_FLOAT(&z); modelData->normals[modelData->numNormals++] = (mdVec3){x, y, z}; printf("> normal: %.2f, %.2f, %.2f\n", x, y, z); } else if (LEX("vt")) { float s, t; NEXT_FLOAT(&s); NEXT_FLOAT(&t); modelData->texcoords[modelData->numTexcoords++] = (mdVec2){s, t}; printf("> texcoord: %.2f, %.2f\n", s, t); } else if (LEX("f")) { // Indices are 1-based. // Texcoord and normal are optional. mdVert vertices[4]; // Handling up to quads. int numVerts = 0; while (NextLexeme(lexer)) { int pos, tex, normal; if (sscanf(lexer->lexeme.str, "%d/%d/%d", &pos, &tex, &normal) == 3) { vertices[numVerts++] = (mdVert){pos-1, tex-1, normal-1}; printf("> vertex: %d/%d/%d\n", pos, tex, normal); } else if (sscanf(lexer->lexeme.str, "%d//%d", &pos, &normal) == 2) { vertices[numVerts++] = (mdVert){pos-1, -1, normal-1}; printf("> vertex: %d//%d\n", pos, normal); } else if (sscanf(lexer->lexeme.str, "%d/%d", &pos, &tex) == 2) { vertices[numVerts++] = (mdVert){pos-1, tex-1, -1}; printf("> vertex: %d/%d\n", pos, tex); } else if (sscanf(lexer->lexeme.str, "%d", &pos) == 1) { vertices[numVerts++] = (mdVert){pos-1, -1, -1}; printf("> vertex: %d\n", pos); } else { // Something past the face. consumeNext = false; break; } } // End of vertices for this face; output the model triangles. assert((numVerts == 3) || (numVerts == 4)); if (numVerts == 3) { modelData->tris[modelData->numTris++] = (mdTri){vertices[0], vertices[1], vertices[2]}; } else if (numVerts == 4) { // Triangulate the quad and output two triangles instead. modelData->tris[modelData->numTris++] = (mdTri){vertices[0], vertices[1], vertices[2]}; modelData->tris[modelData->numTris++] = (mdTri){vertices[0], vertices[2], vertices[3]}; } } } return true; } static bool WriteModelFile(const ModelData* modelData, const char* path) { assert(modelData); assert(path); bool success = false; FILE* file = nullptr; Model model = {0}; // Fill the Model header. IndexedModel* indexed = &model.indexed; model.type = ModelTypeIndexed; indexed->numTris = modelData->numTris; indexed->numPositions = modelData->numPositions; indexed->numNormals = modelData->numNormals; indexed->numTexcoords = modelData->numTexcoords; indexed->offsetTris = 0; // 'data' member. indexed->offsetPositions = indexed->offsetTris + (modelData->numTris * sizeof(mdTri)); indexed->offsetNormals = indexed->offsetPositions + (modelData->numPositions * sizeof(mdVec3)); indexed->offsetTexcoords = indexed->offsetNormals + (modelData->numNormals * sizeof(mdVec3)); if ((file = fopen(path, "wb")) == nullptr) { fprintf(stderr, "Failed opening output file for writing: %s\n", path); goto cleanup; } // Header. if (fwrite(&model, sizeof(model), 1, file) != 1) { fprintf(stderr, "Failed writing Model header\n"); goto cleanup; } // Tris. if (fwrite(&modelData->tris, sizeof(mdTri), modelData->numTris, file) != modelData->numTris) { fprintf(stderr, "Failed writing triangles\n"); goto cleanup; } // Positions. if (fwrite(&modelData->positions, sizeof(mdVec3), modelData->numPositions, file) != modelData->numPositions) { fprintf(stderr, "Failed writing positions\n"); goto cleanup; } // Normals. if (fwrite(&modelData->normals, sizeof(mdVec3), modelData->numNormals, file) != modelData->numNormals) { fprintf(stderr, "Failed writing normals\n"); goto cleanup; } // Texcoords. if (fwrite(&modelData->texcoords, sizeof(mdVec2), modelData->numTexcoords, file) != modelData->numTexcoords) { fprintf(stderr, "Failed writing texture coordinates\n");goto cleanup; } success = true; cleanup: if (file) { fclose(file); } return success; } static bool ReadFile(const char* path, uint8_t** outData, size_t* outSize) { assert(path); bool success = false; uint8_t* data = nullptr; FILE* file = nullptr; if ((file = fopen(path, "rb")) == nullptr) { goto cleanup; } if (fseek(file, 0, SEEK_END) != 0) { goto cleanup; } const size_t fileSize = ftell(file); if (fileSize == (size_t)(-1)) { goto cleanup; } // Allocate one extra byte so that text file data conveniently ends with null. const size_t size = fileSize + 1; if (fseek(file, 0, SEEK_SET) != 0) { goto cleanup; } if ((data = calloc(1, size)) == nullptr) { goto cleanup; } if (fread(data, fileSize, 1, file) != 1) { goto cleanup; } *outData = data; *outSize = size; success = true; cleanup: if (file) { fclose(file); } if (!success && (data != nullptr)) { free(data); } return success; } static void usage(const char* argv0) { fprintf(stderr, "Usage: %s [out.mdl]\n", argv0); fprintf(stderr, "\n"); fprintf(stderr, "Supported file formats:\n"); fprintf(stderr, " OBJ\n"); } int main(int argc, const char** argv) { if ((argc != 2) && (argc != 3)) { usage(argv[0]); return 1; } const char* filePath = argv[1]; const char* outPath = (argc > 2) ? argv[2] : "out.mdl"; bool success = false; uint8_t* fileData = nullptr; size_t dataSize = 0; ModelData* modelData = nullptr; Lexer lexer = {0}; // TODO: Map file to memory instead? if (!ReadFile(filePath, &fileData, &dataSize)) { goto cleanup; } if ((modelData = calloc(1, sizeof(ModelData))) == nullptr) { goto cleanup; } LexerMake((const char*)fileData, dataSize, &lexer); if (!ParseObj(&lexer, modelData)) { goto cleanup; } if (!WriteModelFile(modelData, outPath)) { goto cleanup; } success = true; cleanup: if (modelData) { free(modelData); } if (fileData) { free(fileData); } return success ? 0 : 1; }