Anime Grass Shader

I am trying to make a shader that gives the look of anime grass, like this picture:


Using very rough textures (Provided at the end) I’ve been able to achieve this so far:

It’s nice, but I built it using bits and pieces from various shaders I found online, it’s lacking some flexibility/functionality, and I honestly don’t understand most of it.
Here is the shader code:

Shader "FX/BlowingAnimeGrass"
{
   Properties
   {
       [Space]
       [Header(Terrain)]
       _Color("Color", Color) = (1,1,1,1)
       _MainTex("Albedo (RGB)", 2D) = "white" {}
       _Glossiness("Smoothness", Range(0,1)) = 0.5
       _Metallic("Metallic", Range(0,1)) = 0.0
       [Space]
       [Header(Grass Noise and Movement)]
       _GrassTex("Grass Texture", 2D) = "white" {}
       _WindTex("Distortion Texture", 2D) = "white" {}
       _HorSpeed("Horizontal Flow Speed", Range(0,4)) = 1
       _VertSpeed("Vertical Flow Speed", Range(0,4)) = 1
       _GrassScale("Grass Scale", Range(0,1)) = 0.4
       _WindScale("Wind Noise Scale", Range(0,1)) = 0.4
       _NoiseScale("Side Noise Scale", Range(0,1)) = 0.04
   }
       SubShader
       {
           Tags { "RenderType" = "Fade" "Queue" = "Transparent" }
           LOD 200

           CGPROGRAM
           // Physically based Standard lighting model, and enable shadows on all light types
           #pragma surface surf Standard fullforwardshadows
           // Use shader model 3.0 target, to get nicer looking lighting
           #pragma target 3.0

           sampler2D _MainTex, _GrassTex, _WindTex;

           struct Input
           {
               float2 uv_MainTex;
               float3 worldNormal; INTERNAL_DATA// world normal built-in value
               float3 worldPos; // world position built-in value
           };

           float4 _WindTex_ST, _WorldSize, _WaveSpeed, _GrassTex_ST;
           half _Glossiness;
           half _Metallic;
           fixed4 _Color;
           fixed _HorSpeed, _GrassScale, _WindScale, _NoiseScale, _VertSpeed;

           // Add instancing support for this shader. You need to check 'Enable Instancing' on materials that use the shader.
           // See https://docs.unity3d.com/Manual/GPUInstancing.html for more information about instancing.
           // #pragma instancing_options assumeuniformscaling
           UNITY_INSTANCING_BUFFER_START(Props)
               // put more per-instance properties here
           UNITY_INSTANCING_BUFFER_END(Props)

           void surf(Input IN, inout SurfaceOutputStandard o)
           {
               // get the world normal
               float3 worldNormal = WorldNormalVector(IN, o.Normal);
               // normal for triplanar mapping
               float3 blendNormal = saturate(pow(worldNormal * 1.4, 4));
           
               // use world normal for flow
               //float3 flowDir = -(worldNormal * 2.0f) - 1.0f;
               float3 flowDir = -(worldNormal * 2.0f) - 1.0f;
           
               // horizontal flow speed
               flowDir *= _HorSpeed;

               // flowmap blend timings for grass
               float timing = frac(_Time.y * 0.5f + 0.5f);
               float timing2 = frac(_Time.y * 0.5f);
               float timingLerp = abs((0.5f - timing) / 0.5f);


               // flowmap blend timings for wind
               float windtiming = frac(_Time.y * 0.5f + 0.5f);
               float windtiming2 = frac(_Time.y * 0.5f);
               float windtimingLerp = abs((0.5f - windtiming) / 0.5f);

               // move 2 Wind textures at slight different speeds fased on the flowdirection
               half4 windTex1 = tex2D(_WindTex, IN.worldPos.xz * (_WindScale / 50) + (flowDir.xz * windtiming));
               half4 windTex2 = tex2D(_WindTex, IN.worldPos.xz * (_WindScale / 50) + (flowDir.xz * windtiming2));

               // move 2 Grass textures at slight different speeds fased on the flowdirection
               half4 grassTex1 = tex2D(_GrassTex, IN.worldPos.xz * (_GrassScale / 50) + (flowDir.xz * windtiming));
               half4 grassTex2 = tex2D(_GrassTex, IN.worldPos.xz * (_GrassScale / 50) + (flowDir.xz * windtiming2));

               // vertical flow speed
               float vertFlow = _Time.y * _VertSpeed;

               //Wind motion
               float4 windNoiseTexture = lerp(windTex1, windTex2, windtimingLerp);

               // noise sides
               float4 noisetexture = lerp(grassTex1, grassTex2, timingLerp);

               // add noise to normal
               o.Normal *= noisetexture;

               // Albedo comes from a texture tinted by color
               float4 grassWind = noisetexture * windNoiseTexture;

               fixed4 c = tex2D(_MainTex, IN.uv_MainTex) * _Color;
               o.Albedo = grassWind.a + c.rgb;

               // Metallic and smoothness come from slider variables
               o.Metallic = _Metallic;
               o.Smoothness = _Glossiness;
               o.Alpha = c.a;
           }
           ENDCG
       }
           FallBack "Diffuse"
}

How it works is a windblown grass texture scrolls across the surface, being affected by a caustics texture to make it less uniform. The result is then overlayed on top of whatever texture is on the surface. There are some issues with it though as outlined below:

