Hello, folks!

Since we are working hard and doing things that could be interesting for some of you (or maybe all :D), we thought it would be very nice to share stuff we’ve been making in a little bit more technical way.
You can use these tips and tutorials in your projects and tell us your experience with them, and even suggest us ways to help us improve.

We will start with what we call the “foliage waver”. It’s a rather simple shader that will twist and move your plants, leaves and anything you want to be affected by a force like wind, or the movement of the players.

Foliage Waver in action
We do get knowledge and influence from other people too, and in this case it all started from an awesome post in Unity forums by GambinoInd. Using his example shaders as a base, we modified it to our needs in Degrees of Separation. We simplified it for a use in 2D, added new variables, and introduced rotation of the plant with the waving.

Note: We made a nice Unity package with all the code and resources we are gonna create here. You’ll find it at the bottom of this post.

Making the foliage react to wind

Let’s start. First, we need a sprite that represents a plant. We will use a fern bush, and will place it in the center of the screen:

Make a standard surface shader file and copy this into it:

Shader "Sprites/Waver"
{
    Properties
    {
        _MainTex("Texture", 2D) = "black" { }
        _Color("Tint", Color) = (1, 1, 1, 1)

		_Cutoff("Alpha Cutoff", Range(0,1)) = 0.5
    }
 
    SubShader
    {
		Tags
		{
			"Queue" = "AlphaTest"
			"IgnoreProjector" = "True"
			"PreviewType" = "Plane"
			"CanUseSpriteAtlas" = "True"
		}

		Cull Off
		Lighting Off
		Blend One OneMinusSrcAlpha
		CGPROGRAM

		#pragma surface surf Lambert vertex:vert nofog alphatest:_Cutoff
		#pragma target 3.0

		#include "ComposerEffects.cginc"
 
        sampler2D _MainTex;
 
        fixed4 _Color;
 
        struct Input
        {
            float2 uv_MainTex;
            fixed4 color;
            float3 worldPos;
        };          
 
        /****************************************************************************/
        /*************************************WAVING*********************************/
        /****************************************************************************/
 
 
        float4 _FoliageShake;
        float3 _FoliageZoneApplicationAndTime;
        float3 _FoliageRotation;
        float3 _FoliageTransformRotation;
 
        float4 _FastSin (float4 val) 
        {
            val = val * 6.408849 - 3.1415927;
            float4 r5 = val * val;
            float4 r1 = r5 * val;
            float4 r2 = r1 * r5;
            float4 r3 = r2 * r5;
 
            float4 sin7 = { 1, -0.16161616, 0.0083333, -0.00019841 };
         
            return val + r1 * sin7.y + r2 * sin7.z + r3 * sin7.w;
        }
 
 
        float4x4 _RotationMatrix( float angle_ )
        {
            float sinX = sin ( angle_ );
            float cosX = cos ( angle_ );
            float sinY = sin ( angle_ );
            float4x4 rotation_matrix = float4x4( cosX, -sinX, 0, 0,
                                                 sinY, cosX,  0, 0,
                                                 0,    0,     1, 0,
                                                 0,    0,     0, 1 );
            return rotation_matrix;
        }
 
 
        float2 _Rotate( float2 point_, float angle_ )
        {
            return mul( float4(point_.x, point_.y, 0, 0), _RotationMatrix( angle_ )  );
        }
 
        float2 _RotateAbout( float2 point_, float2 about_, float angle_ )
        {
            point_ -= about_;
            point_ = _Rotate( point_, angle_ );
            return  point_ + about_;
        }   
 
        float2 wave( float2 vertex_, float4 texcoord_, float2 world_pos_ )
        {
            //Constants
            const float4 wave_x_size = float4( 0.048, 0.06, 0.24, 0.096 );
            const float4 wave_speed = float4 ( 1.2, 2, 1.6, 4.8 ); 
            const float4 wave_x_move = float4( 0.024, 0.04, -0.12, 0.096 );
 
            //Getting properties from script
            int shake_freq = _FoliageShake.x;
            float shake_amount = _FoliageShake.y;
            float shake_bending = _FoliageShake.z;
            float shake_speed = _FoliageShake.w;
 
            float2 zone_application = float2( _FoliageZoneApplicationAndTime.x, _FoliageZoneApplicationAndTime.y );
            float time = _FoliageZoneApplicationAndTime.z;
     
            float2 rotation_pivot = float2( _FoliageRotation.x, _FoliageRotation.y );
            float rotation_amount = _FoliageRotation.z;
     
            float3 body_rotation = _FoliageTransformRotation;           
 
            //First let's rotate to put it in the origin;
            vertex_ = _Rotate(vertex_, float2( -body_rotation.z, -body_rotation.y ) );
    
            //Wave calculations
            float4 waves = vertex_.x * wave_x_size;     
            waves += time * shake_speed * wave_speed;
            waves = frac( waves );  
                 
            float coord = ( texcoord_.y - zone_application.x)  / ( zone_application.y - zone_application.x );
            coord = clamp( coord, 0, 1 );
 
            float wave_amount = coord * shake_bending;
            float4 s = _FastSin ( waves ); 
            s *= wave_amount; 
            s *= normalize( wave_speed );   
            s *= shake_freq  ? s : pow( s, 3 );
     
            float3 wave_move = float3 ( 0, 0, 0 );
            wave_move.x = dot( s, wave_x_move );
            vertex_.x += mul( (float3x3)_World2Object, wave_move ).x *  shake_amount;
            vertex_ = _RotateAbout( vertex_, float2( rotation_pivot.x - world_pos_.x, rotation_pivot.y - world_pos_.y   ), rotation_amount * texcoord_.y );         
 
            //Rotate back
            vertex_ = _Rotate( vertex_, float2( body_rotation.z, body_rotation.y ) );
 
            return vertex_;
        }
 
        /****************************************************************************/
        /****************************************************************************/
        /****************************************************************************/
 
        void vert( inout appdata_full v, out Input o )
        {
            #if defined( PIXELSNAP_ON )
                v.vertex = UnityPixelSnap( v_.vertex );
            #endif
 
            UNITY_INITIALIZE_OUTPUT( Input, o );
            o.color = v.color * _Color;
            o.worldPos = mul( _Object2World, v.vertex ); 
 
            //Plant waving          
            v.vertex.xy = wave( v.vertex.xy, v.texcoord, o.worldPos );
                 
        }
 
        void surf( Input i, inout SurfaceOutput o )
        {
            fixed4 color = tex2D( _MainTex, i.uv_MainTex ) * i.color;
 
            o.Albedo = color.rgb * color.a;
            o.Alpha =  color.a;
        }
        ENDCG
    }
 
    FallBack "Diffuse"
}

