From 221d68906d4b8378b834e6fe9d9bbbf1daaa3fd2 Mon Sep 17 00:00:00 2001 From: Camden Dixie O'Brien Date: Tue, 30 Sep 2025 14:43:10 +0100 Subject: [PATCH] Implement basic GLB model loading --- CMakeLists.txt | 1 + main.c | 13 +- model_format_glb.c | 511 +++++++++++++++++++++++++++++++++++++++++++++ model_format_glb.h | 8 + 4 files changed, 529 insertions(+), 4 deletions(-) create mode 100644 model_format_glb.c create mode 100644 model_format_glb.h diff --git a/CMakeLists.txt b/CMakeLists.txt index f0dc4b7..4ed2c86 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -41,6 +41,7 @@ add_executable(demo linear_alg.c main.c model.c + model_format_glb.c model_format_utah.c ) configure_target(demo) diff --git a/main.c b/main.c index 480a3a4..a0b9dd8 100644 --- a/main.c +++ b/main.c @@ -1,5 +1,5 @@ #include "gpu.h" -#include "model_format_utah.h" +#include "model_format_glb.h" #include #include @@ -51,7 +51,7 @@ int main(int argc, char *argv[]) fprintf(stderr, "Usage: %s PATH/TO/MODEL\n", argv[0]); return 1; } - model_t model = model_format_utah_read(argv[1]); + model_t model = model_format_glb_read(argv[1]); // Set up window glfwInit(); @@ -74,9 +74,14 @@ int main(int argc, char *argv[]) const vec3_t cam = { 0, 4, -4 }; const vec3_t tgt = { 0, 0.5, 0 }; mvp_t mvp = { - .model = MAT4_SCALE(0.667), + .model = (mat4_t) { + { 0, 0, -1, 0 }, + { -1, 0, 0, 0 }, + { 0, 1, 0, 0 }, + { 0, 0, 0, 1 }, + }, .view = mat4_lookat(cam, tgt, (vec3_t) { 0, 1, 0 }), - .proj = mat4_perspective((float)W / H, PI / 6, 0.1, 10.0), + .proj = mat4_perspective((float)W / H, PI / 12, 0.1, 10.0), }; const mat4_t rotx = MAT4_ROTATE_X(PI / 128); diff --git a/model_format_glb.c b/model_format_glb.c new file mode 100644 index 0000000..2bf01a0 --- /dev/null +++ b/model_format_glb.c @@ -0,0 +1,511 @@ +#include "model_format_glb.h" + +#include +#include +#include +#include +#include +#include +#include +#include + +/* + * Ensure the system is little-endian. + * + * GLB is a little-endian format and this module assumes the host + * system matches this in multiple places. + */ +#if defined(__BYTE_ORDER__) && defined(__ORDER_LITTLE_ENDIAN__) +static_assert( + __BYTE_ORDER__ == __ORDER_LITTLE_ENDIAN__, + "This module requires a little-endian system"); +#else +#warning "Could not determine endianness: assuming little endian" +#endif + +#define MAGIC 0x46546c67 // "glTF" +#define VERSION 2 + +#define MAX_KEY_SIZE 128 + +enum { + CHUNK_TYPE_JSON = 0x4e4f534a, // "JSON" + CHUNK_TYPE_BIN = 0x004e4942, // "BIN\0" +}; + +enum { + RENDERING_MODE_TRIANGLES = 4, +}; + +typedef struct { + uint32_t magic; + uint32_t version; + uint32_t len; +} file_hdr_t; + +typedef struct { + uint32_t len; + uint32_t type; +} chunk_hdr_t; + +typedef struct { + uint32_t normals; + uint32_t positions; + uint32_t indices; +} mesh_t; + +typedef struct { + uint32_t view; + uint32_t offset; + uint32_t count; +} accessor_t; + +typedef struct { + accessor_t normals; + accessor_t positions; + accessor_t indices; +} accessors_t; + +typedef struct { + uint32_t offset; + uint32_t len; + uint32_t stride; +} view_t; + +typedef struct { + view_t normals; + view_t positions; + view_t indices; +} views_t; + +typedef struct { + uint32_t offset; + uint32_t len; +} gltf_info_member_t; + +typedef struct { + gltf_info_member_t normals; + gltf_info_member_t positions; + gltf_info_member_t indices; +} gltf_info_t; + +static void *map_file(const char *path) +{ + const int fd = open(path, O_RDONLY); + assert(fd != -1); + + struct stat stat; + const int res = fstat(fd, &stat); + assert(res == 0); + + void *addr = mmap(nullptr, stat.st_size, PROT_READ, MAP_PRIVATE, fd, 0); + assert(addr != nullptr); + + close(fd); + return addr; +} + +static int read_key(const uint8_t **p, char buffer[MAX_KEY_SIZE]) +{ + assert(**p == '"'); + ++(*p); + size_t len = 0; + while (**p != '"') { + assert(len < MAX_KEY_SIZE); + buffer[len++] = *(*p)++; + } + ++(*p); + return len; +} + +static bool ends_value(uint8_t b) +{ + switch (b) { + case '}': + case ']': + case ',': + return true; + default: + return false; + } +} + +static void skip_value(const uint8_t **p) +{ + switch (**p) { + int nesting; + case '{': + ++(*p); + nesting = 0; + while (**p != '}' || nesting > 0) { + if (**p == '{') + ++nesting; + if (**p == '}') + --nesting; + ++(*p); + } + ++(*p); + break; + case '[': + ++(*p); + nesting = 0; + while (**p != ']' || nesting > 0) { + if (**p == '[') + ++nesting; + if (**p == ']') + --nesting; + ++(*p); + } + ++(*p); + break; + case '"': + ++(*p); + while (**p != '"') { + if (**p == '\\') + ++(*p); + ++(*p); + } + ++(*p); + break; + default: + ++(*p); + while (!ends_value(**p)) + ++(*p); + break; + } +} + +static uint32_t read_uint(const uint8_t **p) +{ + uint32_t value = 0; + while (!ends_value(**p)) { + assert(isdigit(**p)); + value = 10 * value + *(*p)++ - '0'; + } + return value; +} + +static mesh_t read_mesh_primitives(const uint8_t **p) +{ + char buffer[MAX_KEY_SIZE]; + size_t len; + mesh_t mesh = {}; + + assert(**p == '['); + ++(*p); + + assert(**p == '{'); + ++(*p); + + while (**p != '}') { + len = read_key(p, buffer); + assert(**p == ':'); + ++(*p); + + if (memcmp("attributes", buffer, len) == 0) { + assert(**p == '{'); + ++(*p); + + while (**p != '}') { + len = read_key(p, buffer); + assert(**p == ':'); + ++(*p); + if (memcmp("NORMAL", buffer, len) == 0) { + mesh.normals = read_uint(p); + } else if (memcmp("POSITION", buffer, len) == 0) { + mesh.positions = read_uint(p); + } else { + skip_value(p); + } + if (**p == ',') + ++(*p); + } + ++(*p); + } else if (memcmp("indices", buffer, len) == 0) { + mesh.indices = read_uint(p); + } else if (memcmp("mode", buffer, len) == 0) { + assert(read_uint(p) == RENDERING_MODE_TRIANGLES); + } else { + skip_value(p); + } + + if (**p == ',') + ++(*p); + } + ++(*p); + + assert(**p == ']'); + ++(*p); + + return mesh; +} + +static mesh_t read_meshes(const uint8_t *p) +{ + char buffer[MAX_KEY_SIZE]; + size_t len; + + assert(*p == '['); + ++p; + assert(*p == '{'); + ++p; + + while (*p != '}') { + len = read_key(&p, buffer); + assert(*p == ':'); + ++p; + if (memcmp("primitives", buffer, len) == 0) + return read_mesh_primitives(&p); + else + skip_value(&p); + if (*p == ',') + ++p; + } + + /* + * If we reach here, the primitives key was missing from the mesh + * definition. + */ + assert(false); +} + +static accessor_t read_accessor(const uint8_t **p) +{ + char buffer[MAX_KEY_SIZE]; + size_t len; + accessor_t accessor = {}; + + assert(**p == '{'); + ++(*p); + + while (**p != '}') { + len = read_key(p, buffer); + assert(**p == ':'); + ++(*p); + + if (memcmp("bufferView", buffer, len) == 0) + accessor.view = read_uint(p); + else if (memcmp("byteOffset", buffer, len) == 0) + accessor.offset = read_uint(p); + else if (memcmp("count", buffer, len) == 0) + accessor.count = read_uint(p); + else if (memcmp("sparse", buffer, len) == 0) + assert(false); // Sparse accessors are not supported + else + skip_value(p); + + if (**p == ',') + ++(*p); + } + ++(*p); + + return accessor; +} + +static accessors_t read_accessors(const uint8_t *p, mesh_t mesh) +{ + accessors_t accessors = {}; + assert(*p == '['); + ++p; + for (uint32_t i = 0; *p != ']'; ++i) { + if (i == mesh.normals) + accessors.normals = read_accessor(&p); + else if (i == mesh.positions) + accessors.positions = read_accessor(&p); + else if (i == mesh.indices) + accessors.indices = read_accessor(&p); + else + skip_value(&p); + + if (*p == ',') + ++p; + } + return accessors; +} + +static view_t read_view(const uint8_t **p) +{ + char buffer[MAX_KEY_SIZE]; + size_t len; + view_t view = {}; + + assert(**p == '{'); + ++(*p); + + while (**p != '}') { + len = read_key(p, buffer); + assert(**p == ':'); + ++(*p); + + if (memcmp("buffer", buffer, len) == 0) + assert(read_uint(p) == 0); + else if (memcmp("byteOffset", buffer, len) == 0) + view.offset = read_uint(p); + else if (memcmp("byteLength", buffer, len) == 0) + view.len = read_uint(p); + else if (memcmp("byteStride", buffer, len) == 0) + view.stride = read_uint(p); + else + skip_value(p); + + if (**p == ',') + ++(*p); + } + ++(*p); + + return view; +} + +static views_t read_views(const uint8_t *p, accessors_t accessors) +{ + views_t views = {}; + assert(*p == '['); + ++p; + for (uint32_t i = 0; *p != ']'; ++i) { + if (i == accessors.normals.view) + views.normals = read_view(&p); + else if (i == accessors.positions.view) + views.positions = read_view(&p); + else if (i == accessors.indices.view) + views.indices = read_view(&p); + else + skip_value(&p); + + if (*p == ',') + ++p; + } + return views; +} + +static gltf_info_t read_json(const uint8_t *json) +{ + char buffer[MAX_KEY_SIZE]; + size_t len; + + assert(*json == '{'); + ++json; + + /* + * We want to avoid having to read every single accessor, buffer + * view etc, so we must identify which ones we need *before* + * processing these sections. However, the sections may appear in + * any order, so we must first do a pass through the JSON to + * identify the section locations, so that we can then go through + * them in the necessary order. + */ + const uint8_t *meshes_json = nullptr; + const uint8_t *accessors_json = nullptr; + const uint8_t *views_json = nullptr; + while (*json != '}') { + len = read_key(&json, buffer); + assert(*json == ':'); + ++json; + if (memcmp("meshes", buffer, len) == 0) + meshes_json = json; + else if (memcmp("accessors", buffer, len) == 0) + accessors_json = json; + else if (memcmp("bufferViews", buffer, len) == 0) + views_json = json; + skip_value(&json); + if (*json == ',') + ++json; + } + assert(meshes_json != nullptr); + assert(accessors_json != nullptr); + assert(views_json != nullptr); + + mesh_t mesh = read_meshes(meshes_json); + accessors_t accessors = read_accessors(accessors_json, mesh); + views_t views = read_views(views_json, accessors); + + size_t normals_len, positions_len; + if (accessors.positions.view == accessors.normals.view) { + views.positions = views.normals; + assert(views.normals.stride == sizeof(vec3_t)); + assert(views.positions.stride == sizeof(vec3_t)); + normals_len = accessors.normals.count * sizeof(vec3_t); + positions_len = accessors.positions.count * sizeof(vec3_t); + } else { + normals_len = views.normals.len; + positions_len = views.positions.len; + } + + assert(views.indices.stride == 0); + assert(views.indices.len / accessors.indices.count == sizeof(index_t)); + + return (gltf_info_t) { + .normals = { + .offset = views.normals.offset + accessors.normals.offset, + .len = normals_len, + }, + .positions = { + .offset = views.positions.offset + accessors.positions.offset, + .len = positions_len, + }, + .indices = { + .offset = views.indices.offset + accessors.indices.offset, + .len = views.indices.len, + }, + }; +} + +static void load_vertices( + vertex_t *restrict dest, const vec3_t *restrict positions, + const vec3_t *restrict normals, size_t count) +{ + for (unsigned i = 0; i < count; ++i) { + dest[i].position = positions[i]; + dest[i].normal = normals[i]; + } +} + +model_t model_format_glb_read(const char *path) +{ + uint8_t *file = map_file(path); + + const file_hdr_t *file_hdr = (const file_hdr_t *)file; + assert(file_hdr->magic == MAGIC); + assert(file_hdr->version == VERSION); + const size_t len = file_hdr->len; + + // Read JSON chunk + size_t offset = sizeof(file_hdr_t); + const chunk_hdr_t *json_hdr = (const chunk_hdr_t *)(file + offset); + assert(json_hdr->type == CHUNK_TYPE_JSON); + offset += sizeof(chunk_hdr_t); + const gltf_info_t info = read_json(file + offset); + + // Check glTF info for validity + assert(info.positions.len == info.normals.len); + assert(info.positions.offset != info.normals.offset); + if (info.positions.offset < info.normals.offset) { + const uint32_t pos_end = info.positions.offset + info.positions.len; + assert(pos_end <= info.normals.offset); + } else { + const uint32_t norm_end = info.normals.offset + info.normals.len; + assert(norm_end <= info.positions.offset); + } + + // Read binary chunk header + offset += json_hdr->len; + assert(offset != len); + const chunk_hdr_t *bin_hdr = (const chunk_hdr_t *)(file + offset); + assert(bin_hdr->type == CHUNK_TYPE_BIN); + offset += sizeof(chunk_hdr_t); + const uint8_t *bin_data = file + offset; + offset += bin_hdr->len; + assert(offset == len); + + // Load data from binary chunk into model + const size_t vertex_count = info.positions.len / sizeof(vec3_t); + const size_t index_count = info.indices.len / sizeof(index_t); + model_t model = model_init(vertex_count, index_count); + const vec3_t *positions + = (const vec3_t *)(bin_data + info.positions.offset); + const vec3_t *normals = (const vec3_t *)(bin_data + info.normals.offset); + load_vertices(model.vertices, positions, normals, vertex_count); + memcpy(model.indices, bin_data + info.indices.offset, info.indices.len); + + munmap(file, len); + return model; +} diff --git a/model_format_glb.h b/model_format_glb.h new file mode 100644 index 0000000..47f8953 --- /dev/null +++ b/model_format_glb.h @@ -0,0 +1,8 @@ +#ifndef MODEL_FORMAT_GLB_H +#define MODEL_FORMAT_GLB_H + +#include "model.h" + +model_t model_format_glb_read(const char *path); + +#endif