Geometry Shader Generated Grass

By Joppe Min


Foreword

Playing games during my Game Art career I’ve always been interested in the way grass was placed in certain games. When I just started out I’d make individual Grass objects and place these at random locations around the scene, not only making it look strange but also bad for CPU performance. 

I went and looked at games with relatively large terrains including grass such as ‘The Legend Of Zelda: Breath Of The Wild’ and ‘Genshin Impact’. Wanting to know how to create something similar. 

I knew it was created using shaders but was unaware how this would be achieved. That’s what I will try to figure out.

Grass as shown in ‘The Legend Of Zelda: Breath of the Wild’ – Nintendo.

1. Starting off

1.1. Concepting stage

My goal is to create a Shader that Draws Grass on the surface of a mesh. This grass will have to have certain behaviour such as:

  • Choose custom image and colors.
  • Receive Shadows.
  • Sway with the ‘wind’.
  • Grass amount choice
  • Have LOD’s or will cull on Draw distance.
  • Will not spawn on surfaces with a Red Vertex Color. 
  • Be Dynamically displaced by objects on its surface.

These goals were set after inspecting grass made in games such as ‘Sea of Thieves’, ‘The Legend of Zelda: Breath of the Wild’ and ‘Genshin Impact’. Using these games as reference can help me with the decision making process throughout my research.

Exemplary image, 2D representation of the shader

1.2. Testing environment

To test the Grass spawning, Shadow catching and Vertex color masking I’ve created this testing environment. Using this I can quickly see the results of my shader while simultaneously working on it.

Testing environment recreated in Blender

The Image above shows the current testing environment with vertex colors displayed. The Blue areas will mask out the creation of new triangles. On the plane a shadow will be cast to see how the grass will respond to casted shadows.

To assist me with the Shader making process there is a community made cheat sheet available created by Ilavsky (2017) as the Unity documentation tends to be hard to search.


2. Geometry stage

2.1. Creating Cards

The Geometry stage happens after the Vertex stage and before the Fragment stage. Using the previous vertex stage the geometry stage uses the vertex information to create new possible geometry.

To get familiarized with Geometry shaders I followed a guide by Ross (n.d.) where it is shown how to generate Points and add them to a Triangle stream. Triangle streams make sure the previous point is automatically attached to the newly added point unless a Restart strip is specified. Restarting a strip, like the name suggests, allows you to generate a seperated mesh from the previous triangle group.

In this case I’m using existing vertices to place Geometry in their place. I draw two Quads (a square made up of two triangles), one aligned with the X-axis and the other with the Z-axis. I then also place these Quads into UV space, allowing them to receive a texture.

Two crossed Quads creating one Patch of grass. Fragment pass already applied.
struct geometryOutput
{
	//Information Geometry requires to be sent to the Fragment Shader
	float4 pos : SV_POSITION;
	float2 uv : TEXCOORD0;
	LIGHTING_COORDS(1, 2)
};
 
geometryOutput VertData(float3 pos, float2 uv)
{
	//Filling a Geometry-Vertex with information
	geometryOutput o;
	o.pos = UnityObjectToClipPos(pos);
	o.uv = uv;
	TRANSFER_VERTEX_TO_FRAGMENT(o);
	return o;
}
[maxvertexcount(8)]
void geo(triangle vertexOutput IN[3] : SV_POSITION, inout TriangleStream<geometryOutput> geo)
{
//*RETRIEVING ADDITIONAL INFO HERE, I.E. VERT POSITION, COLOR, UV, NORMAL, TANGENT*
	//first quad
	geo.Append(VertData(float3((-_CardSize / 2), 0, 0)), float2(0, 0));
	geo.Append(VertData(float3((_CardSize / 2), 0, 0)), float2(1, 0));
	geo.Append(VertData(float3((-_CardSize / 2), 0, _CardSize)), float2(0, 1));
	geo.Append(VertData(float3((_CardSize / 2), 0, _CardSize)), float2(1, 1));
 
	//start second quad
	geo.RestartStrip();
	geo.Append(VertData(float3(0, (-_CardSize / 2), 0)), float2(0, 0));
	geo.Append(VertData(float3(0, (_CardSize / 2), 0)), float2(1, 0));
	geo.Append(VertData(float3(0, (-_CardSize / 2), _CardSize)), float2(0, 1));
	geo.Append(VertData(float3(0, (_CardSize / 2), _CardSize)), float2(1, 1));
}

2.2. Fragment shader appearance

Using the UV’s of the quads we can apply an alpha texture to this Mesh. every quad will have the same texture applied to them.
To give the texture color we take two exposed color properties and lerp these colors over the Y-axis of the UV coordinates. after that we multiply these colors with the alpha grass texture to create the stylized look of the grass texture.

