top of page

2D Interactive Vegetation Shader Breakdown

Wind and interactions with vegetation in games are often created using noise layers and masks. For DARUMA, I don't need a complex wind system or interaction, and everything I need can be done with a simple shader.


First, we create the turbulence mask using gradient noise. The turbulence mask tells the shader where (in world position) the wind is blowing the most and the least. This is the effect you can see in Miyazaki films on grass fields. So in the vertex shader, we can create this mask like this:

float simulationSpeed = _SimulationSpeed * _Time.y;
float2 turbulenceSpeed = simulationSpeed * _WindDirection.xy * _TurbulenceSpeed + o.positionWS.z;
float turbulence = 1 - GradientNoise(TilingAndOffset(o.positionWS.xy, float2(1,1), turbulenceSpeed), _TurbulenceScale) * _Turbulence;

You can see the turbulence mask here in black and white:



Now, we will create two noise layers for wind detail on foliage. These two textures will be blended and will displace the vertices of the sprite.

float2 windLayer1Speed = simulationSpeed * _WindDirection.xy * _WindLayer1Speed;
float2 windLayer2Speed = simulationSpeed * _WindLayer2Direction.xy * _WindLayer2Speed;

float windLayer1 = GradientNoise(TilingAndOffset(o.positionWS.xy, float2(1,1), windLayer1Speed), _WindLayer1Scale);
float windLayer2 = GradientNoise(TilingAndOffset(o.positionWS.xy, float2(1,1), windLayer2Speed), _WindLayer2Scale) * _WindLayer2Intensity;

float layerSumm = windLayer1 + windLayer2 / (_WindLayer2Intensity + 1);


Some vegetation assets have parts that don't move in the wind (like rocks, branches, etc.). We can use another mask (as a secondary texture) to prevent the wind from affecting these parts of the sprite.

In the vertex shader, we can read the texture using SAMPLE_TEXTURE2D_LOD. Afterward, we will multiply the final wind layer with the mask.

float mask = SAMPLE_TEXTURE2D_LOD(_WindMask, sampler_WindMask, o.uv, 0).r;

For better results, we can add a remap function to refine the noise details. After that, we can combine all the masks and layers into a "final Wind layer":

float remap = Remap(layerSumm, float2(0,1), float2(0, _WindPresence));
float finalWindMask = saturate(remap * turbulence * mask);


Now we can use this texture to displace the vertices by applying the wind direction and using the combined mask as the strength::

float3 targetPos = o.positionWS + float3(finalWindMask * _WindDirection.xy, 0);

o.positionCS = TransformWorldToHClip(targetPos);
o.positionWS = targetPos;


Now we are done with the wind. However, for added realism, I want objects to be able to affect nearby vegetation when moving. To create this "influence" mask, we need to add a global property for the influence source(s), the influence strength(s), and the influence radius(es). In a script placed on the object, we can update the values in the global shader and use them in our vertex shader like this:

float4 _PlayerPosition;
float _PlayerInfluenceDistance;
float _PlayerInfluenceStrength;
float influenceDist = clamp(distance(_PlayerPosition, o.positionWS) / _PlayerInfluenceDistance,0,1);
float influenceMap  = (1 - influenceDist) * _PlayerInfluenceStrength;
float2 indfluenceDir = normalize(_PlayerPosition.xy - o.positionWS).xy;
In white, the player influence.

Now we are simply instructing the shader: if the influence mask is whiter, the vertices are pushed in the opposite direction of the player; otherwise, the vertices are driven by the wind. We can replace the last lines of the vertex shader with this:

float2 finalLayer = lerp(finalWindMask * _WindDirection.xy, -indfluenceDir, influenceMap);

float3 targetPos = o.positionWS + float3(finalLayer.xy, 0);

o.positionCS = TransformWorldToHClip(targetPos);
o.positionWS = targetPos;

While experimenting with the vegetation effect, I discovered an additional technique that yields better results. For each vegetation asset (such as bushes or tree leaves), I added a random soft bump effect on the sprite using its transform:

float f = (amplitude * Mathf.PerlinNoise((Time.time + seed) * frequency, 0f)) - (amplitude / 2);
workSpace.Set(startScale.x + f, startScale.y + f, 1f);
transform.localScale = workSpace;


I hope to find a solution to use the influence on explosions, particles, and other situations. But for now, I'm satisfied with the result.

Comments


bottom of page