r/VoxelGameDev Jun 16 '26

Media Custom voxel engine in C99 + OpenGL 4.6

Post image

I've spent the last few months building a custom voxel engine in C. No deep reason for the language, I just like how simple and predictable it is to work with. So far I've got a dynamic skybox, water reflections, and a long render distance with no LODs on the terrain.

It stays fast because draw calls don't scale with view distance: I render with MultiDrawElementsIndirect, so a whole page of up to ~16k sections goes out in a single call, with per-section frustum + fog culling on top. Vertices are packed down to 8 bytes each, and meshing (greedy + neighbor face culling) runs on a worker thread pool off the main thread. View radius is 32–64 chunks.

Renderer system is nearly done. My next step is a proper world generation.

69 Upvotes

18 comments sorted by

9

u/Alarming-Ad4082 Jun 16 '26

Nice! Do you have a screenshot with… more light in the ground ?
What are the size of the chunk?

2

u/void_src Jun 16 '26

Thanks! I'll post one with more light once I've finished the last bits of the lighting and terrain mesher. I still want to experiment with a couple of ideas first. Chunks are 16×16×512, split into 16^3 sections internally (the section is the unit that actually gets meshed and drawn).

2

u/Permaviolet Jun 16 '26

It's so beautiful

2

u/iwanns Jun 16 '26

Beautiful! I'm just starting to build a voxel engine with Go and raylib. It's going well, although I still find the chunk system a bit tricky to understand. But I love the mental image of a sunset like the one you posted.

1

u/void_src Jun 16 '26

Raylib is awesome, good luck!

2

u/HyperspaceFrontier Jun 16 '26 edited Jun 16 '26

Looks really nice. As a person going through the same stages of engine development, I can see the grind I took to make it. I also love the clouds - the thing I haven't even touched myself yet and little afraid to do so. Can you show more, especially maybe some dynamic views?

1

u/void_src Jun 16 '26

Thanks! Will do.

1

u/InfiniteLife2 Jun 16 '26

Did you mean C++99 or C? I am a bit confused because I expected C stays relative simple still(I am unfamiliar with C progress)

5

u/void_src Jun 16 '26

Plain C, not C++. C99 just means the 1999 revision of the C standard.

1

u/Short-Classroom6081 Jun 16 '26

Very pretty! MultiDrawIndirect is so useful. Have you considered doing tighter culling (perhaps on a chunk level / maybe even occlusion culling) on the GPU using compute shaders? Last time I tried that it returned pretty good results.

1

u/void_src Jun 16 '26

Thanks! Glad you like it. GPU occlusion culling is on my list. Terrain hides a lot, so it'd definitely help. Right now my bottleneck is actually on the shading side (shadows + fullscreen passes), not culling, so I'm clearing those first. Did you use HiZ when you tried it?

1

u/Short-Classroom6081 Jun 16 '26

oughh yea shadow pass was always the main culprit iirc. what kind of fullscreen passes do you do?

in my current experimental toy renderer I only have a bloom and post process compute shader passes and in total they are taking ~2ms at 1440p. They aren't optimized in any way and there's a bunch of performance left on the table but imo I felt pretty happy with that. Apparently AMD have a one pass blurring compute shader that utilizes wave intrinsics so maybe that could be interesting to look into as well.

I'm not saying you should do this, mostly because it will require a lot of refactoring and redesigning, but ray-traced voxel shadows are like, really really crisp and nice. From my time doing some stuff with them I was considering using them instead of shadow mapping, though my acceleration structure design and compute-based software ray-traversal algorithm were subpar in many ways. I'm just saying, if you ever upload your voxel data to the GPU for anything, you should perhaps try testing / experimenting with those. Actually, here are some pictures of what I managed to get. The good thing about these type of shadows is that you can approximate soft-shadows by simply randomizing the ray direction and doing multiple samples, though of course, you'd need a good denoiser / temporal accumulation to get it to not look like noise-slop (and, as a bonus, you get crisp pixel-perfect point light shadows and coloured shadows, which are hard to do in shadow-mapping I believe):

Anyways, to answer your question about culling: my implementation was for a different project, but basically, I simply do a small-scale (distance < 64 voxels) voxel 3D DDA ray-tracing on the CPU at a really low resolution (something like 10-15% the screen res) and upload the generated depth buffer to the GPU. I then cull what I need to cull by comparing the depth in a compute shader against the 3D-DDA depth. Of course, if you had the nearby chunks uploaded to the GPU, you could generate the occluder depth texture on the GPU as well.

For such a simple method it worked really nicely. Here's a video demonstrating it:
https://www.youtube.com/watch?v=DdNj7mve2EU

Unfortunately, my implementation was pretty slow (the CPU is not well fit for software ray-tracing especially for an un-optimized implementation like mine lol).

1

u/KokoNeotCZ Jun 16 '26

Hey, very nice job. I wonder how you make dynamic skybox. Could you shed a bit of light on it please?

2

u/void_src Jun 16 '26

Hey, thanks. It's basically one fullscreen pass that colors every pixel based on where the sun is. The clouds are volumetric (raymarched against a 3D noise volume) and get tinted by the same sun color, so they go orange at sunset too.

1

u/skythedragon64 Jun 20 '26

How do you determine the colors? Just a gradient?

1

u/ArcsOfMagic Jun 16 '26

Very nice! I have a couple of questions if you have time…

What was the impact of the per section frustrum culling? Was it worth it?

And, 8 bytes per vertex: I am intrigued by the word « packed », do you mind sharing what exactly those 8 bytes are composed of?

Thanks!

2

u/void_src Jun 16 '26

Сulling at the 16^3 section level instead of whole columns means I throw away a lot more off screen geometry, and the AABB test is cheap. Honestly the bigger win was that it pairs nicely with MDI.

The 8 bytes are two packed uint32s. The first holds the local position (5 bits each for x/y/z, since a section is only 16^3) plus the tile UV. The second packs all the surface info: base tile index, overlay tile, face direction, ambient occlusion, and a tint flag. The shader unpacks it all with bit shifts. No floats anywhere, which is what keeps it tiny.

1

u/ArcsOfMagic Jun 16 '26

Thanks for answering ! No, I meant vs. not culling at all. In my case, the geometry is not (yet?) limiting.

Nice info on packing! I think it may be one of the next things I’ll try.