To make sure the Quads are sorted correctly in 3D space the fragment shader has to write to the ZBuffer using “ZWrite On”. Because classic alpha transparency can’t be written to the ZBuffer correctly according to Unity Technologies (n.d.-a) we have to apply “AlphaToMask” to make sure that transparent pixels are removed completely. this way the render will sample the texture behind those removed textures.

Grass alpha texture and the colors lerped over the Y-axis to create the stylized grass appearance.
Classic Alpha One-Minus transparency (left) compared to AlphaToMask transparency (right)

2.3. Sampling Vertex Colors and Culling

Retrieving the Vertex colors from the Vertex shader I can multiply this with the card size to scale based on the redness of the source vertex. If that card size is below a certain threshold the vertices won’t be appended to the geometry meaning they are effectively culled.
After testing this method to see if these non-placed vertices are still affecting performance it shows that when cards aren’t appended they will not lower my FPS. However, because a geometry shader requires a maximum vertex count these unused vertices are loaded into memory.

Secondly, using an exposed Parameter, a culling distance was added. using a distance value the grass cards exceeding that distance from the camera will not be drawn.

float3 color = IN[0].color;
float viewDistance = distance(wpos, _WorldSpaceCameraPos);
 
if (_CardSize * color.< _CardRemovalVal || viewDistance > _RenderDistance)
	return;
 
geo.Append(VertData(pos + mul(transformationMatrix, float3(0, (-_CardSize / 2) * color.r, 0)), float2(0, 0)));
The testing environment. Applying Vertex Color as a Size indicator and Culling the smallest sizes.

2.4. Density using Tesselation

In between the Vertex and Geometry pass a Tesselation pass can be applied. Tesselation is the creation of vertices within a pre-existing quad or triangle. This will in turn create more vertices for our geometry shader to apply grass cards on.

Unity’s default Tesselation is only to be used for with a Surface shader. Because we are using a Geometry shader we can’t make use of the Surface shader which in turn also excludes us from using Unity’s default Tesselation.

To make sure the scope of this project doesn’t get out of hand I used Jasper Flick’s (2017) Tesselation project from Catlikecoding.com. I’ve modified the code slightly to support our shader’s functionality.
I’ve added interpolation of vertexcolors and implemented a world position input. This world position input is needed for our Worldspace Render Target in the next section.

Identical shaders with No Tesselation on the left and 3 Tesselation stages on the right

3. Displacement

3.1. Wind from Sample Texture

Overlaying a worldspace texture allows us to sample a the Red and Green channels of this texture. These values can then be used to move the vertices in the X- and Z-Axis. When panning this texture over time it will seem like wind is pushing the grass in these directions.

A visualisation of the overlaying texture, displacing the top points of the grass. Panning over time seemingly Emulating wind.

3.2. Worldspace Render Target

Needing a Render Target to displace grass but keeping performance and scalability in mind I found a way to simulate a Worldspace Rendertarget.
Being unfamiliar with the usage of RenderTargets a guide was followed created by Minions Art (2019).
Using the following steps you can use a single optimized Render Target for any terrain, including Open-world and Randomly generated environments.

  • Attach a Top-down Orthographic camera to the player. The size of this camera indicates the edge of how far displacement will be sampled to the texture.
  • Set the background color of this camera to hex #808000, this will be a neutral color where the red and green channels are set to 0.5.
  • Add a new Layer to Unity called “Displacement” or something similar and add this layer as a Culling mask to our created camera.
  • Create a Custom Render Texture asset and add it to our created camera. Because we will only sample sideways displacement, two axis, the Color Format can be set to R8G8_UNORM for lowest impact to performance. The texture size can also be relatively small as it will only be used to displace vertices, users won’t see this low resolution texture.
  • In the Vertex stage of the shader you can sample the Rendertexture and project this into worldspace using the following line of code, where _RenderCamSize is the previously set bounds, wpos is this worldposition of the shader vertices and _PlayerPos is the player position in 3D space sent as a global shader variable in Unity’s Update function:
o.uv = TRANSFORM_TEX((o.wpos.xz - _PlayerPos.xz + _RenderCamSize)  , _RenderTexture) / _RenderCamSize * 2;
  • To affect this Render target we can create a particle system that is only visible on the previously created “Displacement” layer, the grass shader will sample the red and green values of the particle system to displace grass in our grass shader.

This Render texture is added to the Geometry pass of the shader similar to how the wind was added. only affecting the top vertices of the grass.

Adding both the wind and the Render displacement together will look like this in code.

float2 windSample = (tex2Dlod(_WindDistortionMap, float4(uv, 0, 0)).xy * 2 - 1) * _WindStrength;
 