We have to make a script that gets the information we need from the user, and send it to the shader we just wrote. Make a script like this one, and add it as a component on the object you want to wave:

using UnityEngine;

[RequireComponent( typeof( SpriteRenderer ) )]
[RequireComponent( typeof( BoxCollider2D ) )]
public class FoliageWaver : MonoBehaviour
{
	#region InspectorFields
	[Header( "Wave Properties" )]
	[MinMaxSlider( 0, 1 )]
	public Vector2 waveDissipation = new Vector2( 0.54f, 0.57f );

	[MinMaxSlider( 0, 1 )]
	public Vector2 waveFriction = new Vector2( 0.35f, 0.4f );

	[MinMaxSlider( 0f, 2f )]
	public Vector2 shakeBending = new Vector2( 1.1f, 1.15f );

	[MinMaxSlider( 0f, 4f )]
	public Vector2 shakeSpeed = new Vector2( 3.3f, 3.8f );

	[MinMaxSlider( 0f, 1f )]
	public Vector2 coordinateApplication = new Vector2( 0.02f, 1f );

	[Header( "Rotation Properties" )]
	public Vector2 rotationPivot = new Vector2( 0f, 0f );

	[MinMaxSlider( 0f, 1f )]
	public Vector2 rotationStrength = new Vector2( 0.038f, 0.21f );

	[Range( 0f, 1f )]
	public float rotationLimits = 0.04f;

	[Range( 1f, 6f )]
	public float rotationWaveSpeed = 0.2f;

	[Range( 0f, 5f )]
	public float rotationBounciness = 0.5f;

	[Header( "Others" )]
	public bool underWater = false;
	#endregion InspectorFields

