
A practical approach to real-time grass deformation in games.
Interactive grass systems have become a standard feature in modern games, contributing significantly to environmental immersion. From The Legend of Zelda: Breath of the Wild to Genshin Impact, players expect vegetation to respond naturally to character movement. However, implementing such systems efficiently while maintaining performance can be challenging.
In this post, we'll explore how to achieve realistic grass interaction using normal maps - a technique that offers an excellent balance between visual quality and performance. We'll dive into the implementation details, discuss optimization strategies, and share insights from our experience integrating this system into production.
Why Normal Maps?
Traditional approaches to grass interaction often rely on complex physics simulations, which can be computationally expensive at scale. Normal maps provide a more efficient alternative by:
- Reducing CPU overhead through GPU-based calculations
- Enabling efficient batch rendering of grass instances
- Providing smooth, natural-looking deformation without complex physics
- Scaling well with varying levels of detail
Detailed Implementation
In this blog, we mainly focus on implementing a specific type of grass interaction: the circular pattern of grass displacement that occurs when a character pushes through a grassy field. This creates a natural-looking effect where grass bends away from the character in a radial pattern, as if being pushed aside by their movement.
While there are many possible patterns for grass interaction (such as weapon swing force, cutting area), understanding this circular displacement pattern provides a solid foundation for grass interaction systems. The techniques we'll discuss - particularly the use of a funnel-shaped mesh for force distribution - can be readily adapted to create other interaction patterns by modifying the mesh shape and normal distribution.
Once you understand how to implement this basic circular interaction, you'll have the knowledge needed to experiment with different force distribution patterns by simply modifying the interaction mesh shape and its normal properties.

This funnel-shaped mesh has several characteristics:
- In the central area (where the character stands), the mesh normals have XZ components approaching
1.0
. - As we move towards the edges of the funnel, these XZ components gradually decrease to
0
. - This natural falloff creates a perfect representation of force distribution: maximum force at the character's position, smoothly decreasing as we move outward.
- The shape effectively simulates how a character would influence surrounding grass in a realistic manner.
To capture and store these interaction forces, we have to use an orthographic camera called RTCamera
that is placed above the character, following their movement. This RTCamera
looks straight down at the funnel mesh and its orthographic size is specifically calculated to match the terrain dimensions. The camera renders to a RenderTexture (which we'll call _GrassMap
) that matches the terrain's dimensions. The funnel mesh's normals are captured and stored in this texture. Crucially, we only need the X and Z components of the normals, as these represent the direction and intensity of the grass bend.

Mesh normals naturally exist in the [-1, 1]
range. However, texture storage typically only supports [0, 1]
range in rgba
channels. To solve this, we transform the normal's XZ components from [-1, 1]
to [0, 1]
using the formula: normal.xz * 0.5 + 0.5
. This transformation preserves all the force information while making it suitable for texture storage. When there is no force applied (the default state), the value in the texture is (0.5, 0.5)
. This represents the state where grass stands straight up without any bending or deformation.
Here's a simplified version of the force map shader:
HLSL
struct Attributes
{
float4 positionOS : POSITION;
float2 uv : TEXCOORD0;
float3 normalOS : NORMAL;
float4 tangentOS : TANGENT;
};
struct Varyings
{
float4 positionHCS : SV_POSITION;
float2 uv : TEXCOORD0;
float3 normalWS : TEXCOORD1;
};
Varyings vert(Attributes IN)
{
Varyings OUT;
OUT.positionHCS = TransformObjectToHClip(IN.positionOS.xyz);
OUT.uv = IN.uv;
VertexNormalInputs vertexNormalInputs = GetVertexNormalInputs(IN.normalOS, IN.tangentOS);
OUT.normalWS = vertexNormalInputs.normalWS;
return OUT;
}
float4 frag(Varyings IN) : SV_Target
{
float2 rb = (normalize(IN.normalWS).xz / 2.0 + 0.5);
return float4(rb.x, rb.y, 0.0, 0.0);
}

