HDRP Shader Graph and Wind Zones

I am (slowly!) working on creating an animated cartoon series using Unity 3D to animate the characters. I am upgrading the project from the older Unity built-in render pipeline to the latest High Definition Render Pipeline (HDRP), which is forcing me to re-learn how to achieve various effects. I wrote up a recent blog mentioning the Unity demo project (FontainbleauDemo) that includes a Wind example based on HDRP volumes. Go back and read it if you like, but it made me reflect upon whether I really need HDRP volumes just for trees swaying in the wind. So here is my new favorite solution – a HDRP Shader Graph that gets its input from Wind Zones without HDRP volumes. We shall see how long before I change my mind again! šŸ˜‰

Note: I step through the shader graph structure in some detail, so this post may interest you as a shader graph example even if you don’t care about wind.

Some Concepts

Before we dive into the details, some terminology to set the stage.

Models (I am going to use a tree in particular) have one or more meshes (of triangles). Here is a model of a character as its a bit easier to see the triangles than on the tree I am using. Each point the triangles meet at the corner is a vertex. The nice thing about triangles is they are always flat, not matter where you move one of the tree vertexes.

To make the mesh look good, you apply a material to the mesh. Materials include textures (PNG or similar) as well as properties such as how light reflects off the material. Materials also reference a shader that is the code that worries about how to render triangles of the texture on the screen.

To help reduce the number of triangles needed for a model (and hence improve performance), there are clever tricks. Where a texture defines the artwork for a material, a normal map defines how light bounces off it. This allows you to create the illusion of shadows on a surface, even if it’s actually perfectly flat. Here is the neck region of the above character. You can just make out the U shape at the top which corresponds to under the chin. Then the vertical lines under neath, and the horizontal lines at the base of the neck for the collar bones.

The end result is the neck has better looking shading, beyond what the mesh alone would look like. The collar bones are highlighted a bit more, you get a bit more texture where the throat is. But the mesh is just an approximate cylinder for the neck.

Another trick is to use transparency in textures. The character above uses transparency for the open section of the clothes, revealing the skin beneath.

This is also useful when draw tree leaves. You just erase the texture to make gaps between the leaves. This means you can have a few triangles contain tens (or even hundreds) of leaves. Using “alpha cutoff”, Unity can may alpha channel data on to what is visible and what is not visible (transparent).

Here is an example texture before applying alpha cut off.

Here is an example alpha layer that can be used to make everything transparent except where the leaves are.

The end result is nice looking leaves on a tree.

Without the alpha transparency cutoff in use by the shader, you get this sort of effect. See how there are relatively big flat sections where you could see multiple leaves before?

Finally, Shader Graph allows you to create shaders for materials using a drag and drop visual editor. The idea is you don’t have to learn programming (well, not as much anyway). It also includes ability to preview your results as you go. Using shader graph you can do clever things with textures for rendering a material.

But there is one more useful capability, especially for wind effects. Shaders can move the vertices of a mesh (the points at the corners of triangles). That is what we are going to use here to achieve a wind effect.  We are going to use a material that makes vertices move around based on the wind.

Unity Wind Zones

Unity has the concept of wind zones that existed before HDRP. The idea is you can put an object with a wind zone component in a scene and set the direction, strength, gustiness, and turbulence of the wind. All the trees in your scene can then use the wind zone, allowing you to change the direction or strength of the wind on one component and all the trees adjust.

HDRP appears to have changed how wind works, putting more emphasis on the creator of trees regarding how they should react to wind. The following is one solution to this problem. I like the simplicity of a wind zone, so I added my own new component you may see above – “Alan Wind Zone to Shader”.  Here is the complete code for this component:

using UnityEngine;
using UnityEngine.Rendering;
using UnityEngine.Rendering.HighDefinition;

[ExecuteAlways]
public class AlanWindZoneToShader : MonoBehaviour
{
    private WindZone windZone;

    void Update()
    {
        ApplySettings();
    }

    void OnValidate()
    {
        ApplySettings();
    }

    void ApplySettings()
    {
        if (windZone == null)
        {
            windZone = gameObject.GetComponent<WindZone>();
        }
        if (windZone != null)
        {
            Shader.SetGlobalVector("_WINDZONE_Direction",
                                   windZone.transform.forward);

            // wind speed/strength (e.g. 1)
            Shader.SetGlobalFloat("_WINDZONE_Main",
                                  windZone.windMain);

            // How many pulses of wind per second
            Shader.SetGlobalFloat("_WINDZONE_Pulse_Frequency",
                                  windZone.windPulseFrequency);

            // Pulse strength, e.g. 0.5
            Shader.SetGlobalFloat("_WINDZONE_Pulse_Magnitude",
                                  windZone.windPulseMagnitude);

            // Degree of variation (e.g. 1 is lots of variation)
            Shader.SetGlobalFloat("_WINDZONE_Turbulence",
                                  windZone.windTurbulence);
        }
    }
}