	#region PrivateFields
	private float _wave_scew = 0f;
	private float _wave_force = 0f;

	private Renderer _rend;

	private float _wave_dissipation;
	private float _wave_friction;
	private float _shake_bending;
	private float _shake_speed;
	private float _rotation_strength;

	private float _wave_dissipation_rn = 0.5f;
	private float _wave_friction_rn = 0.5f;
	private float _shake_bending_rn = 0.5f;
	private float _shake_speed_rn = 0.5f;
	private float _rotation_strength_rn = 0.5f;

	private float _time;
	private float _rot_amount;
	private Vector2 _real_pivot;

	//shader property ids
	private int _foliage_shake_prop;

	private int _foliage_zone_application_and_time;
	private int _foliage_rotation_prop;
	private int _foliage_transform_rotation_prop;
	#endregion PrivateFields

	#region Unity
	private void Awake( )
	{
		_real_pivot = rotationPivot;

		//Using ID's to send properties to the shader
		_foliage_shake_prop = Shader.PropertyToID( "_FoliageShake" );
		_foliage_zone_application_and_time = Shader.PropertyToID( "_FoliageZoneApplicationAndTime" );
		_foliage_rotation_prop = Shader.PropertyToID( "_FoliageRotation" );
		_foliage_transform_rotation_prop = Shader.PropertyToID( "_FoliageTransformRotation" );

		_rend = GetComponent<Renderer>( );

		//Makes a calculation of the numbers given min and max of the range in inspector
		_CalculateRandomNumbers( );
		_RecalculateValues( );
	}

	private void FixedUpdate( )
	{
		//Taking time
		_time += Time.fixedDeltaTime;

		//get velocity of the wind that will move the plant and add it to the total force
		if( WeatherData.Instance && WeatherData.Instance.isActiveAndEnabled && WeatherData.Instance.windProperties != null && WeatherData.Instance.activateWind )
		{
			_wave_force += WeatherData.Instance.windProperties.randomVelocity.x * 0.1f;
		}

		//The skew accumulates the force applied over time. 
		_wave_scew += _wave_force;

		//Friction and dissipation makes the plant go back to initial position little by little.
		//The more friction, the more difficult to make the plant move with the force
		float friction_application = ( 1 - _wave_friction );
		_wave_scew *= friction_application;

		//The more dissipation, the quickest the plant goes back to its initial position
		float dissipation_application = ( 1 - ( 0.1f * _wave_dissipation ) );
		_wave_force *= dissipation_application;

		//Rotation of the plant arount the pivot
		int direction = _wave_scew > 0 ? 1 : -1;
		float rot_amount = _CalculateRotationAmount( friction_application );
		Vector3 transform_rotation = Mathf.Deg2Rad * transform.rotation.eulerAngles;

		//Calculations for material before applying to shader
		_real_pivot = new Vector2( rotationPivot.x * direction, rotationPivot.y );
		Vector2 rotation_pivot = ( Vector2 )transform.position + ( _real_pivot );
		float time_for_shader = _time / 20f;
		Vector4 shake = new Vector4( underWater ? 1 : 0, _wave_scew, _shake_bending, _shake_speed );
		Vector3 zone_application_and_time = new Vector3( coordinateApplication.x, coordinateApplication.y, time_for_shader );
		Vector3 rotation = new Vector3( rotation_pivot.x, rotation_pivot.y, rot_amount );

		foreach( Material material in _rend.materials )
		{
			material.SetVector( _foliage_shake_prop, shake );
			material.SetVector( _foliage_zone_application_and_time, zone_application_and_time );
			material.SetVector( _foliage_rotation_prop, rotation );
			material.SetVector( _foliage_transform_rotation_prop, transform_rotation );
		}

		//Recalculates in editor to be able to adjust the waver in real time
#if UNITY_EDITOR
		_RecalculateValues( );
#endif
	}
	#endregion Unity

	#region LocalMethods
	private float _PickRandomBetween( Vector2 min_max_, float random_seed_ )
	{
		//Typical percentage calculation
		return min_max_.x + random_seed_ * ( min_max_.y - min_max_.x );
	}

