Problems with popular grass shader tutorial...!

Hey folks,

I’m trying to follow this tutorial, but I’m running into problems… can anybody suggest a solution?

I’m supposed to be getting a response like this…

…but instead, my model is showing up as this:

I assume this is to do with the relative positions of the origins on the respective models. Presuming this is to do with world origins… but trying to subtract -5 from the normalised values seems to do very little.
My code is as follows…

Shader "RS/LRGrass"
{
    Properties
    {
        _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

        _WindTex ("Wind Texture", 2D) = "white" {}
        _WorldSize    ("World Size", vector)        = (1,1,1,1)
        _WindSpeed ("Wind Speed", vector)        = (1,1,1,1)
    }
    SubShader
    {
        Tags { "RenderType"="Opaque" }
        LOD 200

        Pass
        {
            CGPROGRAM

            #pragma vertex vert
            #pragma fragment frag
           
            struct vertexInput
            {
                float4 vertex : POSITION;
                float3 normal : NORMAL;
            };

            struct vertexOutput
            {
                float4 pos : SV_POSITION;
                float3 normal : NORMAL;
                float2 sp : TEXCOORD0; //Test sample position
            };

            sampler2D    _WindTex;
            vector        _WorldSize;
            vector        _WindSpeed;

            vertexOutput vert(vertexInput input)
            {
                vertexOutput output;

                // convert input to clip and world space
                output.pos        = UnityObjectToClipPos(input.vertex);
                float4 normal4    = float4(input.normal, 0.0f);
                output.normal    = normalize(mul(normal4, unity_WorldToObject).xyz);

                // get vertex world position
                float4 worldPos = mul(input.vertex, unity_ObjectToWorld);
                // normalize position based on world Size
                float2 samplePos = worldPos.xz / _WorldSize.xz;

                // scroll sample position based on time
                // samplePos += _Time.x * _WindSpeed.xy;

                // Sample wind texture
                //float windSample = tex2Dlod (_WindTex, float4(samplePos, 0, 0));

                output.sp = samplePos; // Test sample position.

               

                return output;
            };
           
            float4 frag (vertexOutput input) : COLOR
            {
                return float4(input.sp.x, 0, 0, 1); // Temporary measure!
            };

            ENDCG
        }

        /*
        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;

        struct Input
        {
            float2 uv_MainTex;
        };

        half _Glossiness;
        half _Metallic;
        fixed4 _Color;

        // 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)
        {
            // Albedo comes from a texture tinted by color
            fixed4 c = tex2D (_MainTex, IN.uv_MainTex) * _Color;
            o.Albedo = c.rgb;
            // Metallic and smoothness come from slider variables
            o.Metallic = _Metallic;
            o.Smoothness = _Glossiness;
            o.Alpha = c.a;
        }
        ENDCG
        */
    }
    FallBack "Diffuse"
}

Can anybody help me out with this? Really confused here and I’d like to have it sorted soon…!

From my tests, the matrix and vertex need to switch order in the multiply, to this:

mul(unity_ObjectToWorld, input.vertex);

Perhaps Linden’s graphics settings are different, or it’s a typo.

Also note that in his image, all his grass starts at a positive coordinate. The scale multiply only adjusts for size of scene, does not fix things being negative.

Good luck
gdb

Look at this asset package. The guy did it based on that tutorial by Linden, but updated it, and added functionality. Well worth a study.

Hey folks, I’m aware that it’s almost a month after my initial posts, but I figured I’d post the solution we went with for our project - just in case anybody else is looking for resources on this topic (it was a pain in the ass for me!). Credit goes to our awesome guest programmer, who I’ll mention properly if I get permission.

It’s a bit of a mess, but our shader code was as follows…

// Upgrade NOTE: replaced 'mul(UNITY_MATRIX_MVP,*)' with 'UnityObjectToClipPos(*)'