float3 wind = float3(windSample.x, 0, windSample.y);
 
float2 wposUV_tex = tex2Dlod(_RenderTexture, float4(wposuv.xy, 0, 0));
float3 renderDisplacement = (float3(wposUV_tex.- 0.25, 0, wposUV_tex.- 0.25)) * _DisplacementStrength;
 
geo.Append(VertData(pos + renderDisplacement + wind * (color.* _CardSize) + mul(transformationMatrix, float3((-_CardSize / 2) * color.r, 0, _CardSize * color.r)), float2(0, 1)));
The player projecting its Particle System to the Shader. With the resulting Rendertarget on the left and the affected Grass on the right

4. Shadows

4.1. Writing to the Shadowmap

Following Unity Technologies (n.d.-b) example. We use built in keywords for lighting, the color of shadows can be sampled in the Fragment shader as a black and white map where black is in shadow and white is lit. the following keywords are placed in their respective sections:

LIGHTING_COORDS(1, 2) in the struct initializes the mapping of lighting, writing to texture coordinates 1 and 2 respectively.
TRANSFER_VERTEX_TO_FRAGMENT(o); Transfers the vertex position onto the Shadowmap.
LIGHT_ATTENUATION(i) references these points in the fragment shader and can be used to color the shadow.

Clamping the Light attenuation allows the use of a slider to choose how dark the shadow is supposed to be.

The resulting shadows being casted on the grass.

4.2. Shadow issues and workaround

Our current shadows are being projected as if the shadow is being cast on a flat surface. This looks fine from a higher angle though when observed from a side angle these shadows are being drawn in front of the grass which looks strange.

To hide this effect from the side view we apply an inverted dot product of the vertex normal of this surface and multiply this with the shadow, this way the shadow fades away when the camera observes the surface from a side angle.

The previous shadow issue where shadows are rendered in front of the side-view

5. Conclusion

5.1. Final Result

The result of this project is an Unlit stylised grass shader which supports Shadows. The shadow intensity is modifyable in the shader itself. Because this shader is using geometry shader pass instead of grass as object it allows the entire grass patch to be created using a single draw call.

Vertex coloring makes sure grass can be painted on any environment or object. With some modifications this shader could also create flowers using a different sample color and shape.

Having a Render Target enabled allows for Displacement using Particle systems, Line renderers and other Billboard systems. Because this shader uses layers for particle systems it allows for simulating bursts of wind from explosions, swinging swords or speeding bullets. This is infinitely scalable and optimized for any environment.

Resulting appearance showing 4 variations of the same shader.
Exposed properties of the main material shown.

5.2. Future Reference

If I would redo this project in the future I would want to spend more time looking into the different ways I could have approached this shader. My main concern is the amount of support available for geometry shaders as some Hardware/Platforms do not support Geometry shaders according to Unity Technologies (2018), there seems to be a lot more support for Compute Shaders instead.
Though I haven’t really looked in to Compute Shaders these shaders seem to give better performance and are becoming more common to use according to internet forums.

Though I couldn’t figure out the lighting issue this would be my first improvement I would give to this shader as the current shadows don’t look appealing. I would also add support for other light sources as the current version only supports one directional light. adding more light sources would require an extra shader pass.


Sources

Ross, E. R. (n.d.). Unity Grass Geometry Shader Tutorial at Roystan. Roystan.Net. Retrieved October 1, 2020, from
https://roystan.net/articles/grass-shader.html

Flick, J. (2017, November 30). Tessellation. Catlikecoding.
https://catlikecoding.com/unity/tutorials/advanced-rendering/tessellation/

Minions Art. (2019, October 6). Shader Graph Interactive Water(with Refraction/Grabpass, Intersection/Foam lines). Patreon. https://www.patreon.com/posts/shader-graph-30490169

Unity Technologies. (n.d.-a). ShaderLab: Blending. Unity Documentation. Retrieved October 5, 2020, from
https://docs.unity3d.com/Manual/SL-Blend.html

Unity Technologies. (n.d.-b). Vertex and fragment shader examples. Unity Documentation. Retrieved October 15, 2020, from
https://docs.unity3d.com/Manual/SL-VertexFragmentShaderExamples.html

Unity Technologies. (2018, March 20). Shader Compilation Target Levels. Unity Documentation.
https://docs.unity3d.com/Manual/SL-ShaderCompileTargets.html

Ilavsky, J. (2017). Unity Shaders Cheat Sheet. Github.
https://gist.github.com/Split82/d1651403ffb05e912d9c3786f11d6a44

Chadwick, E. (2017, June 18). GrassTechnique. Polycount.
http://wiki.polycount.com/wiki/GrassTechnique

Related Posts