GPU Instanced Foliage

Januray 24th, 2025
Mika and the Witch's Mountain

Motivation

During the initial prototype phase of Mika and the Witch's Mountain, a basic foliage system was developed that allowed the team to populate the island with grass. However, it was not easy to maintain or configure and it was expensive to render, specially on weaker hardware. For this reason, I decided to do some research on foliage rendering that led me to explore different ways to get the results we wanted.

Mika Foliage

Original implementation

I wasn't part of the original team that programmed the first foliage system, and they had already left by the time I came in. However, I checked their implementation and it was very easy to understand:

This seems very straightforward and performant at first since everything except rendering is calculated offline. However, it doesn't really scale that well:

Possible implementations

Taking into account the limitations of the original system, I focused on finding ways to overcome them. These are some of the possible solutions I found:

After reviewing the solutions described above, I decided to give GPU Instancing a try because it would allow us to render a lot more foliage in the long run.

The GPU Instancing system

There were some gaps to fill at first, like defining how to generate the blades, how to color them, and how to store them. The resulting workflow that I defined was:

That entire process is automated using buttons in the inspector once the required references are set up. This way, it becomes very easy to iterate on. When the game starts, it just needs to deserialize the blades, set a compute buffer with their data and issue a gpu instanced draw call for the amount of blades required. The shader uses a keyword to identify that it is using GPU Instancing, and then retrieves each instance data accordingly in the vertex shader. We ended up storing the next variables for each instance:

The blade height was used to color tall grass a bit different and make the final result more interesting. As for the random value, we used it to create a smooth LOD transition. Foliage is not only frustrum culled, groups at a certain distance from the camera are also culled, but we didn't want the foliage to just pop in. Instead, we used a random value to cull certain blades in the vertex shader before culling the entire group. This way, individual blades pop in gradually instead of displaying the entire group at once.

Additionally, we scaled down the blades when they were very close to the culling threshold, which looks almost as if blades were growing up progressively as you get close to them. These two techniques combined made the LOD transition seamless.

Results

GPU Instancing allowed us to render millions of blades of grass and flowers very fast, even on weaker hardware. In the game, there is a zone called Windy Meadows where a vast field populated with foliage can be seen. This is a very GPU-intensive zone, but we managed to get it running at native 4k60fps on PS5 with a very high level of detail. I don't think this would have turned out so well without GPU Instancing based on our initial tests with the original implementation. On weaker hardware, like Nintendo Switch, the game maintains 1080p30fps using a lower level of detail all things considered.

Foliage Off Foliage On
Foliage Off vs On
Foliage Off Foliage On
Foliage Off vs On
Foliage Off Foliage On
Foliage Off vs On
Foliage Off Foliage On
Foliage Off vs On
Foliage Off Foliage On
Foliage Off vs On

Going Forward

Our system still had room for a lot of improvement. It could be easier to iterate on and understand for artists, and it could be faster to generate the instances. If I had to code this again, I would try:

Some new shading techniques could potentially make this GPU Instancing system obsolete, like Mesh Shaders. However, they are not currently supported by Unity, but in other contexts they should be considered and I'd love to give them a try in the future.

Mika Foliage
Mika Foliage
Mika Foliage