Pages

25 October, 2021

My Voxel Rendering in C# OpenGL

Something I've always wanted to learn is how to implement voxels into a 3D game similarly to Minecraft. I didn't want to just copy Minecraft however, since there are many easier ways to achieve that.

What made me interested in this endeavor was a small JavaScript voxel game I found online called "Star Defenders 3D" created by "Eric Gurt" (eric-gurt.github.io/StarDefenders3D).

This is what it looked like at the time (not my game):

I loved how the voxels in this game were small and spherical in nature, allowing for great level of detail and colors. This intrigued me, so I began studying different implementations of voxel rendering.

"Star Defenders 3D" used OpenGL Point rendering in these screenshots, which  basically means each voxel was a single vertex in space, and was rendered as a 2d circle on the screen. This is done easily in OpenGL by using the primitive rendering mode of GL_POINTS. On its own, this renders each vertex as a square of pixels on the screen.

Using shaders, you can render this square as a circle. However, this circle would have a flat depth, and would not intersect with geometry as a sphere would. 

In 2020, I did this in C++ and this is what it looked like:


As you can see, where the points overlap its obvious that they are indeed flat circles.

The next step to make these points spherical is to use a fragment shader to alter the depth of each pixel on the circle. It took me many months of researching online to find how to do this back then, but I eventually found ways of doing it.

Here's the results of them when I implemented it in C#:


These are referred to as "Impostor Spheres". They allow for rendering spheres with only a quad. The quad can be rendered using a single vertex with GL_POINTS, or four vertices creating a quad with two triangles. 

The performance of using "Impostor Spheres" as voxels has its pros and cons. The main benefit being that each voxel can be represented with one or four vertices (depending on how accurately you want the spheres to be rendered). However, the OpenGL Rendering pipeline does not like it when you manually alter the depth of fragments. This causes some inefficiencies when rendering the spheres, as they are unable to be "early z-tested" when being rendered. Which basically means their fragment shader code will always execute, even if they are pixels which are obscured completely by other impostor spheres in-front of it.

I very much enjoyed the aesthetics of these "Impostor Sphere" voxels, and I played around with using them for particles, which worked quite nicely. 

Further into my exploration of voxel rendering, I wished to attempt to implement standard voxel rendering, which renders 6 sides of each voxel as a quad with two triangles. This is the technique used by Minecraft for its voxels, and also has its pros and cons.

I wanted to see how efficiently I could render these voxels, so I experimented with tessellation shaders to render each face using only a single vertex.

It was relatively successful in lowering the amount of memory needed to store all the many vertices in a sea of voxels by an order of magnitude. But now I had the task of rendering a world made of voxels. To do this, I implemented "chunks", which similarly to Minecraft, are large 'chunks' of thousands of voxels which are rendered in groups. This can be used to partition the world into segments, allowing for much faster rendering and storage of the data. 

I managed to render a few of these with quite a high resolution. This is the result:


Being C# and single threaded, this ran quite poorly. I also was not doing anything to cull the faces and vertices of voxels which were touching inside the dense chunks. But regardless of that, this was quite a feat. Each chunk was 64x64x64 voxels in dimensions, where as Minecraft has 16x16x16 chunks for comparison. 

Later I scaled this down and implemented textures, which looked like this:

Afterwards I scaled the chunks down to 32x32x32 and also implemented some basic world generation using Perlin noise:

There was no real lighting done, but I simply lowered the brightness of the voxels based on their height, which gave a nice look:

Then, I altered the generation code to create peaks and valleys in a surface, which looked more like a terrain of hills:

It was at this point where I realized that the chunk generation was very slow. I was generating these chunks as the player moved around, and the chunks had to also be sorted by distance to the player in order to generate and render correctly.

This meant that I would have to do quite a lot of refactoring in order to make this run at any acceptable level of performance, including switching to C++ and implementing multi-threading. 

I will be posting updates on my progress in voxels using C++ OpenGL in the future.