For each grass blade, we determine its root position in world space. This position is converted to UV coordinates for sampling the interaction map. The sampled values are transformed back to the [-1, 1]
range using: (_GrassMap.rg - 0.5) * 2.0
. The resulting vector represents both the direction and intensity of the bend.
The force is strongest at the top of the grass blade. It gradually decreases along the blade's height. The grass root remains fixed (zero force) as the anchor point. This graduated force application creates a convincing bending motion where the top of the grass bends more than the middle, while the base stays firmly planted. This height-dependent force distribution mimics how real grass behaves when pushed - bending from the top while remaining anchored at the root.
The vertex deformation code in grass shader looks like below.
HLSL
#if _INTERACTABLE_ON
float4 grassMap = SAMPLE_TEXTURE2D_LOD(_GrassMap, sampler_GrassMap, (originWS.xz - _TerrainCenter.xz) / _TerrainSize, 0);
float2 pushForce = (grassMap.rg - 0.5) * 2.0 * _PlayerPushForceStrength;
float2 forceDir = normalize(pushForce);
float2 offsetXZ = IN.color.r * forceDir * _PushForceHorizontalStrength;
positionWS.xyz += float3(offsetXZ.x, IN.color.r * _PushForceVerticalStrength, offsetXZ.y);
#endif
Note that
IN.color.r
stores the height of the vertex. We store each vertex's height information in the red channel of the vertex color attribute for the grass mesh.
Customizing Force Distribution Patterns
The interaction pattern can be easily modified by changing the mesh used for force distribution. While our funnel mesh creates a circular interaction zone, you can achieve different force patterns by using different mesh shapes:
- A elongated mesh could create directional force for dash movements
- A crescent-shaped mesh might better represent sweeping weapon attacks
The key principle remains the same: the mesh normals define the force direction and intensity, while the mesh shape determines the affected area. This flexibility allows you to create a wide range of interaction patterns without changing the core rendering system.



Extending the Interaction Map
While we've focused on using the rg
channels of our RenderTexture for grass bending, the interaction map concept can be extended to store additional interaction states in the remaining channels. By utilizing the full RGBA channels of the RenderTexture, we can encode various grass states and interactions:
- The blue channel (B) could store grass cutting states, indicating where grass has been trimmed by weapons
- The alpha channel (A) could represent burning states for fire propagation effects
- Additional RenderTextures could track:
- Grass growth stages
- Snow coverage on grass
- Wetness states from rain
- Grass trampling persistence
This multi-channel approach allows us to manage multiple interaction types within the same system architecture. Each new interaction type simply needs to:
- Define how its state should be encoded in the available channels
- Implement appropriate rendering logic for the new state
- Update the grass shader to respond to the additional information
Various grass cutting effect:


Burn grass effect:

Conclusion
In this post, we've explored an efficient approach to grass interaction using normal maps and a funnel mesh. While this interaction system is crucial for creating immersive environments, it's just one piece of the larger puzzle when it comes to large-scale grass rendering in open-world games.
Several critical aspects of grass rendering systems still need to be addressed:
- GPU Instancing for efficient large-scale grass rendering
- Chunk-based grass placement and management with respect to the chunk-based world management
- LOD (Level of Detail) implementation for performance optimization
- View frustum culling and distance-based rendering
- Memory management for vast open worlds
- Dynamic grass density adjustment
These performance considerations become particularly crucial when dealing with massive open worlds where millions of grass instances need to be managed efficiently. We'll explore these topics in future posts, diving into techniques that make large-scale grass rendering both beautiful and performant.
The interaction system we've discussed today serves as a foundation, but combining it with these advanced rendering techniques will be key to creating truly impressive and efficient grass systems for modern open-world games.
Stay tuned for more technical deep dives into grass rendering optimization techniques!