	private void _RecalculateValues( )
	{
		//RePicks the value with the same seed - same minmax will return same value until the game restarts
		_wave_friction = _PickRandomBetween( waveFriction, _wave_friction_rn );
		_wave_dissipation = _PickRandomBetween( waveDissipation, _wave_dissipation_rn );
		_shake_bending = _PickRandomBetween( shakeBending, _shake_bending_rn );
		_shake_speed = _PickRandomBetween( shakeSpeed, _shake_speed_rn );
		_rotation_strength = _PickRandomBetween( rotationStrength, _rotation_strength_rn );
	}

	private void _CalculateRandomNumbers( )
	{
		//Stores random values to take a number between min and max for the properties
		_wave_dissipation_rn = Random.value;
		_wave_friction_rn = Random.value;
		_shake_bending_rn = Random.value;
		_shake_speed_rn = Random.value;
		_rotation_strength_rn = Random.value;
	}

	private float _CalculateRotationAmount( float friction_ )
	{
		//Calculates how much the plant should rotate depending on the force, bounciness and speed.
		_rot_amount += _wave_force * _rotation_strength * 0.05f;
		_rot_amount += ( 0.005f * rotationBounciness * Mathf.Sin( _wave_force * rotationWaveSpeed ) + _wave_force * 0.005f );

		//Makes friction affect
		_rot_amount *= friction_;

		//Don't rotate further than limits
		_rot_amount = Mathf.Clamp( _rot_amount, -rotationLimits, rotationLimits );

		return _rot_amount;
	}
	#endregion LocalMethods
}

Multiple fields of the script have a MinMaxSlider attribute, a property that allows setting 2 floats in the shape of a Vector2. In our case we use it to pick a random amount between these 2 numbers, as you can see in the code inside FoliageWaver.cs. This way, if we have a lot of plants together, they will move a bit differently. You can just change this and ask for a constant float to the user, or ask for 2 floats, the min and the max. Nevertheless, the code for the attribute is:

using UnityEditor;
using UnityEngine;

public class MinMaxSliderAttribute : PropertyAttribute
{
	public readonly float max;
	public readonly float min;

	public MinMaxSliderAttribute( float min_, float max_ )
	{
		min = min_;
		max = max_;
	}
}

[CustomPropertyDrawer( typeof( MinMaxSliderAttribute ) )]
internal class MinMaxSliderPropertyDrawer : PropertyDrawer
{
	private const int _control_height = 16;

	public override float GetPropertyHeight( SerializedProperty property_, GUIContent label_ )
	{
		return base.GetPropertyHeight( property_, label_ ) + _control_height * 2f;
	}

	public override void OnGUI( Rect position_, SerializedProperty property_, GUIContent label_ )
	{
		label_ = EditorGUI.BeginProperty( position_, label_, property_ );

		if( property_.propertyType == SerializedPropertyType.Vector2 )
		{
			Vector2 range = property_.vector2Value;
			MinMaxSliderAttribute attr = attribute as MinMaxSliderAttribute;

			range.x = Mathf.Max( range.x, attr.min );
			range.y = Mathf.Min( range.y, attr.max );

			range = EditorGUI.Vector2Field( position_, label_, range );

			Rect position = EditorGUI.IndentedRect( position_ );
			position.y += _control_height * 1.5f;
			position.height = _control_height + 5;
			EditorGUI.MinMaxSlider( position, ref range.x, ref range.y, attr.min, attr.max );
						
			property_.vector2Value = range;			
		}
		else
		{
			EditorGUI.LabelField( position_, label_, "Use only with Vector2" );
		}

		EditorGUI.EndProperty( );
	}
}

The sprite renderer attached to the object we want to wave needs to use a Material with the shader we created before. Just make a new material and select our shader from the dropdown in the material’s inspector. Then go to the sprite inspector and drag and drop the material into the renderer component.

pick the shader in your new material

Once the Material with the shader is applied, and the FoliageWaver component added, we should have this in our inspector:

The waver fields

