From 5a079a2d114f96d4847d1ee305d5b7c16eeec50e Mon Sep 17 00:00:00 2001 From: 3gg <3gg@shellblade.net> Date: Sat, 27 Dec 2025 12:03:39 -0800 Subject: Initial commit --- tools/ase/main.c | 376 +++++++++++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 376 insertions(+) create mode 100644 tools/ase/main.c (limited to 'tools/ase/main.c') diff --git a/tools/ase/main.c b/tools/ase/main.c new file mode 100644 index 0000000..6ba770a --- /dev/null +++ b/tools/ase/main.c @@ -0,0 +1,376 @@ +#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; +} -- cgit v1.2.3