I would like to have more control over which direction it flows (Currently based somehow off of worldNormal.
Also, better control over how fast it flows (Currently tied closely into the timing of the cross-fading for some reason)
It would also be great to have the Tiling of the textures in the editor actually affect their size.
Finally, I’d like to have it only project on the top of the surface.

It’s taken me almost a week to get it to this point, and I’ve learned a lot! But my head is splitting from all the different factors involved in making this. Anyone have any ideas?

Here are the two textures for the grass (Note, Animegrass has colors inverted so you can actually see it):

Progress update. Added individual controls for the two scrolling grass textures, and the two scrolling “wind” textures.
Still need to figure out how to get it to only project from the top, and how to change it’s direction.

Shader "FX/BlowingAnimeGrass"
{
    Properties
    {
        [Space]
        [Header(Terrain)]
        _Color("Color", Color) = (1,1,1,1)
        _MainTex("Albedo (RGB)", 2D) = "white" {}
        _Glossiness("Smoothness", Range(0,1)) = 0.5
        _Metallic("Metallic", Range(0,1)) = 0.0
        [Space]
        [Header(Grass Noise and Movement)]
        _GrassTex("Grass Texture", 2D) = "white" {}
        _GrassSpeed("Grass Speed", Range(-10, 10.0)) = 1
        _GrassScale("Grass Scale", Range(0,1)) = 0.4
        _Grass2Scale("Second Layer Grass Scale", Range(0, 2)) = 1
        _GrassBlendSpeed("Grass Blend Speed", Range(0, 1)) = .5
        [Space]
        [Header(Wind Noise and Movement)]
        _WindTex("Distortion Texture", 2D) = "white" {}
        _WindSpeed("Wind Speed", Range(-10, 10.0)) = 1
        _WindScale("Wind Noise Scale", Range(0,1)) = 0.4
        _Wind2Scale("Second Layer Wind Scale", Range(0, 2)) = 1
        _WindBlendSpeed("Wind Blend Speed", Range(0, 1)) = .5
    }
        SubShader
        {
            Tags { "RenderType" = "Fade" "Queue" = "Transparent" }
            LOD 200

            CGPROGRAM
            // Physically based Standard lighting model, and enable shadows on all light types
            #pragma surface surf Standard fullforwardshadows
            // Use shader model 3.0 target, to get nicer looking lighting
            #pragma target 3.0

            sampler2D _MainTex, _GrassTex, _WindTex;

            struct Input
            {
                float2 uv_MainTex;
                float3 worldPos; // world position built-in value
            };

            half _Glossiness;
            half _Metallic;
            fixed4 _Color;
            fixed _GrassScale, _WindScale;
            float _GrassSpeed, _Grass2Scale, _GrassBlendSpeed, _WindBlendSpeed, _WindSpeed, _Wind2Scale;

            // Add instancing support for this shader. You need to check 'Enable Instancing' on materials that use the shader.
            // See https://docs.unity3d.com/Manual/GPUInstancing.html for more information about instancing.
            // #pragma instancing_options assumeuniformscaling
            UNITY_INSTANCING_BUFFER_START(Props)
                // put more per-instance properties here
            UNITY_INSTANCING_BUFFER_END(Props)

            void surf(Input IN, inout SurfaceOutputStandard o)
            {
                // blend timings for grass
                float grasstiming = frac(_Time.y * _GrassBlendSpeed + 0.5f);
                float grasstiming2 = frac(_Time.y * _GrassBlendSpeed);
                float grasstimingLerp = abs((0.5f - grasstiming) / 0.5f);
               
                float grassScrollSpeed = _Time.x * _GrassSpeed;
                // move 2 Grass textures at different speeds
                fixed4 grassTex1 = tex2D(_GrassTex, IN.worldPos.xz * _GrassScale / 50 + grassScrollSpeed);
                // same noise, different scale
                fixed4 grassTex2 = tex2D(_GrassTex, IN.worldPos.xz * (_GrassScale / 50 * _Grass2Scale) + grassScrollSpeed * _Grass2Scale);


                // blend timings for wind
                float windtiming = frac(_Time.y * 0.5f + 0.5f);
                float windtiming2 = frac(_Time.y * 0.5f);
                float windtimingLerp = abs((0.5f - windtiming) / 0.5f);

                float windScrollSpeed = _Time.x * _WindSpeed;
                // move 2 Wind textures at different speeds
                fixed4 windTex1 = tex2D(_WindTex, IN.worldPos.xz * _WindScale / 50 + windScrollSpeed);
                // same noise, different scale
                fixed4 windTex2 = tex2D(_WindTex, IN.worldPos.xz * (_WindScale / 50 * _Wind2Scale) + windScrollSpeed * _Wind2Scale);


                //Wind motion
                float4 windNoiseTexture = lerp(windTex1, windTex2, windtimingLerp);

                //Grass motion
                float4 grassTexture = lerp(grassTex1, grassTex2, grasstimingLerp);

                //Combine wind and grass
                float4 grassWind = grassTexture * windNoiseTexture;

                // add to normal (doesn't work ATM)
                //o.Normal *= grassWind.a;

                // Albedo comes from a texture tinted by color
                fixed4 c = tex2D(_MainTex, IN.uv_MainTex) * _Color;
                o.Albedo = grassWind.a + c.rgb;

                // Metallic and smoothness come from slider variables
                o.Metallic = _Metallic;
                o.Smoothness = _Glossiness;
                o.Alpha = c.a;
            }
            ENDCG
        }
            FallBack "Diffuse"
}
2 Likes

It Does’nt receive shadows?

Does this work with URP?