This script is going to expose some variables that will tweak the way our waving behaves:

  • Wave Dissipation: This controls how long the plant will keep waving after the force is applied.
  • Wave Friction: The bigger it is, the most difficult for the plant to wave.
  • Shake Bending: This controls how much the plant waves. A big number will result in funky moves.
  • Shake Speed: How fast the wave travels.
  • Coordinate Application: This represents where in the plant the wave will be applied. if the min is 0 and the max is 1, it will be applied in the entire plant, but always incrementally from the bottom to the top. If the min is 0.5 and the max is 1, the wave will be applied only from the middle of the plant to the top. Take in account that we are moving vertices, and these are connected, so it is inevitable that the entire plant is moving at some point. this happens also in real life.
  • Rotation Pivot: The pivot around which the plant rotates. You can draw gizmos to see the pivot in the editor. For better results, set the pivot X to 0, and Y a bit below the center.
  • Rotation Strength: How strong the rotation is, always depending on the force applied.
  • Rotation Limits: How far the rotation goes. This number should be low.
  • Rotation Wave Speed:  How fast the rotation wave or bounciness is produced.
  • Rotation Bounciness: If the rotation should be constant or bounce a bit.
  • UnderWater: If you want to add waving in plants under water, the wave is different, smoother. Toggle this and it will behave more correctly underwater.

The Rotation Wave Speed and Bounciness are two properties that can be considered a bit experimental. Put them to 0 or very low if your rotation is a bit funky.

A nice setting for the plant would be like this:

Now, if we did everything correctly and play the scene, our bush should wave.
a plant waving with a constant force.

Awesome!

But now it’s just waving constantly because we’re applying a force of 0.2f, hardcoded into the script.

//Update method code ...
wave_force += 0.2f;
//Update method code...

The wind generators

It would be much better to have some options to control this constant force, and, furthermore, add external forces that will play with the wave in very interesting ways:

  • WindMachine: Object that throws wind waves with certain frequency. These waves will make foliage move.
  • Player movement: When player goes through foliage, the speed will trigger waving.
  • Weather: We have a global, constant wind that affects the entire scene. This makes foliage wave all the time, but changing strength and direction with a specific frequency.

For simplicity here, we’ll skip the first two and focus on the global wind, part of the weather.

We need a GameManager object in our scene, or some other object that holds effects or global values. Make a script called WeatherData and add it as a component there:

using System;
using UnityEngine;

public class WindProperties
{
	#region Actions
	public event Action OnPropertiesChanged;
	#endregion Actions

	#region PrivateMethods
	private float _time_passed;

	private Vector2 _frequency;
	private Vector2 _min_velocity;
	private Vector2 _max_velocity;

	private float _random_frequency;
	private Vector2 _random_velocity;

	private float _ran_for_frequency;
	private Vector2 _ran_for_velocity;
	#endregion PrivateMethods

	#region Accessors
	public Vector2 randomVelocity
	{
		get
		{
			return _random_velocity;
		}
	}
	#endregion Accessors

	#region Constructor
	public WindProperties( Vector2 frequency_, Vector2 min_velocity_, Vector2 max_velocity_ )
	{
		ResetProperties( frequency_, min_velocity_, max_velocity_ );
	}

	#endregion Constructor

	#region PublicMethods
	public void ResetProperties( Vector2 frequency_, Vector2 min_velocity_, Vector2 max_velocity_ )
	{
		//If something changed, assign it again and make recalculations
		bool something_changed = frequency_ != _frequency || min_velocity_ != _min_velocity || max_velocity_ != _max_velocity;

		if( something_changed )
		{
			_frequency = frequency_;
			_min_velocity = min_velocity_;
			_max_velocity = max_velocity_;

			_CalculateRandomNumbers( );
			_RecalculatePropertyValues( );
		}
	}

	public void Update( )
	{
		_time_passed += Time.deltaTime;

		//If more time than the frequency set passed, we recalculate numbers again to make wind organic and change with time (frequency)
		if( ( _frequency.x > 0f || _frequency.y > 0f ) && _time_passed >= _random_frequency )
		{
			_CalculateRandomNumbers( );
			_RecalculatePropertyValues( );

			_time_passed = 0f;

			if( OnPropertiesChanged != null )
			{
				OnPropertiesChanged( );
			}
		}
	}

	#endregion PublicMethods

	#region PrivateMethods
	private void _CalculateRandomNumbers( )
	{
		_ran_for_frequency = UnityEngine.Random.value;
		_ran_for_velocity = new Vector2( UnityEngine.Random.value, UnityEngine.Random.value );
	}

