Description
This project reads binary .vox files generated by MagicaVoxel and outputs the voxel model using Raylib. The main learning goal for the project was to familiarize myself with binary file input in C++, as well as parsing the resulting data. After that initial goal, A secondary learning goal came about in learning to compile C++ and Raylib projects to WebAssembly using Emscripten and configuring it to work with my existing build pipeline.
In the course of building this project, I was able to familiarize myself with pointer arithmatic and memory manipulation operations in the C++ standard library. I had to find a way to iterate over an array of raw byte data with irregularly sized hunks of data. Luckily, the .vox files provide headers for each chunk of data that specify what type of chunk it is and how many bytes it occupies. I was able to do this by creating a pseudo 'for' loop where a while loop checks the offset of a byte pointer against the total size of the file being processed. The byte pointer is then used as the starting point for each chunk of data, and is incremented by the size of the chunk afterwards. I really enjoyed implementing this, as it felt very satisfying to successfully read and interpret raw byte data.
In the initial version of the project, I used a naive approach to
rendering the models, because I needed it to be done in a relatively
short timeframe. Once I had the voxel data parsed from the binary file,
I simply looped through the array containing coordinates where a voxel
existed, and drew a cube with raylib. Doing this every frame was
obviously not ideal, and the performance implications were very apparent
when trying to render large models like the voxelized version of the
stanford dragon provided by the
.vox file format github
(this model is included as an example in this build). Not only is
drawing a cube for every voxel causing a lot of overdraw, the bigger
issue is the large number of draw calls happening. To get around this,
the obvious solution is to generate a proper mesh from the provided
voxel data. Not only does this allow me to cull the faces in between
voxels, but crucially it allows the entire model to be drawn in very few
calls to the GPU—ideally even just a single one. At first I tried
turning the models into a single mesh regardless of size, and managed to
get it working pretty quickly. Unfortunately when I tried to load the
aforementioned dragon model, it only rendered a small portion of the
tail. I spent quite a while trying to figure out what was happenning,
before ultimately discovering the culprit was integer overflow.
Raylib(and I'm led to believe OpenGL) uses a uint16 array
for triangle indices. This means that if a mesh has more than 65535
vertices, only a portion will be able to be accessed and converted to
triangles. I knew from a brief venture into bare OpenGl that it was
possible to draw mesh without using the index array, and to my mild
surprise all it took to get the model to draw correctly was adding some
duplicate vertices to the mesh and removing the code to add indices.
Sadly for me, that was not actually the end of the story. When I tried
doing a WASM build of the project, nothing rendered. It wasn't just the
large stanford dragon, but even the smaller models that hadn't had any
issues bfore were now gone, leaving an empty square of cornflower blue
and a very unhelpful webGL error message. As it turns out, webGL and a
few other implementations require index arrays to draw
anything. This means I have to take a few steps back and add another
step of processing to the models before they can be rendered.
Fortunately that step is pretty simple. Instead of storing a single
raylib mesh struct, we can store a vector of meshes and create a new
mesh as soon as the current one gets too close to the limit imposed by
uint16. To absolutely nobody's surprise, I ran into yet
another issue trying to implement this. As it turns out, initializing a
local mesh struct with Mesh mesh{}; does not create a
unique object every time, and in fact causes some weird situations where
even code behind an if statement that isn't ever reached,
can cause the mesh to think it's been uploaded to the GPU more than it
actually has. The way I fixed this was to instead initialize the struct
with auto* mesh = new Mesh();, and then push it to the
vector with meshes.push_back(*mesh). With this final
problem solved, I was able to successfully split up large meshes and
draw them with very acceptable performance, even in web builds.
Source Code
The following is most of the relevant code. Full source code can be found here.
C++
cmakelists