Some key points of particular note for people new to writing C# scripts in Unity.

  • The “[ExecuteAlways]” directive tells Unity that this component should be used in both Editor mode and Play mode, which makes previewing results easier. (This does not always happen for me – I think it might give up if the CPU load is too high.)
  • My script assumes it is added to the same game object as the Wind Zone component (so it can find it easily using GetComponent<WindZone>()).
  • Update() (called in Play mode) and OnValidate() (called in Edit mode) both call the ApplySettings() function.
  • The ApplySettings() function copies the wind zone settings into Shader global default values using Shader.SetGlobalVector() and Shader.SetGlobalFloat(). The shader graph is going to use these values next.

As you can see, the above code is pretty simple.

Shader Graph Input Parameters

Next, the tree I am using has separate meshes for the main trunk and branches (with leaves). What we want to do is make the vertices of the trunk sway from left to right (zero movement at the base of the tree where it connects with the ground, more sideways (X and Z, not Y) movement higher up). For the leaves, we want to add some turbulence so it looks like the leaves are fluttering in the wind, but not on the tree trunk.

My solution is the shader graph picks up default shader global default values, but materials referencing the shade can add their own weights into the mix. E.g. there is a turbulence weight. If set to zero on a material, there is no turbulence. I will use turbulence zero for the trunk of the tree, with a turbulence of 1 for the leaves so they “flutter” in the wind.

Here is the full list of input parameters

The input parameters starting with “WINDZONE” have the “Exposed” property set to false, so they will always pick up the default shader global values. Note the “Reference” name matches the C# code above.

The other values are used per material. There is the main (albedo) texture and normal map, smoothness, etc.  There are also “Turbulence Strength” and “Pulse Strength” settings (typically values in the range of 0 to 1) which are multiplied with the wind zone settings so a material can decide how much weight these effects have. For example, the tree trunk material I set the turbulence to 0 (so no turbulence gets added to the trunk) and the leaves I set to 1 (turbulence makes the leaves shader flutter the leaves more).

Now we know the inputs, let’s go through the sections of the shader graph in detail.

Textures

First, we need to load a texture (a 2D image file, like a PNG) and normal map (also a PNG file) using the Sample Texture 2D node type, and plug them into the rendering part of the graph. Fairly simple and traditional stuff. Nothing from the wind zone component is used here. Note that the main texture RGBA output has the alpha channel in it, but the fragment wants it provided as a separate input, hence the A output is supplied separately.

Some shaders do clever things with the alpha channel of the normal map for feeding into smoothness or emission, but that requires a carefully designed PNG file.

The rest of the shader graph is more interesting. I should note I took a lot from the shader graph presented by Unity in Simulating Wind in URP (Shader Graph Tutorial)

It turned out this shader had closer effects to what I wanted for less effort related to swaying trees and turbulence.

Sway by Height

There are three things I use to contribute to how much a vertex should sway. The height of the vertex (the top of the tree moves more than the bottom), the time, and some settings to control the strength of the sway. These are all eventually multiplied together for the final strength. Let’s start with height.

The Position node works out the position of the vertex. Because I specified “Object” space, it is relative to the base of the object (meaning if a tree is on a hill or in a valley it does not matter – the world space is not important). The Split node can be used to pull out the second value in the vector (shown as the Green channel, but in our case the second value in the vector is the Y coordinate of the vertex X,Y,Z). The base is assumed to be at height 0, but just to be safe we add a Maximum node so values less than zero return 0 (we never get a negative number).

The end result is the root of the tree will output a zero (no movement), then the higher up the tree you go, the more movement there will  be (as Y is getting bigger). The maximum value returned is the height of the tree.

Sway by Time

Next, the shader graph uses a cosine function to sway backwards and forwards with a graceful swinging motion as a function of time. That is, as time progresses, the value will smoothly swing from -1 to +1 and back again.

See the following graph to illustrate. Time is used as the angle to the cosine function, the x-axis on the graph. It keeps getting bigger and bigger, but cosine will always output a value between -1 and +1. (Sine could have been used as well – makes no difference in this case. We just want a smooth swaying motion.)

Here is the shader graph