	private float _PickRandomBetween( Vector2 min_max_, float precreated_random_ )
	{
		return min_max_.x + precreated_random_ * ( min_max_.y - min_max_.x );
	}

	private Vector2 _PickRandomBetweenVectors( Vector2 min_, Vector2 max_, Vector2 precreated_randoms_ )
	{
		float x = min_.x + precreated_randoms_.x * ( max_.x - min_.x );
		float y = min_.y + precreated_randoms_.y * ( max_.y - min_.y );

		return new Vector2( x, y );
	}

	private void _RecalculatePropertyValues( )
	{
		_random_velocity = _PickRandomBetweenVectors( _min_velocity, _max_velocity, _ran_for_velocity );
		_random_frequency = _PickRandomBetween( _frequency, _ran_for_frequency );
	}

	#endregion PrivateMethods
}

public class WeatherData : Singleton<WeatherData>
{
	#region Actions
	public event Action OnChange;

	#endregion Actions

	#region InspectorField
	[Header( "Wind" )]
	public bool activateWind;

	[MinMaxSlider( 0f, 10f )]
	public Vector2 minMaxFrequency;

	public Vector2 minVelocity;
	public Vector2 maxVelocity;
	#endregion InspectorField

	#region Accessors
	public WindProperties windProperties
	{
		get
		{
			return _wind;
		}
	}
	#endregion Accessors

	#region privateFields
	private WindProperties _wind;

	//previous
	private Vector2 _previous_min_velocity;
	private Vector2 _previous_max_velocity;
	private Vector2 _previous_min_max_frequency;
	private bool _previous_activate_wind;
	#endregion privateFields

	#region Unity
#if UNITY_EDITOR
	//If we enable or disable the component, we have to update
	private void OnEnable( )
	{
		if( OnChange != null )
		{
			OnChange( );
		}
	}

	private void OnDisable( )
	{
		if( OnChange != null )
		{
			OnChange( );
		}
	}
#endif

	private void Awake( )
	{
		if( activateWind )
		{
			_wind = new WindProperties( minMaxFrequency, minVelocity, maxVelocity );
		}
	}

	private void Update( )
	{
		//Only in Unity Editor, if something changes we have to get those values and pick a random between them again
#if UNITY_EDITOR
		bool something_changed = _previous_activate_wind != activateWind;

		if( activateWind )
		{
			something_changed |= minVelocity != _previous_min_velocity || maxVelocity != _previous_max_velocity || minMaxFrequency != _previous_min_max_frequency;

			if( something_changed )
			{
				_wind = new WindProperties( minMaxFrequency, minVelocity, maxVelocity );
			}

			_wind.Update( );
		}
#endif
	}

	private void LateUpdate( )
	{
		//Set new values as previous
		_previous_min_velocity = minVelocity;
		_previous_max_velocity = maxVelocity;
		_previous_min_max_frequency = minMaxFrequency;
		_previous_activate_wind = activateWind;
	}
	#endregion Unity
}

The Weather Data is a Singleton, a class that will handle things in our game that have one and only one instance. Singletons make our life easier with convenient access to this objects from anywhere, as if they were static classes. Another thing to note is that our wind has a Vector velocity and not just speed, so it can be used in 2 dimensions. In the case of our shader, only the x coordinate of the velocity will be applied.

Add our WeatherData on the GameManager object we created on the scene:

MinMaxFrequency will make the velocity change randomly every ‘n’ seconds, ‘n’ being a random number between min and max frequency.

Now, instead of adding a constant value to the force applied to the plant, we will add the velocity of our global wind. In FoliageWaver.cs:

	private void FixedUpdate( )
	{
		//Time
		_time += Time.fixedDeltaTime;
		if( WeatherData.Instance && WeatherData.Instance.isActiveAndEnabled && WeatherData.Instance.activateWind )
		{

			wave_force += WeatherData.Instance.windProperties.randomVelocity.x * 0.1f;
		}
		_wave_scew += wave_force;
//... more Update method code

We can tweak our wind component and it will make our bush wave differently!

Plant waving with global Wind

Now you can add all this into your projects and enjoy the breeze!

Foliage Waver complete
You can find a package with all the code, an sprite and an example scene here:
FoliageWaver unity package