World loading and rendering techniques

Last week, I implemented a new and useful feature into Metaworld: the view distance modifier, in the graphical options. Not only does it allow to lower the view distance (woah) but also to increase it (incredible). That small feature allowed me to make some tests about the maximum render distance that will be supported, and it’s not too bad, as you can see in this video:

That maximum render distance was determined by efficiency, using an arbitrary ratio framerate/distance. Above the value I determined (4000 units), adding view distance costs too much frames per second to be worth it. That maximum value runs quite well on my computer, at more than 100 frames per second, what allowed me too record the above video with no framerate problem.

Setting the view distance distance at the maximum is completely useless gameplay-wise. It does not give any kind or advantage, you just see very far heights that are minutes away from you, and use more resources from your computer. I mostly see it like a luxury feature. The default of 2000 units, half as much as the maximum, is already more than enough for proper gameplay.
This being said, setting the vie distance to 4000 units gives the world a strong depth, it’s quite immersive and impressive to see. Thus, it’s fully justifiable if immersion is what you seek (and now, I only hope to be able one day to add an Oculus Rift/Vive support to the game).

When I linked the video on Twitter, I was asked some questions about how the game is dealing with this render. I quickly answered, but Twitter is far from being the ideal platform for details. Thus, here are some more.

Loading

At the moment, at the maximum view range, and on my development computer, the loading is rather quick (~3 seconds), considering the amount of things displayed on the screen at max range.

There are multiple reasons to it:

– First of all, I must make it clear that the terrain is generated from 2D data. It might not be obvious to notice this detail without playing the game, but the terrain is generated from a heightmap. There is only one layer of floor, thus it is made of very simple meshes, devoid of any vertical hole. It requires very few calculations to generate meshes from such data, making the file loading the bottleneck instead of the CPU.

– The entire environment, that is procedurally placed on the map, is cached. What qualifies as “environment” is basically every static object visible above the terrain: trees, plants, etc. The environment effects on the terrain such as an altered ground color or a shadow are also cached (shadows are tile-based for gameplay purposes, making it worth to store them).
When this cache is initially generated, it takes a lot of time to be built, but once it’s available, the loading is very fast. While moving, more cache is generated, but it does not impact the game framerate at normal gameplay speeds. However, it tends to create hang ups when moving quickly (for example when I use the map editor).

– The environment does not have a lot of variations at the moment, limiting the amount of meshes to load.

Rendering

Whether a terrain chunk is displayed or not is done through a basic frustum culling. There’s no culling of chunks hidden behind other chunks at the moment, the engine relies on the GPU z testing. When drawn, the chunks are more or less sent by a front to back order to the GPU to capitalize on the z test. However, the ordering is not very precise at the moment, which makes it very likely that some chunks are rendered when they shouldn’t.
Conclusion: the terrain rendering can still be perfected.

Everything else in the world: the trees, the plants, the rocks, the houses, etc, are voxel sprites displayed through hardware instancing. To avoid too much repetition of the environment, models are rotated, and modelized so that there’s no feature standing out too much that would make the lack of diversity too obvious.
For each chunk in the view frustum, the engine calculates the distance from objects above a chunk to the camera, and uses it to determine a LOD value. Following the value, a more or less detailed mesh is displayed. This task is not done continuously, since it would likely be too costly CPU-wise. It is only executed when a chunk enter or leave the view frustum.

More questions? Feel free to ask!