Dynamic Grass System in Unity?

I’ve been interested in creating an expansive wheat field in a game I’m developing and I noticed this video for an Unreal Engine asset.

Around 0:19 it shows the character walking through the wheat field and interacting with it, which is exactly what I need. I’ve looked and found some grass systems but they all seem relatively low to the ground and have very limited interaction.

To break it down what I’m looking for is:

  • Interactive - Can walk through and make changes to the grass
  • Wind Effects - Waving wheat and adjustable intensity
  • LOD - further away, less detail to save on memory

I’ve looked for a breakdown on how some grass systems work but I haven’t found much; I don’t know whether to use billboards or meshes, if it should just be a replacement for unity’s built-in grass shader or volumetric. Potentially I would really love to be able to paint the grass on to a terrain but I could work with whatever.

I doubt this shader has distance LOD or is interactive, but take a look at this:

https://www.reddit.com/r/Unity3D/comments/6disnt/my_attempt_at_creating_geometry_shader_grass/

I actually followed the sources and found this reddit post which is what that guy based his grass on. The creator actually made video tutorial series as well!

I followed along the tutorial and ended up with this as the result:


This is obviously a little bit overkill and its still pretty basic (e.g. no lighting, no deformation, no LOD, etc.) but I was really impressed by how well it could render such an absolutely ridiculous amount. I definitely learned a lot from the tutorials about geometry shaders, so shout out to World of Zero!

keep us informed of progress :smile:

Got the lighting sort of working:


So the bottom section is not getting any light (besides ambient) and I think its because the quads making the objects are vertical planes which according to the lighting math shouldn’t receive any light. Should I apply some sort of offset to combat this?
This is my code for calculating lighting so far (I’m not the best at shaders as a warning):

            half4 frag(g2f IN) : COLOR
            {
                /*
                    g2f contains {pos, norm, uv, LIGHTING_COORDS}
                */
                fixed4 c = tex2D(_MainTex, IN.uv);
                clip(c.a - _Cutoff);

                half3 worldNormal = UnityObjectToWorldNormal(IN.norm);
                half nl = max(0, dot(worldNormal, _WorldSpaceLightPos0.xyz));
                fixed3 diff = nl * _LightColor0.rgb;
                fixed3 ambient = ShadeSH9(half4(worldNormal, 1));
                fixed attenuation = LIGHT_ATTENUATION(IN);

                fixed3 lighting = diff * attenuation + ambient * 2;
                c.rgb *= lighting;
                return c;
            }

Anyone have any pointers to a solution?

Also I’m doing a pass for the shadow caster and I noticed that, since my object is essentially 6 quads (sense they intersect there has 2 per fin, if there is a better way to do this I would love to know :smile:), only the last quad to be built by the geometry shader is actually casting shadows (as seen in 1 below). Will I need to do a pass for each of the quads?

Lastly it seems that my shadows overlay on my objects despite not actually casting on to the object (which is in 2 and 3 below). Is this something to do with how I calculate shadows in my fragment shader above?

I don’t know exactly what procedure you are using, but yeah, the quad must have normal in the direction of the grass upright, not relative to their mesh. That’s how it’s done for short fur and hair too.

Beyond that I know nothing about geometry shader and haven’t look at the tut you almost linked to.

Good news, the shadows seem to be working now! I condensed my geometry code so now it only uses 12 vertices and 3 quads, TriangleStream.RestartStrip() was what I was needing. And I also started calculating my normal for each quad which gave me proper lighting however only on half of the area.

Both areas should be getting about the same amount of light however the left side remains un-shaded. Any clues as to what may be causing this?

Here is my code for the shader, its a bit rough but if there’s any confusion I should be able to clarify.

