Grass

September 2025

Because I need image assets to render plants, I had to develop a system for storing and loading textures. These will all be loaded at startup while the game is loading other systems, so all textures can just be compiled into a big batch of data that needs to be unpacked and uploaded as soon as possible. This is very similar to what I'm already doing with the shaders, so I've extended the shader archive system to be used for more types of assets, like images. All source files are sorted by name, the names and pointers to the file bytes are stored in a header and the file data is appended after that.

Modern image formats are great. They compress images in optimal ways, and since images are one of the largest file types that are sent around, it helps when they can be compressed even a little bit more. These compressions algorithms are usually better than the Zlib compression I'm already using for shaders. It turns out:

  • PNG files are easily 20% smaller than my Zlib compressed files.
  • This advantage partially disappears when I compress all my textures in a single blob.
  • PNG decompression is slower than Zlib, which increases load times.
  • Smaller files load much faster, which partially negates that difference.

I settled on compressing textures using only Zlib for now, since it's very fast and the systems are already in place, although I may add one of the PNG pre-processing algorithms later to improve image compression a bit more. These pre-processing algorithms can be very simple, like storing the differences between neighboring pixels instead of the pixel values themselves, which is easy enough to try.

Flat planes versus curved planes
Flat planes versus curved planes

A common way to render grass and small plants is to render partially transparent images sticking from the ground. This works for some perspectives, but not all. The image on the right shows how bad vertical grass planes look if you view them top down, and since the camera in Koi Farm is free, this doesn't work well. A good solution is curving the planes towards the ground in a random direction, which is shown in the comparison. Curved textured partially transparent planes are therefore a good option for grass and small plants, but only if they are curved.

Sprites versus geometry
Sprites versus geometry

Another way to render grass is to render the entire shape as a model, not from an image. The image shows how that works out. Image based grass is shown on the left, while fully modelled grass blades are shown on the right. While the geometry looks nice in a way, it takes much more memory to store. Lighting effects tend to work better because the shapes are more detailed, but this can also be approximated using normal maps for textured grass.

Overdraw
Overdraw

A disadvantage of rendering small foliage is overdraw. This means that many pixels that were rendered to the render target are overwritten again because there's so much overlap. The image shows brighter colors where more overdraw exists, and it's clearly problematic near the horizon. This can be a big performance issue. When rendering fully modelled grass blades, overdraw can be reduced a little bit more at the cost of more memory usage, and by rendering fewer simplified objects in the distance, even more overdraw can be prevented. Still, rendering plants will most likely be the biggest source of overdraw in any renderer.

In addition to texture loading and grass experiments, the engine has been improved (and changed) significantly. This is the biggest Vulkan application I've ever written, and after about a year, I've learnt a lot more about modern graphics programming. Most of my experience comes from OpenGL and WebGL programming, but those APIs are quite old, so I'm used to do things in old-fashioned ways. With older APIs, it was necessary to carefully "feed" the GPU with all required data before a draw command is dispatched. That means setting up all required textures to their respective slots, configuring any data input streams, and lining up the correct transformation data before the GPU can start drawing. All this work takes time from me while programming the application, but also from the CPU which has to send many commands to the GPU, and it can become a bottleneck.

Modern GPUs aren't just faster, they also allow programmers to communicate with them in much more efficient and logical ways. I've implemented so-called bindless rendering techniques. This means I don't have to carefully feed data before every draw call, the shaders that run on the GPU can fetch that data themselves instead. They can access any texture or data source I've made accessible. While it took some time to implement, the codebase shrunk by a few thousand lines, and there are fewer points of failure now. Carefully setting up the right data before draw calls caused plenty of bugs, and I'm happy that's no longer possible. At the same time, the CPU now sends several times fewer commands to the GPU, improving performance across the board. Most of all, it allows me to program in a much less cumbersome way, adding new rendering systems is a lot easier now.

The only disadvantage is that older GPUs don't support these more modern ways of talking to them. I want the game to be playable on older PCs as much as possible, but there has to be a limit. I've decided to at least support 10 years old hardware when the game releases. My office PC uses a GTX 1070ti, so the Nvidia 10 series and up has guaranteed support. We also have an older GTX 760 around, which is probably just below the required specs.