FeaturedImage_Grass_shader_in_unity
Effects Shader

Tutorial How To Make An Interactive Grass Shader In Unity

You will learn how to write a geometry shader to generate blades of grass and make it interact with objects moving through it.
This tutorial is the step-by-step guide on how to add interactivity to the grass shader. It is the extension of the geometry grass shader tutorial by Roystan. Here I would like to show how to improve it with a very simple interactivity example. The shader will take the position and radius of the interacting object and apply displacement for the closest grass blades.

As a starting point you can use my code available at GitHub. Or you can complete Roystan’s tutorial yourself and get the same result as my code. I would recommend you to do it as it is the very detailed step-by-step guide on how to generate grass blades in the geometry shader. The only difference with my code is that I have not added lighting, shadows, and therefore normals for the sake of simplicity of the effect code.



MonoBehaviour Component

Let’s start with passing parameters from an object to the shader. The position and radius of a game object that can interact with the grass should be passed into the shader so it can react accordingly. For that you need to create a simple MonoBehaviour component that has a reference to the grass material and passes _Trample property to the shader each frame:

[ExecuteInEditMode]
public class GrassTrample : MonoBehaviour
{
    [SerializeField] private Material material;
    [SerializeField] [Range(0, 10)] private float radius;
    [SerializeField] [Range(-1, 5)] private float heightOffset;
    private Transform cachedTransform;
    private readonly int grassTrampleProperty = Shader.PropertyToID("_Trample");
    
    private void Awake()
    {
        cachedTransform = transform;
    }

    private void Update()
    {
        if (material == null)
        {
            return;
        }

        var position = cachedTransform.position;
        material.SetVector(grassTrampleProperty, new Vector4(position.x, position.y + heightOffset, position.z, radius));
    }
}

Add this component to the game object that will interact with the grass and add a reference to the material that the grass plane is using via inspector and GrassTrample serialized field material.



Shader

Properties

Now lets modify the GrassShader.shader file. Add the following properties to the shader properties block:

Properties
{
    ...
    [Header(Wind)]
    _WindDistortionMap("Wind Distortion Map", 2D) = "white" {}
    _WindFrequency("Wind Frequency", Vector) = (0.05, 0.05, 0, 0)
    _WindStrength("Wind Strength", Float) = 1

    [Header(Trample)]
    _Trample("Trample", Vector) = (0, 0, 0, 0)
    _TrampleStrength("Trample Strength", Range(0, 1)) = 0.2
}

_Trample is passed from the MonoBehaviour component . And _TrampleStrength would allow us to tune the effect’s strength.
Then add the same properties into the CGINCLUDE block:

CGINCLUDE
#include "UnityCG.cginc"
#include "Autolight.cginc"
#include "CustomTessellation.cginc"

...

sampler2D _WindDistortionMap;
float4 _WindDistortionMap_ST;
float2 _WindFrequency;
float _WindStrength;

float4 _Trample;
float _TrampleStrength;

struct geometryOutput
{
	float4 pos : SV_POSITION;
	float2 uv : TEXCOORD0;
};

...

Grass Interaction

We start with grass piercing the object:

Now let’s modify the grass blade generation algorithm to add interaction. As the first iteration we can find the direction in which we should move each vertex. To do it we should take the current vertex position and subtract the trample position. Then multiply the result with _TrampleStrength property to control the strength of the effect.

void geo(triangle vertexOutput IN[3] : SV_POSITION, inout TriangleStream<geometryOutput> triStream)
{
    ...
    for (int i = 0; i < BLADE_SEGMENTS; i++)
    {
        float t = i / (float)BLADE_SEGMENTS;
        float segmentHeight = height * t;
        float segmentWidth = width * (1 - t);
        float segmentForward = pow(t, _BladeCurve) * forward;

        float3x3 transformMatrix = i == 0 ? transformationMatrixFacing : transformationMatrix;

        float3 trampleDiff = pos - _Trample.xyz;
        pos += trampleDiff * _TrampleStrength;

        triStream.Append(
            GenerateGrassVertex(pos, segmentWidth, segmentHeight, segmentForward, float2(0, t), transformMatrix));
        triStream.Append(
            GenerateGrassVertex(pos, -segmentWidth, segmentHeight, segmentForward, float2(1, t), transformMatrix));
    }

    triStream.Append(GenerateGrassVertex(pos, 0, height, forward, float2(0.5, 1), transformationMatrix));
}