Shader "RS/WindGrassRS"
{
    Properties
    {
        // Surface shader parameters
        _Color                ("Color", Color)                    = (1,1,1,1)
        _EmColor            ("Emission 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
        // Wind effect parameters
        _WindDirection        ("Wind Direction", vector)            = (1,0, 1,0)

        _WindTex            ("Wind Texture", 2D)                = "white" {}
        _WorldSize            ("World Size", vector)                = (1,1,1,1)
        _WindSpeed            ("Wind Speed", vector)                = (1,1,1,1)

        _OverallBillowEffect("Overall Billow Effect", Range(0,1))= 0.0

        //--- TRIPLANAR STUFF ---//

        _Texture1            ("Texture 1", 2D)                    = "white" {}
        _Texture2            ("Texture 2", 2D)                    = "white" {}
        _Texture3            ("Texture 3", 2D)                    = "white" {}
        _Scale                ("Scale", Range(0.001, 0.2))        = 0.1

        //--- OUTLINE STUFF ---//
        //_Outline            ("_Outline", Range(0,0.1))            = 0
        //_OutlineColor        ("Outline Color", Color)                    = (1, 1, 1, 1)

        //--- DISSOLVE STUFF ---//
        _SliceGuide            ("Slice Guide (RGB)", 2D)            = "white" {}
        _SliceAmount        ("Slice Amount", Range(0.0, 1.0))    = 0.5

        _PercentSeaweed     ("Percentage Seaweed", float)       = 1
        _PercentStormy      ("Percentage Stormy", float)        = 0
        _PercentBreathing   ("Percentage Breathing", float)     = 0
    }


    SubShader
    {
        Tags {"RenderType" = "Opaque"}
      
        //Cull Front
        //Cull Off // DISSOLVE STUFF!

        LOD 200
        CGPROGRAM
/*
        Pass {
            Tags { "RenderType"="Opaque" }
            Cull Front

            CGPROGRAM
            #pragma vertex vert
            #pragma fragment frag
            #include "UnityCG.cginc"
            struct v2f {
                float4 pos : SV_POSITION;
            };
            float _Outline;
            float4 _OutlineColor;
            float4 vert(appdata_base v) : SV_POSITION {
                v2f o;
                o.pos = UnityObjectToClipPos(v.vertex);
                float3 normal = mul((float3x3) UNITY_MATRIX_MV, v.normal);
                normal.x *= UNITY_MATRIX_P[0][0];
                normal.y *= UNITY_MATRIX_P[1][1];
                o.pos.xy += normal.xy * _Outline;
                return o.pos;
            }
            half4 frag(v2f i) : COLOR {
                return _OutlineColor;
            }
            ENDCG
        }
        */

        #pragma surface surf Standard vertex:vert
       
        struct Input
        {
            // float2 uv_MainTex;

            //--- TRIPLANAR STUFF ---//

            float3 worldNormal;
            float3 worldPos;

            //--- DISSOLVE STUFF ---//
            //float2 uv_SliceGuide;
            //float _SliceAmount;

            //--- VAINS ---//
            float3 localPos; // Local position of vertex
            float electricalOffset;
        };
        //sampler2D    _MainTex;
        half        _Glossiness;
        half        _Metallic;
        fixed4        _Color;
        fixed4        _EmissionColor;
        float3        _WindDirection;

        sampler2D    _WindTex;
        float4      _WorldSize;
        float4        _WindSpeed;

        float        _OverallBillowEffect;

        //--- TRIPLANAR STUFF ---//
        sampler2D    _Texture1;
        sampler2D    _Texture2;
        sampler2D    _Texture3;
        float        _Scale;

        //--- DISSOLVE STUFF ---//
        sampler2D    _SliceGuide;
        float        _SliceAmount;

        float       _PercentSeaweed;
        float       _PercentStormy;
        float       _PercentBreathing;

        // our vert modification function
        void vert(inout appdata_full v, out Input o) // TODO: Add in out Input o...
        {
            UNITY_INITIALIZE_OUTPUT(Input, o);
            o.localPos = v.vertex.xyz;

            float4 localSpaceVertex = v.vertex;
            // Takes the mesh's verts and turns it into a point in world space
            // this is the equivalent of Transform.TransformPoint on the scripting side
            float4 worldSpaceVertex = mul( unity_ObjectToWorld, localSpaceVertex );
            // normalize position based on world Size
            float2 samplePos = ((worldSpaceVertex.xz + _WorldSize.xz/2) / _WorldSize.xz);

            // scroll sample position based on time
            samplePos += _Time.x * _WindSpeed.xy;

            // Sample wind texture
            const float da = 0.05;
            float windSample = tex2Dlod(_WindTex, float4(samplePos, 0, 0));
            windSample += tex2Dlod(_WindTex, float4(samplePos.x + da, samplePos.y, 0, 0));
            windSample += tex2Dlod(_WindTex, float4(samplePos.x, samplePos.y + da, 0, 0));
            windSample += tex2Dlod(_WindTex, float4(samplePos.x - da, samplePos.y, 0, 0));
            windSample += tex2Dlod(_WindTex, float4(samplePos.x, samplePos.y - da, 0, 0));
            windSample /= 5;


            // windSample = sin(_Time.x * 10); // TESTING: Shows that it is the windsample that's screwing it up :c.
          
            worldSpaceVertex.x += (windSample * _WindDirection.x * v.color) * _OverallBillowEffect;
            worldSpaceVertex.z += (windSample * _WindDirection.z * v.color) * _OverallBillowEffect;
            // takes the new modified position of the vert in world space and then puts it back in local space
            v.vertex = mul( unity_WorldToObject, worldSpaceVertex );

            // Stuff for vains

            float2 samplePos2 = ((worldSpaceVertex.xz + _WorldSize.xz / 2) / _WorldSize.xz);
            o.electricalOffset = tex2Dlod(_WindTex, float4(samplePos2, 0, 0));
          

            //float2 modelPos = mul(float4(0, 0, 0, 1), unity_ObjectToWorld).xz / _WorldSize.xz;
            //o.electricalOffset = (tex2Dlod(_WindTex, float4(modelPos, 0, 0)) + _Time.x)  % 1;
        }

        float getLightVal(float3 p1, float3 p2, Input IN) {
            // Unpacking vects
            float offsetMult = p1.x, freq = p1.y, timePercOutside = p1.z;
            float numWaves = p2.x;
            float sinMult = p2.y, sinAdd = p2.z;

            // Parametric func
            float off = (IN.electricalOffset*offsetMult + _Time.x * freq / timePercOutside) * timePercOutside;
            float t = -IN.localPos.y*numWaves + off;
            return clamp(sin(t) * sinMult + sinAdd, 0, 1);
        }
        void surf(Input IN, inout SurfaceOutputStandard o)
        {


            //--- TRIPLANAR STUFF ---//
            //fixed4 col1 = tex2D(_Texture2, IN.worldPos.yz * _Scale);// * _Color;
            //fixed4 col2 = tex2D(_Texture1, IN.worldPos.xz * _Scale);// * _Color;
            //fixed4 col3 = tex2D(_Texture3, IN.worldPos.xy * _Scale);// * _Color;

            fixed4 col1 = tex2D(_Texture2, IN.worldPos.yz * _Scale);// * _Color;
            fixed4 col2 = tex2D(_Texture1, IN.worldPos.xz * _Scale);// * _Color;
            fixed4 col3 = tex2D(_Texture3, IN.worldPos.xy * _Scale);// * _Color;


            float3 vec = abs(IN.worldNormal);
            vec /= vec.x + vec.y + vec.z + 0.001f;
            fixed4 col = vec.x * col1 + vec.y * col2 + vec.z * col3;

            /*
                       fixed4 slice1 = tex2D(_SliceGuide, IN.worldPos.yz * _Scale);
                       fixed4 slice2 = tex2D(_SliceGuide, IN.worldPos.xz * _Scale);
                       fixed4 slice3 = tex2D(_SliceGuide, IN.worldPos.xy * _Scale);

                       fixed4 sliceAll = vec.x * slice1 + vec.y * slice2 + vec.z * slice3;

                       clip (sliceAll.rgb - _SliceAmount);
                       */
            o.Albedo = col;
            //o.Emission = col + _EmissionColor;

            // Metallic and smoothness come from slider variables
            o.Metallic = _Metallic;
            o.Smoothness = _Glossiness;
            o.Alpha = col.a;

            // TEMP: Testing for veins - Base version w/ ~2 waves per tree


            // Sharper waves, visable best from the side. (STORMY)
            //o.Albedo = col / 2;
            //float off = (IN.electricalOffset*3.14 + _Time.x * 3) * 4;
            //float t = -IN.localPos.y / 3.14 + off;
            //o.Emission = col * clamp(sin(t) * 50 - 48.7, 0, 1);


            // Stormy (kinda)
            //o.Albedo = col / 2;
            //float off = (IN.electricalOffset*3.14 + _Time.x * 7) * 3;  // More 'breathing'
            //float t = -IN.localPos.y /1.5 + off;
            //o.Emission = col * clamp(sin(t) * 2 - 0.8, 0, 1);


            // SEAWEED
            //o.Albedo = col / 2;
            //float off = (IN.electricalOffset + _Time.x * 40);
            //o.Emission = 0.05 + col * (0.2 + 0.5*sin(-IN.localPos.y*2.3 + off));


            // SEAWEED (parametric)
            //float offsetMult = 1, freq = 40, timePercOutside = 1;
            //float numWaves = 2.3;
            //float sinMult = 0.5, sinAdd = 0.2;
            const float3 p1W = float3(1, 40, 1);
            const float3 p2W = float3(2.3, 0.5, 0.2);
            float sw = getLightVal(p1W, p2W, IN);
              
            // Stormy (kinda/breathing) (parametric)
            //float offsetMult = 3.14, freq = 21, timePercOutside = 3;
            //float numWaves = 1/1.5;
            //float sinMult = 2, sinAdd = -0.8;
            const float3 p1STB = float3(3.14, 21, 3);
            const float3 p2STB = float3(1 / 1.5, 2, -0.8);
            float stb = getLightVal(p1STB, p2STB, IN);


            // Stormy (parametric)
            //float offsetMult = 3.14, freq = 12, timePercOutside = 3;
            //float numWaves = 1 / 3.14;
            //float sinMult = 50, sinAdd = -48.7;
            const float3 p1ST = float3(3.14, 12, 3);
            const float3 p2ST = float3(1 / 3.14, 50, -48.7);
            float st = getLightVal(p1ST, p2ST, IN);


            // Interpolate
            //const float3 p1 = _PercentSeaweed * p1W + _PercentStormy * p1ST + _PercentBreathing * p1STB;
            //const float3 p2 = _PercentSeaweed * p2W + _PercentStormy * p2ST + _PercentBreathing * p2STB;

            float brightness = _PercentSeaweed * sw + _PercentStormy * st + _PercentBreathing * stb;
          
          
            // Shader vals...
            o.Albedo = col / 2;
            o.Emission = col * brightness;
        }
        ENDCG
    }
    FallBack "Diffuse"
}

@gamedevbill - Many thanks for the advice, man! I’m going to sit down and study that now at my leisure!

@DrDust265 - Really appreciate that, man! That tutorial drove me a little crazy, and I’d appreciate the chance to sit down and really study a fully-working version of the shader.

1 Like