Tessellation

April 2025

Shore tessellation
Shore tessellation

Render quality and performance are always tricky to balance. One of the best things you can do to be able to render things at high quality levels on all devices, is not do things that don't matter. This month, I've built the systems that take care of not doing things. The most important techniques to do less are tessellation and culling.

Tessellation takes care of increasing the detail of shapes close to the camera, while reducing the number of triangles in the distance. This is especially useful when zooming out; frame rates would drop dramatically if the quality of all things remains the same. I've implemented two types of tessellation. The first is hardware tessellation, which is a feature (almost) all modern GPUs have. I've applied this first on the pond edges. Instead of modelling them with some sufficient level of detail, I don't model them at all anymore and instead just send the shape of the ponds to the GPU. The GPU then generates a number of triangles that's suitable for the distance of the shore edge to the camera. The image shows the same pond at two different distances. The close up has many triangles and a smooth shape, while the distant render just renders the contours without details that wouldn't show up at that distance. I'll use this technique for hills and the water surface as well.

Koi tessellation
Koi tessellation

The koi (and many other objects) need to vary in detail as well, but hardware tessellation isn't super flexible. It will work for some simpler shapes, but not for complex things like koi, plants and other objects in the scene. Instead of using hardware tessellation there, I generate a number of variants with varying levels of detail. Every frame, just before rendering starts, the GPU decides which variant is appropriate. Distant koi and small fry will use a very low poly model, but koi will still look very smooth up close.

Another thing that happens at the same time is culling. The GPU determines which things are entirely outside the camera view, and doesn't render them. Doing this on the GPU is much faster than doing it on the CPU, since it's very easy to cull many things in parallel, and the GPU is good at that. Still, visibility is determined for every object before every frame, so I'll have to do a rough visibility pass first. Sending literally every object in the scene through this process would be wasteful.

Koi by now consist of several separate components that are stored together. They have an object containing generic information about the koi, a bunch of information about their position and size, information about their models and animation properties and a brain. Together, all these components take up more and more size, and there's still much more to come. This means the computer has to load a lot of koi data from RAM to the CPU whenever I want to know anything about that koi, even if it's only its position. In fact, I only want to know a position and nothing else very often. When a big school of koi interact with each other, they need to know where all the others are, but there's no need to load the entire koi from RAM; that just means >95% of loaded memory isn't used. Unfortunately, loading things from RAM is one of the slowest things you can do. This seriously limits how much koi I can have actively swimming around in the scene.

Data oriented design
Data oriented design

Fortunately, there's a solution for this problem that's used in many games, and it's called "data oriented design". The image shows approximately how this works: instead of storing all koi data together, I split up the data in themed chunks, and I store those chunks together. When I want to load some data for a koi, I only load the things I need, which means koi interactions are now much faster to resolve.

Some game engines take this concept to an extreme, where everything is split up in this way. That's a bit unwarranted in my view, I need this for things like koi and possibly other things that appear frequently in the scene, but certainly not for everything. That's why I made this way of doing this optional in my engine. I'll probably use it for other things besides koi later, but it's optional and the system doesn't get in my way when I don't need it.

Navigation
Navigation

I'll implement more intelligent koi behavior soon. To allow koi to be smart, I'll have to give them all the information they need. That includes navigation data, like proximity to the shore, or the direction towards a nearby open space, but also information about neighbors. The data oriented design solution allows koi to query all kinds of information about each other without performance being a problem. They won't just react to other fish in general, they'll take each others properties into account. Koi can now detect whether another koi is similar to them, whether they should yield to an incoming school and how big and fast another koi is.

The game currently has very few assets, but their size is growing nonetheless. Something funny I discovered is that loading compressed asset files is faster than loading uncompressed data. Compressed files are of course nicer in terms of storage space, but I always expected that the required decompression time was the price you had to pay for it. It turns out loading from disk is so much slower than computation, that loading a smaller file and decompressing it is actually faster!