It is something already, grass is pushed around the object, but not quite what we are looking for. Let’s normalize trampleDiff in order to make it of length 1, to avoid that the further vertices are pushed even further away from the object.

 
float3 trampleDiff = pos - _Trample.xyz;
float4 trample = float4(
    float3(normalize(trampleDiff).x,
           normalize(trampleDiff).y,
           normalize(trampleDiff).z),
    0);
pos += trample * _TrampleStrength;

Already looks kinda better, but we need to use the radius of the game object in order to affect only the closest grass blades. To achieve that, we can multiply the trample vector by

 
(1.0 - saturate(length(trampleDiff) / _Trample.w))

_Trample.w is the radius of the object, so we divide the length of tramplleDiff by the radius and clamp it between 0 and 1 using saturate function. So if the length of trampleDiff is bigger than the radius, then the result of the division would be more than 1. But we need the multiplier to be 0 in such cases, since the effect should not be applied if length is bigger than radius. So we invert the multiplier after it is clamped by subtracting it from 1.0. Therefore the closer the position of our object to the current vertex the closer the value to 1.0. Finally, multiply the vector by this value to get a gradual displacement of vertices around the position of our object.

 
float3 trampleDiff = pos - _Trample.xyz;
float4 trample = float4(
    float3(normalize(trampleDiff).x,
           normalize(trampleDiff).y,
           normalize(trampleDiff).z) * (1.0 - saturate(length(trampleDiff) / _Trample.w)),
    0);
pos += trample * _TrampleStrength;

Much better, grass now bends around the ball.

But it is also pushed below the plane, so we should just use 0 instead of the Y component and that’s it.

 
float3 trampleDiff = pos - _Trample.xyz;
float4 trample = float4(
    float3(normalize(trampleDiff).x,
           0,
           normalize(trampleDiff).z) * (1.0 - saturate(length(trampleDiff) / _Trample.w)),
    0);
pos += trample * _TrampleStrength;

Now the first segment of a grass blade does not clip through the plane when affected by the object.


Previously it was not that visible, but if the radius is set to be bigger, then we can notice that tips are pointed directly upwards.

To fix this we also need to calculate the trample for the tip to displace it too. Add the same code after the loop.

[maxvertexcount(BLADE_SEGMENTS * 2 + 1)]
void geo(triangle vertexOutput IN[3] : SV_POSITION, inout TriangleStream<geometryOutput> triStream)
{
	...

	for (int i = 0; i < BLADE_SEGMENTS; i++)
	{
		...
	}
	
    float3 trampleDiff = pos - _Trample.xyz;
    float4 trample = float4(
        float3(normalize(trampleDiff).x,
               0,
               normalize(trampleDiff).z) * (1.0 - saturate(length(trampleDiff) / _Trample.w)),
        0);
    pos += trample * _TrampleStrength;
    triStream.Append(GenerateGrassVertex(pos, 0, height, forward, float2(0.5, 1), transformationMatrix));
}

Instantly looks much better now when radius is big.


Let’s also follow the DRY principle and remove code duplication by extracting the calculation into a separate method.

float4 GetTrampleVector(float3 pos)
{
    float3 trampleDiff = pos - _Trample.xyz;
    return float4(
        float3(normalize(trampleDiff).x,
               0,
               normalize(trampleDiff).z) * (1.0 - saturate(length(trampleDiff) / _Trample.w)),
        0);
}

[maxvertexcount(BLADE_SEGMENTS * 2 + 1)]
void geo(triangle vertexOutput IN[3] : SV_POSITION, inout TriangleStream<geometryOutput> triStream)
{
    ...

    for (int i = 0; i < BLADE_SEGMENTS; i++)
    {
        ...

        float4 trample = GetTrampleVector(pos);
        pos += trample * _TrampleStrength;
       
        triStream.Append(
            GenerateGrassVertex(pos, segmentWidth, segmentHeight, segmentForward, float2(0, t), transformMatrix));
        triStream.Append(
            GenerateGrassVertex(pos, -segmentWidth, segmentHeight, segmentForward, float2(1, t), transformMatrix));
    }

    float4 trample = GetTrampleVector(pos);
    pos += trample * _TrampleStrength;
    triStream.Append(GenerateGrassVertex(pos, 0, height, forward, float2(0.5, 1), transformationMatrix));
}

