Grass Rendering

C++

 

CUDA

 

CUSTOM-ENGINE

 

GRAPHICS

 

OPENGL

 

SCHOOL

 

During this project, I researched grass rendering and animation. The goals I set for myself were to render high quantities of geometry within short durations and make it modular. The final result reached those goals and more!

Grass and wind

The grass is created using a compute shader with CUDA. This generates all the geometry at the start of the game, preventing the use of a geometry shader. In this pass, I generate all the vertices and indices, so I can render it later using a triangle list. Research showed this was more efficient than using a triangle strip, that's why I chose this direction.

In the vertex shader, I perform the procedural animation. Using the offset from the base of the base of the grass blade, I can apply a certain amount of rotation to bend both forwards and sidewards. This effect can be used for both wind and displacement.

The wind is sampled from a Perlin noise function and applied to the rotation of the grass blade. Using several layers of noise and parameters to configure direction and strength, I created a dynamic wind system.

Displacements

The displacement system aims to improve the interaction with the grass. Instead of the grass being a static mesh, we want to receive feedback when moving through it. The goal here is that when an object moves through or stays still, the grass responds and moves out of the way.

I achieved this by making use of a displacement texture. In our world, we assign several components to our gameobjects;

  • Displacer
  • Focus

The displacer component determines what can displace the grass and affect it in a certain way. The focus component decides the relative centre of all displacements.

The strategy here is to write to a texture with colour coded information about the velocities of our displacer objects. The centre of this texture will be the focus object. This means that if we assign it to our player or camera, all displacements are relative to them and far away displacement don't even have to be registered.

Using reprojection, we can determine where the previous displacement was and apply that to our current frame. That allows me to fade the previous displacement over time, resolving in trails through the grass. This is a cheap solution for a complex problem.

Culling

To further increase the performance of the terrain, I decided on adding culling. This would help with reducing the amount of draw calls to chunks that are outside the camera frustum.

This was a relatively simple feature to add, since I could infer the AABB of the chunk by its size and position. The next step was to test this against the frustum of the camera and, based on the results, render it.

I implemented the rendering by exploiting the feature of 'tags' in EnTT (the ECS library I was using). You can use empty structs as tags for entities and filter by them. So, I added a Culled tag for the terrain entities and rendered all the entities that didn't have that tag.

Level of Detail

To further improve the performance of the terrain, I implemented levels of detail (LOD). This was a relatively easy process, since all the grass was already procedurally generated. I had full control over the number of vertices per blade of grass when creating the terrain.

The next step was to change this parameter based on the distance from the terrain chunk. When the camera reached a certain threshold, the terrain would be refreshed with a lower resolution version.

This meant that the chunk you are in (and close neighbouring ones) were high detail, but further away ones were much lower.

While this helped a bunch, there is still a problem here. There is a very high amount of overdraw, due to the amount of geometry in the scene. Here can still be optimized by using lower density grass fields in the distance and potentially removing the entire lower segments of the blades (since these will be shrouded by close by blades).