UE4 Procedural Wind

Procedural Wind

A simple procedural wind material implemented in UE4 and HLSL, designed for various vegetation types like trees, bushes and foliage. It can be easily ported to Unity or GLSL as most of the work is done in the HLSL shaders.

Features:

  • configurable trunk animation such as bending or axis drag
  • configurable foliage animation for branches and leaves via edge and branch attenuation
  • material collection for controling the global wind settings
  • level of detail

Trunk animation

The animation for the tree trunk is based on a noise function2 that approximates the motion of the trunk caused by the drag of the wind on the branches:

cos(x) * cos(x * 3) * cos(x * 5) * cos(x * 7) * 1.0 + sin(x * 25) * 0.1

The high amplitude will represent the tree leaning away from it's resting point due to the wind, while the low amplitudes represents the tree going back to the equilubrium point due to it's internal elastic force.


The noise function can be adjusted to take into account the mess of the tree:

cos(x) * cos(x * 1) * cos(x * 3) * cos(x * 5) * 0.5 + sin(x * 25) * 0.02

A tree with greater mass would have lower elasticity, thus, lower amplitude.


Next a blending factor is computed based on the height of the tree and used in computing the displacement based on the wind direction and strength, also a small force that is perpendicular to the wind direction is applied.

// amplitude is dependent on tree's mass
float objPhase = dot(ObjectPosition.xyz, 1) * WindPhase;
float t = Time * WindSpeed * pi + objPhase;
float amplitude = cos(t) * cos(t * lerp(3.0, 1.0, TreeMass));
amplitude *= cos(t * lerp(5.0, 3.0, TreeMass)); 
amplitude *= cos(t * lerp(7.0, 5.0, TreeMass)) * lerp(1.0, 0.5, TreeMass);
amplitude += sin(t * 25.0) * lerp(0.1, 0.02, TreeMass);

// apply the blending factor
float height = WorldPosition.z - ObjectPosition.z;
float blendFactor = height * Blending;
blendFactor += 1.0;
blendFactor *= blendFactor;
blendFactor = blendFactor * blendFactor - blendFactor;

// apply parallel and perpendicular forces
float2 windDir = normalize(WindDirection.xy);
float2 perpWindDir = float2(-windDir.y, windDir.x);
float windStrength = (WindStrength + sin(t * 2.0) * (WindStrength / 10.0)) * amplitude * blendFactor;
float2 displacement = windStrength * windDir + windStrength * BranchForce * perpWindDir;

return float3(displacement, amplitude);

Finally, due to the uneven distribution of branches, a rotation around the Z axis will be applied, that uses the same noise function to be in synch (i.e. amplitude):

Foliage animation

The foliage animation is based on Tiago Sousa's work for Crysis1, which uses triangle waves for the animation instead of costly trigonometric functions.

struct TriangleWave
{
  float4 smoothCurve(float4 x) 
  {
      return x * x *(3.0 - 2.0 * x);
  }
  float4 triangleWave(float4 x) 
  {
      return abs(frac(x + 0.5) * 2.0 - 1.0);
  }
  float4 smoothTriangleWave(float4 x) 
  {
      return smoothCurve(triangleWave(x));
  }
};

Note that using struct helpers in UE4 custom nodes will not compile on some platforms like OpenGL or Vulkan, for that you can define the wave functions as macros.
This will produce a smooth wave:

Next the foliage animation will require that the assets to provide extra information:

  • branch weight: weight of the foliage displacement (e.g. for tree trunk it whould be zero)
  • branch phase: per branch phase
  • edge stiffnes: per vertex stiffness of the leaf's edges
  • overall stiffnes: per vertex stiffness of leaf's vertical movevent

This can be stored in the vertex color of the mesh, which can be easily auto-generated for some properties like: branch phase (i.e. green channel) or branch weight (i.e. red channel, inverted):

For other properties they can be stored in the textures to provide more flexibility like: edge stiffness (i.e. left) or overall stiffnes (i.e. right):

For the animation we then use the branch phase of the asset and create two phases, one for the edges animation and on for the upward animation, we then use these and create 4 triangle waves, 2 for each animation.
Also the triangle waves will have to be synched with the main trunk animation, I've used Graphtoy to get a close approximation.

Finally, we use the edge stiffnes for the sideways movement along the normal and the overall stiffnes for the Z-axis movement, then combine them with the triangle waves.

float objPhase = dot(ObjectPosition.xyz, 1) * WindPhase;
float branchPhase = BranchData.x + objPhase;
float vertexPhase = dot(WorldPosition.xyz, branchPhase);

float t = Time * WindSpeed;
float2 phases = float2(t + vertexPhase, t + branchPhase);
float4 waves = frac(phases.xxyy * float4(1.975, 0.793, 0.375, 0.193)) * 2.0 - 1.0;
waves *= WindSpeed * 800.0; // multiply to synch with trunk animation

TriangleWave waveHelper;
waves = waveHelper.smoothTriangleWave(waves);

float2 wavesSum = waves.xz + waves.yw;

// edge (xy) and branch bending (z)
float edgeAmplitude = WindStrength * EdgeAmplitude;
float branchAmplitude = -WindStrength * BranchAmplitude;
float branchAttenuation = BranchData.y;
float edgeAttenuation = BranchData.z;
return wavesSum.xxy * float3( (edgeAttenuation * edgeAmplitude) * Normal.xy, branchAttenuation * branchAmplitude);

Wind parameters

There global wind settings are defined in a Material Collection, but can be modified to support other wind sources like wind fields or spherical sources:

Some settings are per material and can also be per instance (e.g. if you use the scale to affect the tree mass):

Where:

  • blending - amount of bend/swing of the tree
  • branch drag - amount of Z axis rotation due to the drag of the branches
  • perpendicular force - amount sideways movement perpendicular to the wind
  • tree mass - affects the amplittude, or strength of the wind direction
  • edge amplitude - affects the sideways animation of the foliage/leaves
  • upward amplitude - affects the upward animation of the foliage/leaves

Usage

Using the wind material functions is trivial:

Preview