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 a 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 on GitHub. Or you can complete Roystan’s tutorial yourself and get the same result as my code. I would recommend you do it as it is a 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 the 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 the _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 let’s 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:

Unity_grass_shader_starting_point

Now let’s modify the grass blade generation algorithm to add interaction. In 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 the _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));
}
Unity_grass_shader_first_step_small


It is something already, grass is pushed around the object, but not quite what we are looking for. Let’s normalize trampleDiff to make it of length 1, to avoid the further vertices being 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;
Unity_grass_shader_second_step_small


Already looks kinda better, but we need to use the radius of the game object 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 the 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 the length is bigger than the 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;
Unity_grass_shader_third_step_HQ1500_small

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.

Unity_grass_shader_pointed_upward_HQ1500_small

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 the 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 can see that the base of each blade is affected by the trample effect, and the 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 a general focus on the main character, then most likely no one will notice it, but I still want to tackle this issue. We want to remove the trample for the first blade segment. The naive approach would be adding the 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 at 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 behavior.
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 number of shaders you have written, even following tutorials. So if you want to learn shaders just pull up your sleeves and try to implement effects from your favorite game or any effect you think looks cool.
The interaction I have added may look like 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 on 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 it with your result: GrassShader.shader

Further steps

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


9 Comments

  1. Hi, This shader is not working on mac and iOS devices. Any idea how to make it mac (Metal API) compatible?

      1. Thanks for the tutorial. Works great for one object, but i am trying to get it to work with multiple objects. But got kinda stuck on how to get it working. Anybody idea?

          1. I’m trying to implement the loop thing to make it work with multiple characters, but it’s not working for me, where should it be implemented to work propertly? Also should the grass trample script be attached to every interactable object or it just needs one for the whole scene?

Leave a Reply to reteerCancel reply

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