I decided that a pulse frequency of 1 should cause the tree to swing backwards and forwards once per second. Since the cosine function takes radians as input, I multiple the pulse frequency by 6.28 to cause a sway and return each second when the pulse frequency setting is 1 (there are 2Pi, or 6.28, radians in a circle). A pulse frequency of 2 will make the angle go around the circle twice per second.

So the output is a number between -1 and +1, following the cosine wave shape at a speed based on the pulse frequency from the wind zone.

Sway Strength

We have a few different inputs that we want to use to control the sway strength. The pulse magnitude from the wind zone must contribute. But to give materials a bit of control, each material can also have a pulse strength they can contribute (e.g. so different trees in a scene can sway at different amounts). The overall strength of the wind should also contribute, so we just multiply all three together.

The shader graph multiply node can only take two inputs, so to multiply three values you normally have 2 multiply nodes. You multiply the first two values, then feed the result into another node that takes the result and multiplies the third value in – like ((A*B)*C).

For fun, I created a custom function that multiplies 3 numbers rather than using 2 multiple nodes chained together. The definition of the custom function is as follows. It has 3 inputs I creatively called A, B, and C. The output is also a float. The script itself is just a multiply function “Out = A*B*C;”. This is probably overkill here, but just illustrating you can do more complicated formulas when needed.

Combining the Sways

The three sway values (sway by height, sway by time, and sway by strength) are then multiplied together to result in a single float value of how much a vertex should sway by. I use multiply and not add because if sway by height is zero, the result should be zero no matter what.

I use another copy of my multiple three numbers custom function.

Wind Direction

We know how far a vertex should move when swaying, but it should move in the direction of the wind.  So we want the shader graph to take the direction of the wind (from the wind zone) and multiply it by the strength result from above.

The normalize node is probably not needed, but it makes sure the direction vector is always of unit length 1. We don’t want its length to affect the amount of swaying, just its direction. Multiplying by our single strength number generates a vector that is how much we want a vertex to move by (a delta amount to add to its original position).


Turbulence

Next, let’s look at turbulence for making particularly the leaves of trees flutter in the wind.

The bottom left corner starts by multiplying the wind zone turbulence and the material turbulence. I normally set a leaf material texture turbulence strength to 1 and the tree trunk turbulence to 0. I don’t want the trunk or branches fluttering in the wind, just the leaves. The final turbulence strength gets used in two places, as we will see later.

I want the fluttering to be a bit out of sync with the tree sway, so I take the pulse frequency and multiply it by 5 (feel free to try your own value!). This is multiplied with Time to change the speed of change. I also multiply the turbulence strength in for good measure (high turbulence increase the speed of fluttering).

This value is then fed into a Tiling And Offset node which feeds into the offset of a Gradient Noise node. (Since we feed a single float in but the offset wants a vector of 2 values, the the Shader Graph uses the single float for both values in the Vector2 – so the texture slides on a diagonal.) I set the gradient scale to 10 of the noise node so you can see the changes. Again, you can fiddle some of these numbers if you like – they will all impact the rate of turbulence. If you look inside shader graph, you will see the gradient texture slide diagonally over time across the view area. This contributes to the vertices moving over time (fluttering in the wind).

The reason for using the Gradient Noise node is so each vertex will have a different additional offset value, but with vertices next to each other having a smaller difference than ones far apart. So a leaf mesh gets distorted a bit, sort of like crunching up a sheet of paper a bit. Over time, they will all move around a bit. The gradient scale for example will change by how much values close to each other can change.

Finally, a multiply node uses the turbulence strength to also control the magnitude of how far vertices move due to the turbulence.

Add Final Movement to Vertex

Phew! We are into the home run! Last, we take the vector of how much to move the vertex due to swaying, add in the turbulence, then add the delta we computed to the vertex original position in object space. That then feeds into the vertex handle for vertices. Done!

Everything

Here is the final total graph. (Look at the above for readable explanations!)

As you can see, you can do pretty clever things with shader graphs. Obviously, the more complex the shader graph, the more CPU that is required. But it creates some pretty clever effects, such as wind in the trees.

Got a better solution? I would love to hear it! I am writing down things as I learn them in case it helps others on their journey. If there is a better way I would love to know!


Leave a Reply

Fill in your details below or click an icon to log in:

WordPress.com Logo

You are commenting using your WordPress.com account. Log Out /  Change )

Twitter picture

You are commenting using your Twitter account. Log Out /  Change )

Facebook photo

You are commenting using your Facebook account. Log Out /  Change )

Connecting to %s