3D OBJ loading
This commit is contained in:
@@ -105,3 +105,4 @@ yarn.lock
|
|||||||
|
|
||||||
/build2
|
/build2
|
||||||
/build*
|
/build*
|
||||||
|
/assets/test
|
||||||
@@ -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)
|
||||||
@@ -28,7 +28,7 @@ target_link_libraries(${DUSK_LIBRARY_TARGET_NAME} PUBLIC
|
|||||||
target_compile_definitions(${DUSK_LIBRARY_TARGET_NAME} PUBLIC
|
target_compile_definitions(${DUSK_LIBRARY_TARGET_NAME} PUBLIC
|
||||||
DUSK_SDL2
|
DUSK_SDL2
|
||||||
DUSK_OPENGL
|
DUSK_OPENGL
|
||||||
DUSK_OPENGL_LEGACY
|
# DUSK_OPENGL_LEGACY
|
||||||
DUSK_LINUX
|
DUSK_LINUX
|
||||||
DUSK_DISPLAY_SIZE_DYNAMIC
|
DUSK_DISPLAY_SIZE_DYNAMIC
|
||||||
DUSK_DISPLAY_WIDTH_DEFAULT=640
|
DUSK_DISPLAY_WIDTH_DEFAULT=640
|
||||||
|
|||||||
@@ -32,6 +32,15 @@ if(NOT yyjson_FOUND)
|
|||||||
endif()
|
endif()
|
||||||
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)
|
if(NOT Lua_FOUND)
|
||||||
find_package(Lua REQUIRED)
|
find_package(Lua REQUIRED)
|
||||||
if(Lua_FOUND AND NOT TARGET Lua::Lua)
|
if(Lua_FOUND AND NOT TARGET Lua::Lua)
|
||||||
|
|||||||
@@ -6,6 +6,7 @@
|
|||||||
# Sources
|
# Sources
|
||||||
target_sources(${DUSK_LIBRARY_TARGET_NAME}
|
target_sources(${DUSK_LIBRARY_TARGET_NAME}
|
||||||
PUBLIC
|
PUBLIC
|
||||||
|
assetmeshloader.c
|
||||||
assettextureloader.c
|
assettextureloader.c
|
||||||
assettilesetloader.c
|
assettilesetloader.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);
|
||||||
|
}
|
||||||
@@ -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);
|
||||||
@@ -19,7 +19,7 @@
|
|||||||
#include "entity/entitymanager.h"
|
#include "entity/entitymanager.h"
|
||||||
#include "game/game.h"
|
#include "game/game.h"
|
||||||
|
|
||||||
#include "display/mesh/cube.h"
|
#include "asset/loader/display/assetmeshloader.h"
|
||||||
|
|
||||||
engine_t ENGINE;
|
engine_t ENGINE;
|
||||||
texture_t TEXTURE;
|
texture_t TEXTURE;
|
||||||
@@ -60,9 +60,10 @@ errorret_t engineInit(const int32_t argc, const char_t **argv) {
|
|||||||
camPos,
|
camPos,
|
||||||
(vec3){ 0.0f, 0.0f, 0.0f },
|
(vec3){ 0.0f, 0.0f, 0.0f },
|
||||||
(vec3){ 0.0f, 1.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);
|
componentid_t camCam = entityAddComponent(cam, COMPONENT_TYPE_CAMERA);
|
||||||
|
entityCameraSetZFar(cam, camCam, 5000.0f);
|
||||||
|
|
||||||
ent1 = entityManagerAdd();
|
ent1 = entityManagerAdd();
|
||||||
ent1Pos = entityAddComponent(ent1, COMPONENT_TYPE_POSITION);
|
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);
|
mesh_t *mesh = entityMeshGetMesh(ent1, ent1Mesh);
|
||||||
errorChain(meshInit(
|
errorChain(assetMeshLoad("test/test.obj", mesh));
|
||||||
mesh,
|
|
||||||
CUBE_PRIMITIVE_TYPE,
|
|
||||||
CUBE_VERTEX_COUNT,
|
|
||||||
CUBE_MESH_SIMPLE_VERTICES
|
|
||||||
));
|
|
||||||
|
|
||||||
shadermaterial_t *mat = entityMaterialGetShaderMaterial(ent1, ent1Mat);
|
shadermaterial_t *mat = entityMaterialGetShaderMaterial(ent1, ent1Mat);
|
||||||
mat->unlit.color = COLOR_BLACK;
|
mat->unlit.color = COLOR_BLACK;
|
||||||
@@ -103,9 +99,9 @@ errorret_t engineUpdate(void) {
|
|||||||
vec3 rotation;
|
vec3 rotation;
|
||||||
entityPositionGetRotation(ent1, ent1Pos, rotation);
|
entityPositionGetRotation(ent1, ent1Pos, rotation);
|
||||||
#if DUSK_TIME_DYNAMIC
|
#if DUSK_TIME_DYNAMIC
|
||||||
rotation[1] += 2.0f * TIME.dynamicDelta;
|
rotation[1] += 1.0f * TIME.dynamicDelta;
|
||||||
#else
|
#else
|
||||||
rotation[1] += 2.0f * TIME.delta;
|
rotation[1] += 1.0f * TIME.delta;
|
||||||
#endif
|
#endif
|
||||||
entityPositionSetRotation(ent1, ent1Pos, rotation);
|
entityPositionSetRotation(ent1, ent1Pos, rotation);
|
||||||
|
|
||||||
|
|||||||
@@ -19,6 +19,42 @@ void entityCameraInit(const entityid_t ent, const componentid_t comp) {
|
|||||||
cam->perspective.fov = glm_rad(45.0f);
|
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(
|
void entityCameraGetProjection(
|
||||||
const entityid_t ent,
|
const entityid_t ent,
|
||||||
const componentid_t comp,
|
const componentid_t comp,
|
||||||
|
|||||||
@@ -53,3 +53,47 @@ void entityCameraGetProjection(
|
|||||||
const componentid_t comp,
|
const componentid_t comp,
|
||||||
mat4 out
|
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
|
||||||
|
);
|
||||||
Reference in New Issue
Block a user