Shader "Custom/GrassGeomShader" {
    Properties{
        _MainTex("Albedo (RGB)", 2D) = "white" {}
        _Cutoff("Cutoff", Range(0,1)) = 0.25
        _GrassHeight("Grass Height", Float) = 0.25
        _WindSpeed("Wind Speed", Float) = 100
        _WindStrength("Wind Strength", Float) = 0.5
    }
    SubShader{
        Tags{
            "Queue" = "Geometry"
            "RenderType" = "Opaque"
        }
        LOD 200
        Pass
        {
            Name "ForwardBase"
            Tags { "LightMode" = "ForwardBase"}
            CULL OFF

            CGPROGRAM
            #include "UnityCG.cginc"
            #pragma vertex vert
            #pragma fragment frag
            #pragma geometry geom
            #include "Lighting.cginc"
            #pragma multi_compile_fwdbase
            #include "AutoLight.cginc"

            #pragma target 5.0

            sampler2D _MainTex;

            struct v2g
            {
                float4 pos : SV_POSITION;
                float3 norm : NORMAL;
                float2 uv : TEXCOORD0;
                float3 color : TEXCOORD1;
            };

            struct g2f
            {
                float4 pos : SV_POSITION;
                float3 norm : NORMAL;
                float2 uv : TEXCOORD0;
                LIGHTING_COORDS(1,2)
            };

            struct QuadComponents {
                float3 v0;
                float3 v1;
                float3 norm;
                float3 color;
            };

            half _GrassHeight;
            half _Cutoff;
            half _WindStrength;
            half _WindSpeed;

            void BuildQuad(QuadComponents quadComp, float3 displacement, inout TriangleStream<g2f> triStream) {
                g2f OUT;
                OUT.norm = cross(quadComp.norm, quadComp.v0 + displacement);
              
                float3 position[4];
                float2 uv[4];
                int vertexIndices[4] = { 0,1,2,3 };
                position[0] = quadComp.v0 - displacement * 0.5 * _GrassHeight; 
       uv[0] = float2(0, 0);
                position[1] = quadComp.v1 - displacement * 0.5 * _GrassHeight; uv[1] = float2(0, 1);
                position[2] = quadComp.v0 + displacement * 0.5 * _GrassHeight; uv[2] = float2(1, 0);
                position[3] = quadComp.v1 + displacement * 0.5 * _GrassHeight; uv[3] = float2(1, 1);

                for (int i = 0; i < 4; i++) {
                    OUT.uv = uv[vertexIndices[i]];
                    OUT.pos = UnityObjectToClipPos(position[vertexIndices[i]]);
                    TRANSFER_VERTEX_TO_FRAGMENT(OUT);
                    triStream.Append(OUT);
                }

                triStream.RestartStrip();
            }

            v2g vert(appdata_full v)
            {
                v2g OUT;
                OUT.pos = v.vertex;
                OUT.norm = v.normal;
                OUT.uv = v.texcoord;
                OUT.color = tex2Dlod(_MainTex, v.texcoord).rgb;
                return OUT;
            }

            [maxvertexcount(12)]
            void geom(point v2g IN[1], inout TriangleStream<g2f> triStream)
            {
                QuadComponents quadComp;
                quadComp.v0 = IN[0].pos.xyz;
                quadComp.v1 = IN[0].pos.xyz + IN[0].norm * _GrassHeight;

                float3 perpendicularAngle = float3(0, 0, 1);
                quadComp.norm = cross(perpendicularAngle, IN[0].norm);
                quadComp.color = (IN[0].color);

                float3 wind = float3(sin(_Time.x  * _WindSpeed + quadComp.v0.x * _WindSpeed) + sin(_Time.x * _WindSpeed + quadComp.v0.z * 2), 0, cos(_Time.x  * _WindSpeed + quadComp.v0.z) + cos(_Time.x  * _WindSpeed + quadComp.v0.x *2));
                quadComp.v1 += wind * _WindStrength;

                float sin60 = 0.866f;
                float cos60 = 0.5f;
              
                //Creates inner section grass mesh - Can be replaced
                BuildQuad(quadComp, perpendicularAngle, triStream);
                BuildQuad(quadComp, float3(sin60, 0, cos60), triStream);
                BuildQuad(quadComp, float3(sin60, 0, -cos60), triStream);
            }

            half4 frag(g2f IN) : COLOR
            {
                /*
                    g2f contains {pos, norm, uv, LIGHTING_COORDS}
                */
                fixed4 c = tex2D(_MainTex, IN.uv);
                clip(c.a - _Cutoff);

                half3 worldNormal = UnityObjectToWorldNormal(IN.norm);
                half nl = max(0, dot(worldNormal, _WorldSpaceLightPos0.xyz));
                fixed3 diff = nl * _LightColor0.rgb;
                fixed3 ambient = ShadeSH9(half4(worldNormal, 1));
                fixed attenuation = LIGHT_ATTENUATION(IN);

                fixed3 lighting = diff * attenuation + ambient * 2;
                c.rgb *= lighting;
                return c;
            }
        ENDCG
        }
    }
}
1 Like

I found another useful project. Open source from a graphics guy who works at Unity - GitHub - keijiro/KvantGrass: Animating grass shader for Unity

Seems interesting I’ll have to look more into it.

Update on the grass shader: I got lighting working! All that I needed to change was line 64 so that it wasn’t adding v0 to displacement (displacement being the angle at which the new quad is to be built). I also added a wind texture lookup rather than doing a calculation to find it so it can be more deliberate rather than random, although its very simple and needs to be rewritten.

I posted a gif of it in action here

3 Likes

Hey man, any update on this? can u share your work so we can improve it? Im trying to reproduce the same experiment for learning purposes, if u can share it would be aweasome!