If the plane’s position is (0, 0, 0), then everything will look fine here. But once the plane is moved, the trample effect stays in its previous place.

Unity_grass_shader_showoff_v1_incorrect

To accommodate for that we need to subtract the plane position from the trample position (if anyone knows a better way, feel free to share in the comments):

float4 GetTrampleVector(float3 pos, float4 objectOrigin)
{
    float3 trampleDiff = pos - (_Trample.xyz - objectOrigin);
    return float4(
        float3(normalize(trampleDiff).x,
               0,
               normalize(trampleDiff).z) * (1.0 - saturate(length(trampleDiff) / _Trample.w)),
        0);
}
float4 objectOrigin = mul(unity_ObjectToWorld, float4(0.0, 0.0, 0.0, 1.0));

for (int i = 0; i < BLADE_SEGMENTS; i++)
{
    float t = i / (float)BLADE_SEGMENTS;
    float segmentHeight = height * t;
    float segmentWidth = width * (1 - t);
    float segmentForward = pow(t, _BladeCurve) * forward;
    float3x3 transformMatrix = i == 0 ? transformationMatrixFacing : transformationMatrix;

    float4 trample = GetTrampleVector(pos, objectOrigin);
    pos += trample * _TrampleStrength;

    triStream.Append(
        GenerateGrassVertex(pos, segmentWidth, segmentHeight, segmentForward, float2(0, t), transformMatrix));
    triStream.Append(
        GenerateGrassVertex(pos, -segmentWidth, segmentHeight, segmentForward, float2(1, t), transformMatrix));
}

float4 trample = GetTrampleVector(pos, objectOrigin);
pos += trample * _TrampleStrength;
triStream.Append(GenerateGrassVertex(pos, 0, height, forward, float2(0.5, 1), transformationMatrix));

Now we get the correct displacement for grass blades nevertheless the plane position.

Unity_grass_shader_showoff_v1_correct

The effect already looks good, but under close inspection you could see that the base of each blade is affected by the trample effect and grass is not only bent but also literally moved aside:

Unity_grass_shader_showoff_v2_incorrect_base_moving

If this effect is used in the game with another camera setup and general focus on the main character, then most likely no one will notice it, but I still want to tackle this issue. Basically we want to remove the trample for the first blade segment. The naive approach would be adding condition to not apply it and that’s it.

if (i < 0)
{
    float4 trample = GetTrampleVector(pos, objectOrigin);
    pos += trample * _TrampleStrength;
}

Or a more fancy way to do this. It should not have much difference performance-wise (you can read more on shader branching here or here, but do not believe me or anyone else undoubtedly, profile your code if it is a performance critical section, your case could be an exception):

float4 trample = GetTrampleVector(pos, objectOrigin);
pos += trample * _TrampleStrength * saturate(i);

Now the base always stays in the same place and the effect looks better close-up.


Conclusion

It was my first attempt at writing at least some small shader effect myself. For many years I have been reading articles like the one listed in the beginning of the blog post, but never actually got down to write it following the tutorial, not speaking about extending it with additional effects or behaviour.
As you can see the hardest task here is to break down the effect into smaller steps (like the line with the trample effect taking a good paragraph to describe). And then implementation of each step is a lot easier, definitely not as hard as it seems at the start. The art of decomposition of such effects can come only with experience and the amount of shaders you have written, even following tutorials. So if you want to learn shaders just pull up the sleeves and try to implement effects from your favorite game or any effect you think looks cool.
The interaction I have added may look as an easy and simple task for experienced pals, but I love to see it working.

Share your thoughts about the tutorial in the comments or Twitter. And if you would like to get more tips and interesting game dev articles follow my telegram channel where I post stuff I find interesting every day.


Source code

A git repository with full source code and the scene setup is available at GitHub. Or the direct link to the shader itself if you just wanna grab it or compare with yours result: GrassShader.shader


Further steps

As an improvement you can add lighting and shadows. Again use the Roystan’s tutorial to add it for unaffected grass and improve it to apply lighting to displaced grass blades.


Leave a Reply

This site uses Akismet to reduce spam. Learn how your comment data is processed.

%d bloggers like this: