From d02673e04a03aafe290de9051e9e1f31decf857a Mon Sep 17 00:00:00 2001 From: Dominic Masters Date: Fri, 10 Apr 2026 22:09:01 -0500 Subject: [PATCH] 3D OBJ loading --- .gitignore | 3 +- cmake/modules/Findfast_obj.cmake | 20 +++ cmake/targets/linux.cmake | 2 +- src/dusk/CMakeLists.txt | 9 ++ src/dusk/asset/loader/display/CMakeLists.txt | 1 + .../asset/loader/display/assetmeshloader.c | 122 ++++++++++++++++++ .../asset/loader/display/assetmeshloader.h | 27 ++++ src/dusk/engine/engine.c | 16 +-- .../entity/component/display/entitycamera.c | 36 ++++++ .../entity/component/display/entitycamera.h | 46 ++++++- 10 files changed, 269 insertions(+), 13 deletions(-) create mode 100644 cmake/modules/Findfast_obj.cmake create mode 100644 src/dusk/asset/loader/display/assetmeshloader.c create mode 100644 src/dusk/asset/loader/display/assetmeshloader.h diff --git a/.gitignore b/.gitignore index 5eeb76df..d29de0c7 100644 --- a/.gitignore +++ b/.gitignore @@ -104,4 +104,5 @@ yarn.lock .venv /build2 -/build* \ No newline at end of file +/build* +/assets/test \ No newline at end of file diff --git a/cmake/modules/Findfast_obj.cmake b/cmake/modules/Findfast_obj.cmake new file mode 100644 index 00000000..1517cb9c --- /dev/null +++ b/cmake/modules/Findfast_obj.cmake @@ -0,0 +1,20 @@ +# Copyright (c) 2026 Dominic Masters +# +# This software is released under the MIT License. +# https://opensource.org/licenses/MIT + +include(FetchContent) + +if(NOT TARGET fast_obj) + FetchContent_Declare( + fast_obj + GIT_REPOSITORY https://github.com/thisistherk/fast_obj.git + GIT_TAG master + ) + FetchContent_MakeAvailable(fast_obj) +endif() + +set(fast_obj_FOUND TRUE) +set(FAST_OBJ_INCLUDE_DIRS "${fast_obj_SOURCE_DIR}") +set(FAST_OBJ_LIBRARIES fast_obj) +mark_as_advanced(FAST_OBJ_INCLUDE_DIRS FAST_OBJ_LIBRARIES fast_obj_FOUND) diff --git a/cmake/targets/linux.cmake b/cmake/targets/linux.cmake index e83e87a4..40723585 100644 --- a/cmake/targets/linux.cmake +++ b/cmake/targets/linux.cmake @@ -28,7 +28,7 @@ target_link_libraries(${DUSK_LIBRARY_TARGET_NAME} PUBLIC target_compile_definitions(${DUSK_LIBRARY_TARGET_NAME} PUBLIC DUSK_SDL2 DUSK_OPENGL - DUSK_OPENGL_LEGACY + # DUSK_OPENGL_LEGACY DUSK_LINUX DUSK_DISPLAY_SIZE_DYNAMIC DUSK_DISPLAY_WIDTH_DEFAULT=640 diff --git a/src/dusk/CMakeLists.txt b/src/dusk/CMakeLists.txt index ff8c5acb..f8b4daed 100644 --- a/src/dusk/CMakeLists.txt +++ b/src/dusk/CMakeLists.txt @@ -32,6 +32,15 @@ if(NOT yyjson_FOUND) endif() endif() +if(NOT fast_obj_FOUND) + find_package(fast_obj REQUIRED) + if(fast_obj_FOUND) + target_link_libraries(${DUSK_LIBRARY_TARGET_NAME} PUBLIC fast_obj) + else() + message(FATAL_ERROR "fast_obj not found. Please ensure fast_obj is correctly fetched.") + endif() +endif() + if(NOT Lua_FOUND) find_package(Lua REQUIRED) if(Lua_FOUND AND NOT TARGET Lua::Lua) diff --git a/src/dusk/asset/loader/display/CMakeLists.txt b/src/dusk/asset/loader/display/CMakeLists.txt index 562a1298..391df145 100644 --- a/src/dusk/asset/loader/display/CMakeLists.txt +++ b/src/dusk/asset/loader/display/CMakeLists.txt @@ -6,6 +6,7 @@ # Sources target_sources(${DUSK_LIBRARY_TARGET_NAME} PUBLIC + assetmeshloader.c assettextureloader.c assettilesetloader.c ) \ No newline at end of file diff --git a/src/dusk/asset/loader/display/assetmeshloader.c b/src/dusk/asset/loader/display/assetmeshloader.c new file mode 100644 index 00000000..8c4b907e --- /dev/null +++ b/src/dusk/asset/loader/display/assetmeshloader.c @@ -0,0 +1,122 @@ +/** + * Copyright (c) 2026 Dominic Masters + * + * This software is released under the MIT License. + * https://opensource.org/licenses/MIT + */ + +#include "assetmeshloader.h" +#include "assert/assert.h" +#include "util/memory.h" +#define FAST_OBJ_IMPLEMENTATION +#include "fast_obj.h" + +// Wraps assetfile_t for fast_obj callbacks. Only the primary OBJ file is +// opened through the ZIP; returning NULL for any secondary path (e.g. .mtl) +// causes fast_obj to skip material loading, which is fine for our use case. +static void* meshFileOpen(const char *path, void *user_data) { + assetfile_t *file = (assetfile_t *)user_data; + if(file->zipFile != NULL) return NULL; + errorret_t ret = assetFileOpen(file); + if(ret.code != ERROR_OK) return NULL; + return file; +} + +static void meshFileClose(void *handle, void *user_data) { + if(handle == NULL) return; + assetfile_t *file = (assetfile_t *)handle; + errorCatch(assetFileClose(file)); +} + +static size_t meshFileRead(void *handle, void *dst, size_t bytes, void *user_data) { + if(handle == NULL) return 0; + assetfile_t *file = (assetfile_t *)handle; + errorret_t ret = assetFileRead(file, dst, bytes); + if(ret.code != ERROR_OK) return 0; + return (size_t)file->lastRead; +} + +static unsigned long meshFileSize(void *handle, void *user_data) { + if(handle == NULL) return 0; + assetfile_t *file = (assetfile_t *)handle; + return (unsigned long)file->size; +} + +errorret_t assetMeshLoader(assetfile_t *file) { + assertNotNull(file, "Asset file cannot be NULL."); + assertNotNull(file->output, "Asset file output cannot be NULL."); + + fastObjCallbacks callbacks = { + .file_open = meshFileOpen, + .file_close = meshFileClose, + .file_read = meshFileRead, + .file_size = meshFileSize, + }; + + fastObjMesh *obj = fast_obj_read_with_callbacks(file->filename, &callbacks, file); + if(obj == NULL) { + errorThrow("Failed to parse OBJ: %s", file->filename); + } + + // Count output vertices, triangulating any polygons via fan decomposition. + int32_t vertexCount = 0; + for(unsigned int i = 0; i < obj->face_count; i++) { + if(obj->face_vertices[i] >= 3) { + vertexCount += (int32_t)(obj->face_vertices[i] - 2) * 3; + } + } + + if(vertexCount == 0) { + fast_obj_destroy(obj); + errorThrow("OBJ has no valid faces: %s", file->filename); + } + + meshvertex_t *vertices = (meshvertex_t *)memoryAllocate( + sizeof(meshvertex_t) * vertexCount + ); + memoryZero(vertices, sizeof(meshvertex_t) * vertexCount); + + int32_t vi = 0; + fastObjIndex *idx = obj->indices; + for(unsigned int fi = 0; fi < obj->face_count; fi++) { + unsigned int fv = obj->face_vertices[fi]; + // Fan triangulation: anchor at idx[0], triangle (0, j, j+1). + for(unsigned int j = 1; j + 1 < fv; j++) { + fastObjIndex corners[3] = { idx[0], idx[j], idx[j + 1] }; + for(int c = 0; c < 3; c++) { + vertices[vi].color = COLOR_WHITE_4B; + + unsigned int p = corners[c].p; + if(p > 0 && p < obj->position_count) { + vertices[vi].pos[0] = obj->positions[p * 3 + 0]; + vertices[vi].pos[1] = obj->positions[p * 3 + 1]; + vertices[vi].pos[2] = obj->positions[p * 3 + 2]; + } + + unsigned int t = corners[c].t; + if(t > 0 && t < obj->texcoord_count) { + vertices[vi].uv[0] = obj->texcoords[t * 2 + 0]; + vertices[vi].uv[1] = obj->texcoords[t * 2 + 1]; + } + + vi++; + } + } + idx += fv; + } + + fast_obj_destroy(obj); + + errorret_t ret = meshInit( + (mesh_t *)file->output, + MESH_PRIMITIVE_TYPE_TRIANGLES, + vertexCount, + vertices + ); + memoryFree(vertices); + return ret; +} + +errorret_t assetMeshLoad(const char_t *path, mesh_t *out) { + return assetLoad(path, assetMeshLoader, NULL, out); +} diff --git a/src/dusk/asset/loader/display/assetmeshloader.h b/src/dusk/asset/loader/display/assetmeshloader.h new file mode 100644 index 00000000..bedef3b1 --- /dev/null +++ b/src/dusk/asset/loader/display/assetmeshloader.h @@ -0,0 +1,27 @@ +/** + * Copyright (c) 2026 Dominic Masters + * + * This software is released under the MIT License. + * https://opensource.org/licenses/MIT + */ + +#pragma once +#include "asset/asset.h" +#include "display/mesh/mesh.h" + +/** + * Loads a mesh from an OBJ asset file. + * + * @param file Asset file to load from. + * @return Any error that occurs during loading. + */ +errorret_t assetMeshLoader(assetfile_t *file); + +/** + * Loads a mesh from the specified OBJ asset path. + * + * @param path Path to the OBJ asset. + * @param out Output mesh to load into. + * @return Any error that occurs during loading. + */ +errorret_t assetMeshLoad(const char_t *path, mesh_t *out); diff --git a/src/dusk/engine/engine.c b/src/dusk/engine/engine.c index 26deff65..f5619fd7 100644 --- a/src/dusk/engine/engine.c +++ b/src/dusk/engine/engine.c @@ -19,7 +19,7 @@ #include "entity/entitymanager.h" #include "game/game.h" -#include "display/mesh/cube.h" +#include "asset/loader/display/assetmeshloader.h" engine_t ENGINE; texture_t TEXTURE; @@ -60,9 +60,10 @@ errorret_t engineInit(const int32_t argc, const char_t **argv) { camPos, (vec3){ 0.0f, 0.0f, 0.0f }, (vec3){ 0.0f, 1.0f, 0.0f }, - (vec3){ 5.0f, 5.0f, 5.0f } + (vec3){ 300.0f, 300.0f, 300.0f } ); componentid_t camCam = entityAddComponent(cam, COMPONENT_TYPE_CAMERA); + entityCameraSetZFar(cam, camCam, 5000.0f); ent1 = entityManagerAdd(); ent1Pos = entityAddComponent(ent1, COMPONENT_TYPE_POSITION); @@ -74,12 +75,7 @@ errorret_t engineInit(const int32_t argc, const char_t **argv) { }); mesh_t *mesh = entityMeshGetMesh(ent1, ent1Mesh); - errorChain(meshInit( - mesh, - CUBE_PRIMITIVE_TYPE, - CUBE_VERTEX_COUNT, - CUBE_MESH_SIMPLE_VERTICES - )); + errorChain(assetMeshLoad("test/test.obj", mesh)); shadermaterial_t *mat = entityMaterialGetShaderMaterial(ent1, ent1Mat); mat->unlit.color = COLOR_BLACK; @@ -103,9 +99,9 @@ errorret_t engineUpdate(void) { vec3 rotation; entityPositionGetRotation(ent1, ent1Pos, rotation); #if DUSK_TIME_DYNAMIC - rotation[1] += 2.0f * TIME.dynamicDelta; + rotation[1] += 1.0f * TIME.dynamicDelta; #else - rotation[1] += 2.0f * TIME.delta; + rotation[1] += 1.0f * TIME.delta; #endif entityPositionSetRotation(ent1, ent1Pos, rotation); diff --git a/src/dusk/entity/component/display/entitycamera.c b/src/dusk/entity/component/display/entitycamera.c index c31cd725..fd1b238a 100644 --- a/src/dusk/entity/component/display/entitycamera.c +++ b/src/dusk/entity/component/display/entitycamera.c @@ -19,6 +19,42 @@ void entityCameraInit(const entityid_t ent, const componentid_t comp) { cam->perspective.fov = glm_rad(45.0f); } +float_t entityCameraGetZNear(const entityid_t ent, const componentid_t comp) { + entitycamera_t *cam = (entitycamera_t *)componentGetData( + ent, comp, COMPONENT_TYPE_CAMERA + ); + return cam->nearClip; +} + +void entityCameraSetZNear( + const entityid_t ent, + const componentid_t comp, + const float_t zNear +) { + entitycamera_t *cam = (entitycamera_t *)componentGetData( + ent, comp, COMPONENT_TYPE_CAMERA + ); + cam->nearClip = zNear; +} + +float_t entityCameraGetZFar(const entityid_t ent, const componentid_t comp) { + entitycamera_t *cam = (entitycamera_t *)componentGetData( + ent, comp, COMPONENT_TYPE_CAMERA + ); + return cam->farClip; +} + +void entityCameraSetZFar( + const entityid_t ent, + const componentid_t comp, + const float_t zFar +) { + entitycamera_t *cam = (entitycamera_t *)componentGetData( + ent, comp, COMPONENT_TYPE_CAMERA + ); + cam->farClip = zFar; +} + void entityCameraGetProjection( const entityid_t ent, const componentid_t comp, diff --git a/src/dusk/entity/component/display/entitycamera.h b/src/dusk/entity/component/display/entitycamera.h index d99cc2dc..a258262e 100644 --- a/src/dusk/entity/component/display/entitycamera.h +++ b/src/dusk/entity/component/display/entitycamera.h @@ -43,7 +43,7 @@ void entityCameraInit(const entityid_t ent, const componentid_t comp); /** * Renders out the projection matrix for the given camera. - * + * * @param ent The entity ID. * @param comp The component ID. * @param out The output projection matrix. @@ -52,4 +52,48 @@ void entityCameraGetProjection( const entityid_t ent, const componentid_t comp, mat4 out +); + +/** + * Gets the near clip distance of a camera. + * + * @param ent The entity ID. + * @param comp The component ID. + * @return The near clip distance. + */ +float_t entityCameraGetZNear(const entityid_t ent, const componentid_t comp); + +/** + * Sets the near clip distance of a camera. + * + * @param ent The entity ID. + * @param comp The component ID. + * @param zNear The near clip distance. + */ +void entityCameraSetZNear( + const entityid_t ent, + const componentid_t comp, + const float_t zNear +); + +/** + * Gets the far clip distance of a camera. + * + * @param ent The entity ID. + * @param comp The component ID. + * @return The far clip distance. + */ +float_t entityCameraGetZFar(const entityid_t ent, const componentid_t comp); + +/** + * Sets the far clip distance of a camera. + * + * @param ent The entity ID. + * @param comp The component ID. + * @param zFar The far clip distance. + */ +void entityCameraSetZFar( + const entityid_t ent, + const componentid_t comp, + const float_t zFar ); \ No newline at end of file