2025-02-06 - reading time: 8 minutes
so i've been working for a while on a large game project and was really starting to get burnt out
it's been my primary focus since graduating from school and progress started slowing to a crawl
it was clear to me that i needed a break from the project to recharge a little bit
so i decided to try and start a project i've been fantasizing about for a long time:
mons is a monumental undertaking, an excercise in insanity, and my personal code sanctuary
the goal of the project is pretty abstract, i'm just coding whatever i feel like, but doing it with
no rust :(
i've been writing rust almost every day for the last 3 years, it's time to get scary
c is like dead simple which is why i think it will be a pretty meditative experience
by meditative i mean mind-numbingly tedious but i'm not in a rush here
i think the absolute worst part of this is the lack of namespaces but i'll live
it's time to get real
if i'm undertaking this journey in masochism voluntary discomfort (only true stoic sigmas get this one) i might as well be learning something while i do it
this would be "no dependencies" but like i kind of want to make some fun stuff at some point
so i have 3 big exceptions
i can use stdlib and posix shit (at my discretion)
i can use "important" libraries
i don't really have a good description for what makes a library "important" but think hardware stuff like opengl/vulkan or xlib
the last thing is that i can use freely available implementations of algorithms i have no chance in hell of understanding, like mikktspace
i just don't see much value in doing that myself
starting out from nothing i decided my first goal would be to spin up a basic 3d graphical application
i decided to start with opengl, i have experience with both it and vulkan and opengl is significantly less work to get started with
before i could even render a basic triangle though, i needed (wanted) a way to represent and manipulate basic linear algebra types such as vectors, matrices, and quaternions
so on the first day i just sat on my couch and typed up a library for exactly that
mons_math/src/
├── mat2.c
├── mat3.c
├── mat4.c
├── quat.c
├── util.c
├── vec2.c
├── vec3.c
└── vec4.c
(i won't get into implementation details too much in this post in the interest of brevity, but all the code is available on my git server)
i found writing the matrix and vector operations super satisfying in spite of the repetition for different dimensionalities
i do wish i had kept in mind that opengl is column-major for matrices, since i wrote mine as row-major like a normal person, but it might honestly be for the best, as it makes using them much more intuitive based on the stuff i learned in math class
once i had this library i was quickly able to spin up a basic 3d app using opengl, along with xlib and glx for windowing and input under x11
i had always used glfw for windowing so it was interesting to see how similar it was using xlib (although there were some tricky differences, like windows being created without a depth buffer by default)
i set up my project to automatically embed shaders in my code which was pretty cool
basically there isn't a c mechanism similar to rust's lovely include_bytes
macro, so the best you can really do is generate a constant byte array and store it in a header file
luckily i learned how to use the xxd tool to automatically generate such a header file, and then added a script to automatically embed all files located in my project's embed
folder and place generated headers in the corresponding include/embedded
location
embed/
└── shaders
├── basic.frag.glsl
└── basic.vert.glsl
include/
├── ...
├── embedded
│ └── shaders
│ ├── basic.frag.glsl.h
│ └── basic.vert.glsl.h
└── ...
then i added it as a fake target in my cmake project and made the main project target depend on it
not satisfied with a simple solid color, i decided i needed to load images
obviously i wasn't allowed to use an existing loader like stbi
i did take a quick look at the png and jpeg specifications since these are by far and away the most popular texture image formats out there
my lazy ass was not about to write a loader for either of those formats (for now)
for now i decided to look for something easy to implement but like actually respectable (so not ppm), and boy oh boy
enter qoi
this shit descended from the heavens i swear to god
qoi, or the quite ok image format, is extremely simple and extremely fast to encode and decode, and offers pretty decent lossless compression
my decoder is around 200 lines of c i think, which is a bit more than the reference implementation i believe but i tried my best to not refer to it for fun
once i had that i was able to just pop the loaded image data into opengl textures
up to this point i was using hardcoded vertex buffers and i wanted to be able to load more complex geometry
i considered using obj for my models but i'm a big fan of gltf so i decided to just jump the gun and go for a gltf loader right off the bat
gltf is stored as json data so i knew if i wanted to have a shot i would need a flexible, dynamic json solution
struct mons_json_value;
union mons_json_entry;
typedef struct mons_json_array {
struct mons_json_value *values;
unsigned int len;
} mons_json_array;
typedef union mons_json_value_data {
char *string;
float number;
mons_hashmap object;
mons_json_array array;
bool boolean;
void *null;
} mons_json_value_data;
typedef enum mons_json_value_type {
MONS_JSON_STRING,
MONS_JSON_NUMBER,
MONS_JSON_OBJECT,
MONS_JSON_ARRAY,
MONS_JSON_BOOL,
MONS_JSON_NULL,
} mons_json_value_type;
typedef struct mons_json_value {
mons_json_value_type type;
mons_json_value_data data;
} mons_json_value;
i discovered a cool pattern here where a combination of an enum and a union (never actually used one before this project) allowed me to store a variety of types in a mons_json_value
while being aware of which type it actually was
the performance of my json structures depended on my ability to store key-value pairs in an efficient-to-access way
obvious choice was a hashmap data structure
i just implemented this from memory from my data structures class back in school
typedef struct mons_hashmap_pair {
char *key;
void *value;
struct mons_hashmap_pair *next;
} mons_hashmap_pair;
typedef struct mons_hashmap {
mons_hashmap_pair **data;
unsigned int bucket_count;
unsigned int member_size;
unsigned int len;
} mons_hashmap;
this hashmap is basically just an array of linked lists that pairs get shoved into based on the hash of the key
for the hash function i just went with the djb2 hash function since it worked well for me in the past
at long last, i was able to dig into the gltf format
the specification is quite deep and i focused on implementing basic mesh features first, opting to do things like animations, morph targets, scenes, node hierarchies, etc. later
i actually found the process of implementing the loader pretty chill, i especially enjoyed learning how the data was stored in the binary buffers and how to use accessors to read it
after about a day of work i managed to get mesh primitives out of the gltf files and into my in-memory model structure
(i was using this model for testing)
the textures that came with the model were in jpeg format, which i obviously could not yet load, and the gltf spec doesn't support formats other than jpeg and png
so i said "To hell with the spec!" and switched the image mimetypes to the (illegal) image/qoi
and converted the textures using imagemagick anyway
then my textures loaded beautifully
at this point i was getting pretty tired lol
i was ready to take a break from my break
but i wanted to leave off in a really satisfying place so i decided to implement normal mapping as one last thing
as i previously mentioned i would use the mikktspace algorithm to do my tangent calculations
and with that i decided i was ready to go back and get some work done on something actually important to me
this whole process took me about a week and was a pretty chill experience
thanks for reading this far, i hope it's not too brutal
- silas