From cd6d2340a6fcbd2376aad291081ae5d667bd3317 Mon Sep 17 00:00:00 2001 From: 3gg <3gg@shellblade.net> Date: Sat, 31 Jan 2026 16:12:21 -0800 Subject: Add a ground. Add support for multiple objects and materials in MDL --- include/model.h | 60 +++++++++++++++++-- src/main.c | 149 ++++++++++++++++++++++++++++++++++++---------- tools/ase/main.c | 175 ++++++++++++++++++++++++++++++++++++++++++------------- 3 files changed, 308 insertions(+), 76 deletions(-) diff --git a/include/model.h b/include/model.h index 0730809..10c9cdf 100644 --- a/include/model.h +++ b/include/model.h @@ -1,9 +1,15 @@ #pragma once +#include #include #include -constexpr size_t ModelPathLen = 256; +constexpr size_t ModelPathLen = 256; +constexpr size_t ModelNameLen = 64; +constexpr size_t ModelMaxObjects = 256; // uint8_t +constexpr size_t ModelMaxMaterials = 256; // uint8_t +constexpr size_t ModelMaxVerts = 4294967296; // uint32_t +constexpr size_t ModelMaxFaces = 4294967296; // uint32_t typedef uint16_t mdIdx; typedef struct mdVert { mdIdx position, texcoord, normal; } mdVert; @@ -11,9 +17,18 @@ typedef struct mdTri { mdVert v0, v1, v2; } mdTri; typedef struct mdVec2 { float x, y; } mdVec2; typedef struct mdVec3 { float x, y, z; } mdVec3; -typedef struct Material { +typedef struct ModelObject { + uint32_t offset; // FlatModel: offset into indices. IndexedModel: offset into tris. + uint32_t count; // FloatModel: number of indices. IndexedModel: number of tris. + uint8_t material; // Material index. + uint8_t pad[3]; + char name[ModelNameLen]; +} ModelObject; + +typedef struct ModelMaterial { + char name[ModelNameLen]; char diffuseTexture[ModelPathLen]; -} Material; +} ModelMaterial; // Every three indices form a triangle, and each index indexes all attribute // arrays simultaneously. This is best for a fast, linear-scan rendering. @@ -28,6 +43,8 @@ typedef struct FlatModel { uint32_t offsetTexcoords; uint32_t offsetNormals; /* + [objects] -- numObjects Object + [materials] -- numMaterials Material [indices] -- numIdxs mdIdx [positions] -- numVerts mdVec3 [texcoords] -- numVerts mdVec2 @@ -53,6 +70,8 @@ typedef struct IndexedModel { uint32_t offsetTexcoords; uint32_t offsetNormals; /* + [objects] -- numObjects Object + [materials] -- numMaterials Material [triangles] -- numTris mdTri [positions] -- numPositions mdVec3 [texcoords] -- numTexcoords mdVec2 @@ -68,9 +87,42 @@ typedef enum ModelType { typedef struct Model { uint32_t type; - Material material; + // Counts. + uint8_t numObjects; + uint8_t numMaterials; + uint8_t pad[2]; + // Offsets. + uint32_t offsetObjects; + uint32_t offsetMaterials; + // Model details. union { FlatModel flat; IndexedModel indexed; }; } Model; + +static inline const ModelObject* modelObjects(const Model* model) { + assert(model); + switch (model->type) { + case ModelTypeIndexed: + return (const ModelObject*)(model->indexed.data + model->offsetObjects); + case ModelTypeFlat: + return (const ModelObject*)(model->flat.data + model->offsetObjects); + default: + assert(false); + break; + } +} + +static inline const ModelMaterial* modelMaterials(const Model* model) { + assert(model); + switch (model->type) { + case ModelTypeIndexed: + return (const ModelMaterial*)(model->indexed.data + model->offsetMaterials); + case ModelTypeFlat: + return (const ModelMaterial*)(model->flat.data + model->offsetMaterials); + default: + assert(false); + break; + } +} diff --git a/src/main.c b/src/main.c index dcd0335..225af5f 100644 --- a/src/main.c +++ b/src/main.c @@ -31,9 +31,13 @@ static constexpr R Fovy = (R)(90 * TO_RAD); static constexpr R Near = 0.1f; static constexpr R Far = 100.0f; -#define DEBUG_EVENT_LOOP 1 +static constexpr size_t MaxTextures = 256; -#ifdef DEBUG_EVENT_LOOP +static const char* StateFile = "game.bin"; + +#define DEBUG_EVENT_LOOP 0 + +#if DEBUG_EVENT_LOOP #define EVENT_LOOP_PRINT printf #else #define EVENT_LOOP_PRINT(...) @@ -68,13 +72,55 @@ typedef struct State { SDL_Window* window; void* gfx_mem; swgfx* gfx; - Model* model; - sgImage texture; Camera camera; CameraController camera_controller; Uint64 last_tick; + Model* model; + size_t numTextures; + sgImage textures[MaxTextures]; } State; +static bool StateWrite(const State* state, FILE* file) { + assert(state); + assert(file); + return fwrite(&state->camera, sizeof(state->camera), 1, file) == 1; +} + +static bool StateRead(FILE* file, State* state) { + assert(file); + assert(state); + Camera camera; + if (fread(&camera, sizeof(camera), 1, file) != 1) { + return false; + } + state->camera = camera; + return true; +} + +static bool StateSave(const State* state, const char* path) { + assert(state); + assert(path); + FILE* file = fopen(path, "wb"); + if (!file) { + return false; + } + const bool result = StateWrite(state, file); + fclose(file); + return result; +} + +static bool StateLoad(const char* path, State* state) { + assert(path); + assert(state); + FILE* file = fopen(path, "rb"); + if (!file) { + return false; + } + const bool result = StateRead(file, state); + fclose(file); + return result; +} + static sgVec3 SgVec3FromMathVec3(vec3 v) { return (sgVec3){v.x, v.y, v.z}; } @@ -82,8 +128,9 @@ static sgVec3 SgVec3FromMathVec3(vec3 v) { static CameraCommand CameraCommandFromInput( const bool* keyboard_state, const SDL_MouseButtonFlags mouse_flags) { assert(keyboard_state); - if (keyboard_state[SDL_SCANCODE_W]) { - printf("W: %d\n", keyboard_state[SDL_SCANCODE_W]); + // Control is used for save/load/quit, etc. + if (keyboard_state[SDL_SCANCODE_LCTRL]) { + return (CameraCommand){}; } return (CameraCommand){ .CameraMoveLeft = keyboard_state[SDL_SCANCODE_A], @@ -146,22 +193,34 @@ static bool Update(State* state, R dt) { return true; } -static void RenderIndexedModel(swgfx* gfx, const IndexedModel* model) { +static void RenderIndexedModel(swgfx* gfx, const IndexedModel* model, const ModelObject* object) { assert(gfx); assert(model); - const sgTriIdx* tris = (const sgTriIdx*)(model->data + model->offsetTris); + assert(object); + assert((object->offset + object->count) <= model->numTris); + const sgTriIdx* tris = (const sgTriIdx*)(model->data + model->offsetTris) + object->offset; const sgVec3* positions = (const sgVec3*) (model->data + model->offsetPositions); - const sgVec2* texcoords = (const sgVec2*)(model->data + model->offsetTexcoords); - sgTrianglesIndexedNonUniform(gfx, model->numTris, tris, positions, texcoords); + const sgVec2* texcoords = (const sgVec2*) (model->data + model->offsetTexcoords); + sgTrianglesIndexedNonUniform(gfx, object->count, tris, positions, texcoords); } -static void RenderModel(swgfx* gfx, const Model* model) { +static void RenderModel(swgfx* gfx, const sgImage* textures, const Model* model) { assert(gfx); + assert(textures); assert(model); - switch (model->type) { - case ModelTypeIndexed: RenderIndexedModel(gfx, &model->indexed); break; - case ModelTypeFlat: /* TODO: Render flat models. */ break; - default: assert(false); break; + const ModelObject* objects = modelObjects(model); + for (size_t i = 0; i < model->numObjects; ++i) { + const ModelObject* object = &objects[i]; + // TODO: This indexing into the textures array assumes that we have loaded a + // single model. Generalize later. + assert((size_t)object->material < MaxTextures); + const sgImage* texture = &textures[object->material]; + sgTexture(gfx, texture); + switch (model->type) { + case ModelTypeIndexed: RenderIndexedModel(gfx, &model->indexed, object); break; + case ModelTypeFlat: /* TODO: Render flat models. */ break; + default: assert(false); break; + } } } @@ -225,15 +284,19 @@ static bool Render(State* state) { sgModelId(state->gfx); sgView(state->gfx, SgVec3FromMathVec3(cam->spatial.p), SgVec3FromMathVec3(cam->spatial.f)); sgPerspective(state->gfx, cam->fovy, cam->aspect, cam->near, cam->far); - sgTexture(state->gfx, &state->texture); - RenderModel(state->gfx, state->model); - /*sgIdx indices[3] = {0, 1, 2}; - sgVec3 positions[3] = { + RenderModel(state->gfx, state->textures, state->model); + /*const sgIdx indices[3] = {0, 1, 2}; + const sgVec3 positions[3] = { (sgVec3){0, 0, 0}, (sgVec3){5, 2, 0}, (sgVec3){8, 8, 0}, }; - sgTrianglesIndexed(state->gfx, 3, indices, positions);*/ + const sgVec2 texcoords[3] = { + (sgVec2){0, 0}, + (sgVec2){0.5, 0.5}, + (sgVec2){1.0, 1.0}, + }; + sgTrianglesIndexed(state->gfx, 3, indices, positions, texcoords);*/ sgPresent(state->gfx, WindowDims, window_surface->pixels); if (!SDL_UpdateWindowSurface(state->window)) { @@ -284,23 +347,28 @@ static bool Initialize(State* state) { return false; } - const char* model_path = "/home/jeanne/blender/box_textured.mdl"; + const char* model_path = "/home/jeanne/blender/boxout.mdl"; if (!(state->model = read_file(model_path))) { fprintf(stderr, "Failed to load model: [%s]\n", model_path); return false; } - if (state->model->material.diffuseTexture[0] != 0) { + if ((size_t)state->model->numMaterials > MaxTextures) { + fprintf(stderr, "Model material count is larger than max textures, increase limit\n"); + return false; + } + const ModelMaterial* materials = modelMaterials(state->model); + for (size_t i = 0; i < state->model->numMaterials; ++i) { + const ModelMaterial* material = &materials[i]; // TODO: When doing lighting, need to gamma-correct here. - sgImage texture = {0}; + sgImage* texture = &state->textures[state->numTextures++]; int channels = 0; constexpr int desired_channels = 4; - texture.pixels = (sgPixel*)stbi_load(state->model->material.diffuseTexture, &texture.width, &texture.height, &channels, desired_channels); - if (!texture.pixels) { - fprintf(stderr, "Failed to read texture: [%s]\n", state->model->material.diffuseTexture); + texture->pixels = (sgPixel*)stbi_load(material->diffuseTexture, &texture->width, &texture->height, &channels, desired_channels); + if (!texture->pixels) { + fprintf(stderr, "Failed to read texture: [%s]\n", material->diffuseTexture); return false; } assert(channels == desired_channels); - state->texture = texture; } Camera* camera = &state->camera; @@ -324,16 +392,19 @@ static bool Initialize(State* state) { static void Shutdown(State* state) { assert(state); - if (state->texture.pixels) { - free(state->texture.pixels); - state->texture = (sgImage){0}; + for (size_t i = 0; i < state->numTextures; ++i) { + sgImage* texture = &state->textures[i]; + if (texture->pixels) { + free(texture->pixels); + *texture = (sgImage){0}; + } } if (state->model) { free(state->model); state->model = nullptr; } - + if (state->gfx) { sgDel(&state->gfx); } @@ -412,6 +483,22 @@ int main() { case SDLK_D: running = false; break; + // Save state. + case SDLK_S: + if (StateSave(&state, StateFile)) { + fprintf(stderr, "State saved\n"); + } else { + fprintf(stderr, "Failed to save state\n"); + } + break; + // Load state. + case SDLK_L: + if (StateLoad(StateFile, &state)) { + fprintf(stderr, "State loaded\n"); + } else { + fprintf(stderr, "Failed to load state\n"); + } + break; default: break; } diff --git a/tools/ase/main.c b/tools/ase/main.c index 30c6812..5bffed0 100644 --- a/tools/ase/main.c +++ b/tools/ase/main.c @@ -163,25 +163,36 @@ static inline bool IsLexeme(const Lexer* lexer, const char* expected) { } // Reasonable limits for the parser implementation. -// The model spec does not impose a limit on tris, but vertex attributes are -// indexed by uint16_t. +// The model spec does not impose a limit on tris or materials. Vertex +// attributes are indexed by uint32_t. #define MAX_TRIS 65536 #define MAX_VERTS 65536 +typedef struct ObjectData { + ModelObject modelObject; + char materialName[ModelNameLen]; // For linking objects and materials. +} ObjectData; + // 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; - Material material; - char mtl_file [PATH_MAX]; - mdTri tris [MAX_TRIS]; - mdVec3 positions[MAX_VERTS]; - mdVec3 normals [MAX_VERTS]; - mdVec2 texcoords[MAX_VERTS]; + uint32_t numTris; + uint32_t numPositions; + uint32_t numNormals; + uint32_t numTexcoords; + uint32_t numObjects; + ObjectData objects[ModelMaxObjects]; + char mtl_file [PATH_MAX]; + mdTri tris [MAX_TRIS]; + mdVec3 positions[MAX_VERTS]; + mdVec3 normals [MAX_VERTS]; + mdVec2 texcoords[MAX_VERTS]; } ModelData; +typedef struct MaterialsData { + uint32_t numMaterials; + ModelMaterial materials[ModelMaxMaterials]; +} MaterialsData; + #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("~ "); } @@ -197,6 +208,12 @@ typedef struct ModelData { static bool ParseObj(Lexer* lexer, ModelData* modelData) { assert(lexer); assert(modelData); +#define PRINT_FINALIZED_OBJECT() { \ + assert(curObject < ModelMaxObjects); \ + ModelObject* const object = &modelData->objects[curObject].modelObject; \ + printf("> Finalized: %s (tris offset: %u, count: %u)\n", object->name, object->offset, object->count); \ + } + size_t curObject = 0; bool consumeNext = true; for (;;) { if (consumeNext) { @@ -209,8 +226,22 @@ static bool ParseObj(Lexer* lexer, ModelData* modelData) { NEXT_STRING(modelData->mtl_file); PRINT("> material: "); } else if (LEX("o")) { - NEXT_LEXEME(); // object name + // Print line for finalized previous object. + if (modelData->numObjects > 0) { + PRINT_FINALIZED_OBJECT(); + } + // Next object. + modelData->numObjects++; + curObject = modelData->numObjects - 1; + assert(curObject < ModelMaxObjects); + ModelObject* const object = &modelData->objects[curObject].modelObject; + object->offset = modelData->numTris; + NEXT_STRING(object->name); PRINT("> object: "); + } else if (LEX("usemtl")) { + const size_t curObject = modelData->numObjects - 1; // Object comes before material. + assert(curObject < ModelMaxObjects); + NEXT_STRING(modelData->objects[curObject].materialName); } else if (LEX("v")) { float x, y, z; NEXT_FLOAT(&x); @@ -267,28 +298,65 @@ static bool ParseObj(Lexer* lexer, ModelData* modelData) { modelData->tris[modelData->numTris++] = (mdTri){vertices[0], vertices[2], vertices[3]}; } + // Increase current object's triangle count. + assert(curObject < ModelMaxObjects); + ModelObject* const object = &modelData->objects[curObject].modelObject; + object->count += (numVerts == 3) ? 1 : 2; } } + if (modelData->numObjects > 0) { + PRINT_FINALIZED_OBJECT(); // Print line for the last finalized object. + } return true; } -static bool ParseMtl(Lexer* lexer, Material* material) { +static bool ParseMtl(Lexer* lexer, MaterialsData* materialData) { assert(lexer); - assert(material); + assert(materialData); + size_t cur = (size_t)-1; for (;;) { NEXT_LEXEME(); if (LEX("newmtl")) { - NEXT_LEXEME(); // Material name. + cur++; + assert(cur < ModelMaxMaterials); + NEXT_STRING(materialData->materials[cur].name); + materialData->numMaterials++; PRINT("> material: "); } else if (LEX("map_Kd")) { - READ_LINE(material->diffuseTexture); + assert(cur < ModelMaxMaterials); + READ_LINE(materialData->materials[cur].diffuseTexture); } } return true; } -static bool WriteModelFile(const ModelData* modelData, const char* path) { +static bool LinkMaterials(ModelData* modelData, MaterialsData* materialsData) { + assert(modelData); + assert(materialsData); + bool all_linked = true; + for (size_t i = 0; i < modelData->numObjects; ++i) { + bool found = false; + ObjectData* object = &modelData->objects[i]; + for (size_t j = 0; !found && (j < materialsData->numMaterials); ++j) { + if (strcmp(object->materialName, materialsData->materials[j].name) == 0) { + object->modelObject.material = j; + found = true; + } + } + all_linked = all_linked && found; + } + return all_linked; +} + +static void AssertOffset(FILE* file, size_t offset) { + const long int pos = ftell(file); + constexpr size_t headerSize = sizeof(Model); + assert((headerSize + offset) == (size_t)pos); +} + +static bool WriteModelFile(const ModelData* modelData, const MaterialsData* materialsData, const char* path) { assert(modelData); + assert(materialsData); assert(path); bool success = false; @@ -297,16 +365,19 @@ static bool WriteModelFile(const ModelData* modelData, const char* path) { // Fill the Model header. model.type = ModelTypeIndexed; + model.numObjects = modelData->numObjects; + model.numMaterials = materialsData->numMaterials; + model.offsetObjects = 0; // 'data' member. + model.offsetMaterials = model.offsetObjects + (modelData->numObjects * sizeof(ModelObject)); IndexedModel* indexed = &model.indexed; 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)); - memcpy(&model.material, &modelData->material, sizeof(Material)); + indexed->offsetTris = model.offsetMaterials + (materialsData->numMaterials * sizeof(ModelMaterial)); + 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); @@ -317,22 +388,42 @@ static bool WriteModelFile(const ModelData* modelData, const char* path) { fprintf(stderr, "Failed writing Model header\n"); goto cleanup; } + // Objects. + AssertOffset(file, model.offsetObjects); + for (size_t i = 0; i < modelData->numObjects; ++i) { + const ObjectData* data = &modelData->objects[i]; + const ModelObject* object = &data->modelObject; + if (fwrite(object, sizeof(ModelObject), 1, file) != 1) { + fprintf(stderr, "Failed writing object\n"); + goto cleanup; + } + } + // Materials. + AssertOffset(file, model.offsetMaterials); + if (fwrite(materialsData->materials, sizeof(ModelMaterial), materialsData->numMaterials, file) != materialsData->numMaterials) { + fprintf(stderr, "Failed writing materials\n"); + goto cleanup; + } // Tris. + AssertOffset(file, indexed->offsetTris); if (fwrite(&modelData->tris, sizeof(mdTri), modelData->numTris, file) != modelData->numTris) { fprintf(stderr, "Failed writing triangles\n"); goto cleanup; } // Positions. + AssertOffset(file, indexed->offsetPositions); if (fwrite(&modelData->positions, sizeof(mdVec3), modelData->numPositions, file) != modelData->numPositions) { fprintf(stderr, "Failed writing positions\n"); goto cleanup; } // Normals. + AssertOffset(file, indexed->offsetNormals); if (fwrite(&modelData->normals, sizeof(mdVec3), modelData->numNormals, file) != modelData->numNormals) { fprintf(stderr, "Failed writing normals\n"); goto cleanup; } // Texcoords. + AssertOffset(file, indexed->offsetTexcoords); if (fwrite(&modelData->texcoords, sizeof(mdVec2), modelData->numTexcoords, file) != modelData->numTexcoords) { fprintf(stderr, "Failed writing texture coordinates\n");goto cleanup; } @@ -404,48 +495,50 @@ int main(int argc, const char** argv) { 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? + bool success = false; + uint8_t* fileData = nullptr; + size_t dataSize = 0; + ModelData modelData = {0}; + MaterialsData materialsData = {0}; + Lexer lexer = {0}; + if (!ReadFile(filePath, &fileData, &dataSize)) { - goto cleanup; - } - if ((modelData = calloc(1, sizeof(ModelData))) == nullptr) { + fprintf(stderr, "Failed to read model file\n"); goto cleanup; } LexerMake((const char*)fileData, dataSize, &lexer); - if (!ParseObj(&lexer, modelData)) { + if (!ParseObj(&lexer, &modelData)) { + fprintf(stderr, "Failed to parse OBJ\n"); goto cleanup; } - if (modelData->mtl_file[0] != 0) { + if (modelData.mtl_file[0] != 0) { free(fileData); fileData = nullptr; char dir[PATH_MAX]; char mtl[PATH_MAX]; GetParentDir(filePath, dir); - PathConcat(dir, modelData->mtl_file, mtl); + PathConcat(dir, modelData.mtl_file, mtl); if (!ReadFile(mtl, &fileData, &dataSize)) { + fprintf(stderr, "Failed to read MTL file\n"); goto cleanup; } LexerMake((const char*)fileData, dataSize, &lexer); - if (!ParseMtl(&lexer, &modelData->material)) { + if (!ParseMtl(&lexer, &materialsData)) { + fprintf(stderr, "Failed to parse MTL file\n"); + goto cleanup; + } + if (!LinkMaterials(&modelData, &materialsData)) { + fprintf(stderr, "Failed to link materials\n"); goto cleanup; } } - if (!WriteModelFile(modelData, outPath)) { + if (!WriteModelFile(&modelData, &materialsData, outPath)) { goto cleanup; } success = true; cleanup: - if (modelData) { - free(modelData); - } if (fileData) { free(fileData); } -- cgit v1.2.3