Voxel engine 2

From 11-02-2024 to present

This is my second attempt at a 3D voxel engine. I set out to write a traditional meshed voxel engine with the following traits:

cubic chunks
level of detail
asynchronous chunk loading
animated textures
randomised textures
connected textures
non-cube voxel meshes
dynamically coloured voxels
ambient occlusion
per-face raytraced lighting
coloured lighting
semi-transparency
cross-chunk structure generation
dynamic occlusion
unreasonable render distance
greedy meshing

This project is still a work-in-progress. Surprisingly, I spent most of my time getting player movement to work properly and it's still broken.

Chunks

The chunk loader manages N asynchronous workers, each of which can be assigned a chunk to process. The amount of workers is fixed, but could be determined based on CPU core count. Chunks start out as simple integer coordinates (chunk ID). The world save is queried for the ID and loaded if found, otherwise the chunk is sent off to the world generator. Once the block IDs are loaded, the chunk is meshed. Meshing works by asking each block ID's geometry generator what to do given the chunk and voxel position. Geometry can be generated based on neighbouring voxel IDs and neighbouring chunks. The generated mesh is uploaded to the GPU using a vertex buffer pool, and marked for rendering.

During all this, the chunk ID is reserved to prevent other workers from messing with it. Chunk processing can be cancelled, which is occasionally done to prevent chunks from loading when the player has already left the area (e.g. when moving really quickly). Ambient occlusion is performed per-voxel as a post processing step. Because each voxel can define its own geometry, ambient occlusion has to be voxel specific. A visibility graph is maintained to occlude invisble chunks, but this has not been fully implemented yet.

LOD

When a chunk is generated, an LOD has to be provided. The LOD is based on the distance from the chunk to the camera, and it determines the amount of voxels per meter in a chunk. For distant chunks, larger voxels are acceptable, so the chunk must shrink in capacity and grow in size. This is practically unnoticeable. but allows for greater render distances. In my implementation, chunk skirts are not yet implemented. This causes visible gaps between LOD transitions. The transition also causes ambient occlusion to become inaccurate, as neighbouring voxels differ in size to the voxels in the current chunk. The result is an unwarranted shaded edge at the chunk boundary once the LODs match. The solution to this is to recalculate ambient occlusion if the chunk's neighbour is updated.

Physics

Collision detection and response is handled using raycasts and point queries. It's commonly known that voxel worlds can be efficiently traversed, and the project uses this algorithm to do so. Collision response was especially difficult to get right, and it still breaks occasionally. The concept seems pretty simple. The world is made of axis aligned bounding boxes, so you can check each component of the movement vector for contacts. Once a contact is found, the component is adjusted to prevent intersection. The component is also removed from the velocity vector. This allows the object to slide over the surface, as it should. This process is repeated a couple of times per frame, because a correcting adjustment in one direction may cause an intersection in another direction. This approach works for a infinitely small point in space, but some moving objects are cuboids. Instead of having to cast a ray (a sweep of a single point), I now have to test a swept cuboid. The resulting shape could have many points of contact at the same time. This is improperly accounted for in the current implementation and the player can get stuck.

Thoughts

This approach is nothing new. My goal with this project was to make an videogame, not a novel voxel engine. However, I now realise that some of my needs can't be satisfied by this system. Fixed size cubic chunks and LOD don't work together nicely, unlike the intuitive combination of octrees and LODs. The memory and processing advantages of this would allow me to render world at a practically infinite render distance, especially when combined with a logarithmic depth buffer and raymarching. While I am reluctant to these ideas because I feel like they are too complex, and would get in the way of actually writing the game itself, I might reconsider if I ever start over.

Dependencies

Walgelijk
Game engine
Present