Home

Awesome

Unity-Grass

Grass rendering experiment in Unity. Uses an HLSL compute shader and graphics shader to draw procedurally generated blades on the GPU.

Essentially an implementation of the approach used in Ghost of Tsushima, detailed in this incredible talk: Procedural Grass in 'Ghost of Tsushima' .

The grass is quite performant (although there are crucial optimisations that should be added, like LODing). The look and movement of the grass is highly customisable and can be changed using various parameters.

NOTE: This shader is not intended for commercial use. It would probably be difficult to do so anyway, as it is tailored to my needs. Nevertheless, I have put the code up, with explanation, as part of my portfolio.

Screenshot (4)

<img src = "https://user-images.githubusercontent.com/52975691/212654275-0cc1cb56-3e68-475a-be8d-7b0a78378228.png" width ="400" /> <img src = "https://user-images.githubusercontent.com/52975691/212654303-5e9fba2c-40c2-4017-88dd-ef455d575a4f.png" width ="400" />

<img src = "https://user-images.githubusercontent.com/52975691/212654333-c546aaf9-8883-437e-b208-f92c74dd3a3f.png" width ="400" /> <img src = "https://user-images.githubusercontent.com/52975691/212654378-c91c0c3a-d840-4e8b-a0ce-565c2d80d604.png" width ="400" />

Key Features:

Video

IMAGE ALT TEXT HERE

Overview

A compute shader is run: each thread of the compute shader computes a single blade of grass. First, a position is computed: the blades are evenly spread across the terrain and slightly jittered. We check if the grass blade should be rendered by doing frustum and distance culling on the position. If the blade should be rendered, we continue, else we drop out. Each blade belongs to a particular clump. Each clump type has its own set of artist-authored parameters that determine things like the height, bend, and color of the blade. The computed parameters for the blade are packed into a GrassBlade struct and added to an AppendBuffer.

NOTE: It is most convenient to use an AppendBuffer as opposed to a RWStructuredBuffer because the number of blades rendered varies per frame due to frustum and distance culling. It is possible to use a RWStructuredBuffer though as demonstrated in Acerolas video about grass rendering.

The vertex shader is then told to render many blades of grass using Graphics.DrawProceduralIndirect(). The blades of grass were modelled in Blender and have data packed into the vertex colors, such as how far along the blade the vertex is, and which side of the blade its on.

NOTE: The vertex shader needs to know how many blades to draw. This is achieved by copying the size of the AppendBuffer into the indirectArgsBuffer of DrawProceduralIndirect() using ComputeBuffer.CopyCount().

In the vertex shader, we can index into the GrassBlades buffer (that lives on the GPU) to get the parameters for our current blade. The vertex is placed based on a Bezier curve determined by the GrassBlade parameters. Since we are using Bezier curves it is also easy to get the normal for the vertex, crossing the tangent of the curve (easily computable), with the side facing vector. We also animate the blade in the vertex shader by moving points of the Bezier curve based on the windForce.

In the fragment shader, we do lighting (Phong Shading) and coloring of our procedurally generated geometry.

Details:

Shape of blades

The shape of the blades is determined by evaluating cubic Bezier curves. Each blade is 15 vertices which are placed along a Bezier curve in the vertex shader. The Bezier curve is defined by its 4 Bezier control points, which are determined based on parameters such as height, width, tilt, and bend, with random variation between blades.

The parameters of a grassblade are contained in the GrassBlade struct:

struct GrassBlade {

    float3 position; 
    float rotAngle; 
    float hash; 
    float height; 
    float width;
    float tilt; 
    float bend; 
    float3 surfaceNorm;
    float3 color; 
    float windForce;
    float sideBend;
    float clumpColorDistanceFade;
};

The blades are also tapered down along the length.

Wind animation

The wind animation is driven by scrolling 2D perlin noise. The noise is inputted to a sin-based function that modulates various parameters of the grass such as its Bezier control points and its facing direction.

Grass clumping:

The way the grass clumps together in the field can be controlled, for a less uniform, more organic look. This is meant to mimic the way grass grows in patches in nature.

The grass is divided into cells generated using a procedural Voronoi algorithm. Each cell is assigned a clump id indexing into the list of user defined clumps.

Each clump type has its own set of artist-authored parameters. All grass that belongs to that clump will use these parameters.

struct ClumpParametersStruct {

      float pullToCentre;
      float pointInSameDirection;
      float baseHeight;
      float heightRandom;
      float baseWidth;
      float widthRandom;
      float baseTilt;
      float tiltRandom;
      float baseBend;
      float bendRandom;
      
};

These parameters can be used to achieve various effects, like pulling grass towards the center point of its clump, or controlling how much the grass in a clump points in the same direction.

Clever tricks (mostly from the Tsushima grass talk)

Redistributing vertices of grass towards tip

Often most of the bend in grass blades is in the tip. If the verts are evenly distributed, this results in wasted verts used to represent straight geometry at the bottom of the blade. Verts can be distributed more towards the tip of the blade (where they are needed) by tweaking a parameter.

Curved normals

The normals of the blade can be tilted outwards to give the appearance of curvature. This helps the blades look more 3D, and fuller, without adding extra verts.

Blending normals to surface normal at distance

Even with temporal anti-aliasing the grass can be very grainy and aliased at distance, due to the constant movement of the blades, and the glossy specular highlights. To alleviate this, the normals of the blades can be lerped towards the normal of the underlying terrain at distance. This results in less noisiness and graininess because the normals vary less in screen space.

Some other tricks that I used for distant grass:

Realigning blades verts when viewed side-on

Often the player will be looking at the grass exactly side on - rendering it very thin, or even invisible. In such cases the verts can be tilted to more face the player's view. This is achieved by slightly shifting the verts when the blade's facing is roughly orthogonal to the view vector. Each vert is rotated about the tangent to the Bezier curve at that point.

To-do

Resources

I used a lot of online resources to learn the techniques used. I don't remember everything that I referenced but I'll list the main ones: