Unity ShaderGraph Procedural Skybox Tutorial Pt.2 (Day/Night Cycle)


Rotate the sun,
Animate the stars and the clouds,

Create a brightness AnimationCurve,
Access properties of the skybox ShaderGraph from a C# script,
Create HDR gradients for the sky colors

0. Introduction

In this part two of the Unity ShaderGraph Procedural Skybox tutorial, we are going to create a basic day/night cycle C# script. To see what the result of this tutorial looks like check out this YouTube video:

The purpose of this tutorial is to show you how easy it is to create a basic day/night cycle for a game and also how you can control the properties/variables that we created in the skybox ShaderGraph from part one, from a C# script.

If you haven’t done part one then I suggest doing that first:
Unity ShaderGraph Procedural Skybox Tutorial Pt1

If you don’t need a skybox shader but want to use your own then simply use the names of your own skybox shader’s properties instead of the ones from this tutorial!

This tutorial was written using Unity 2019.2.4f1 using the LWRP template and later upgraded to work with Unity 2019.4.18f1 LTS and the URP.

1.0 A Day/Night Cycle

Now that we have a lot of properties in our ShaderGraph to play with, it will be nice to see how we can access those properties from within a C# script, so we can use the procedural skybox shader made in part one for a simple day/night cycle script for a game:

While we are at it we will also change the sun’s brightness, the light color, sky colors, moving stars, the scene’s fog color and maybe some more things throughout the day, all based on the rotation of the sun or the (decimal) time of the day.
In the final steps of this tutorial we’ll also setup a simple ‘time of the day’ events system using handy UnityEvents that you can use for instance to enable/disable other lights like streetlights or torches in your game on specific times of the day!

All very useful indeed! So let’s get started..

1.1 Create a new C# Script

Create a new C# Script in the Project View, name it something like DayNightCycle.cs and drag it onto the Sun/Directional Light. (Or any other GameObject in the Hierarchy View):

Open your script editor by double-clicking on the script in the Project View or by double-clicking on the greyed out script name in the inspector.

For the following code examples:
New code will be shown in bold.
Ommitted code will be shown by three dots

Removed code will be shown with a strikethrough.
The complete script can be seen at the end of the tutorial.

2.0 Rotate the sun

At the top of the script create a public variable of type Transform to hold a reference to our sun:

using System.Collections;
using System.Collections.Generic;
using UnityEngine;

public class DayNightCycle : MonoBehaviour
{    
    public Transform sun;

    void Start()
    {

    }

    void Update()
    {

    }
}

To have our day always start with a sunrise in the east, we only have to rotate the sun -90 degrees on the y-axis in Start().
Then to simply rotate the sun on the x-axis by one degree per second use the Transform.Rotate method inside of the Update() method:

using System.Collections;
using System.Collections.Generic;
using UnityEngine;

public class DayNightCycle : MonoBehaviour
{    
    public Transform sun;

    void Start()
    {
        sun.rotation = Quaternion.Euler(0,-90,0);
    }

    void Update()
    {
        sun.Rotate(Vector3.right * Time.deltaTime);
    }
}

2.1 Rotate method

Because we are going to be adding lot’s of more stuff to the script it is probably smart to keep the Update method clean and to organise our code into different methods that only do one thing each.

Create a new method named RotateSun underneath Update and move the rotation code from Update into the new method:

...

    void Update()
    {
        // Rotate on the x-axis by one degree per second
sun.Rotate(Vector3.right * Time.deltaTime);
    }
    
    void RotateSun()
    {
        // Rotate on the x-axis by one degree per second
        sun.Rotate(Vector3.right * Time.deltaTime);
    }
}

Because or own method is not going to be called by Unity automatically we need to call of from Update:

...

    void Update()
    {
        RotateSun();
    }
    
...

2.2 Day/Night Cycle in Minutes

To make the DayNightCycle component easier to use let’s make sure we can specify how many minutes a game day should last. This way we don’t have to wait 6 minutes every time we want to see what happens..Luckily, the math is easy.

If the sun rotates at 1 degree per seconds it will take 360 seconds to do a full rotation. 360 divided by 60 (the amount of seconds in a minute) gives us 6 so if we multiply time by 6 then a day will last exactly one minute. Then we can just divide by the amount of minutes that we want a day to last:

...

public class DayNightCycle : MonoBehaviour
{  
    public Transform sun;
    public float cycleInMinutes = 1;

    void Start()
    {
        sun.rotation = Quaternion.identity;
    }

    void Update()
    {
        RotateSun();
    }

    void RotateSun()
    {
        // Rotate 360 degrees every cycleInMinutes minutes.
        sun.Rotate(Vector3.right * Time.deltaTime * 6 / cycleInMinutes);
    }
}

3.0 Sunlight Brightness Curve

To control the sun’s brightness trough out the day and night we could look at the time of the day and base the brightness off of that but we can also look at the angle that the sun is rotated at and base it on that.
By basing the brightness on the angle of the sun it will automatically be less bright during simulated winters when the sun is lower in the sky during midday than it would be in summers!

To set up a minimum and a maximum brightness we could use two floats for min/max brightness and Lerp between those but a nicer way to do this in my opinion is to use a AnimationCurve. Using a curve makes it very easy to control exactly how long it should stay fully bright during day and how long it should stay dark at night.

Create a new variable of type AnimationCurve named sunBrightness at the top of the script. Make it public so we can see it in the inspector:

...

public class DayNightCycle : MonoBehaviour
{ 
    public Transform sun;
    public float cycleInMinutes = 1;
    public AnimationCurve sunBrightness;

...


I think visually it makes sense for a day night cycle curve to be highest in the center of the curve when it is noon, just like the sun is at its highest during noon in reality. So we’ll set up the script to evaluate the center of the curve at noon.
We can also make it easy for the user to see what’s going on by giving the curve a good default value. We can create keys at the initialisation of the curve like this:

...

public class DayNightCycle : MonoBehaviour
{ 
    public Transform sun;
    public float cycleInMinutes = 1;
    public AnimationCurve sunBrightness = new AnimationCurve(
new Keyframe(0 ,0.01f),
new Keyframe(0.15f,0.01f),
new Keyframe(0.35f,1),
new Keyframe(0.65f,1),
new Keyframe(0.85f,0.01f),
new Keyframe(1 ,0.01f)
);

...

Now that we initialise the curve with default keyframes we can just reset the component in the inspector and it will go back to the default curve that we’ve set up. You can test it by right-clicking on the script in the inspector and selecting Reset. Make sure to re-drag the sun back into the script because that is going to be reset as well:

What we want to control with the curve is the intensity variable of the Light component that is attached to the Sun GameObject:

We can get a reference to the Light component with the GetComponent() method, so we need a variable of type Light to store it in:

...

public class DayNightCycle : MonoBehaviour
{
    public Transform sun;
    public float cycleInMinutes = 1;
    public AnimationCurve sunBrightness = new AnimationCurve(
new Keyframe(0 ,0.01f),
new Keyframe(0.15f,0.01f),
new Keyframe(0.35f,1),
new Keyframe(0.65f,1),
new Keyframe(0.85f,0.01f),
new Keyframe(1 ,0.01f)
);;

    private Light sunLight;

...

We only have to get it once at the beginning of the game, so we can initialise sunLight at Start:

...
    
    void Start()
    { 
        sun.rotation = Quaternion.Euler(0,-90,0); 
        sunLight = sun.GetComponent<Light>(); 
    }

...

To control the sun’s intensity first create a separate new method named SetSunBrightness underneath RotateSun:

...

    void RotateSun()
    {
        sun.Rotate(Vector3.right * Time.deltaTime * 6 / cycleInMinutes);
    }

    void SetSunBrightness()
    {

    }

...

And call it from Update:

...    

    void Update()
    {
        RotateSun();
        SetSunBrightness();
    }

...

If we want to evaluate the curve at the middle when it is noon then we need a value that goes from 0 to 1 and then back to 0 again after a full rotation. My first idea was to use the dot product of the sun’s forward and the world’s down vector for this, which almost works. The problem is that the dot product goes from -1 up to 1 and than from 1 back down to -1, so we could only use it for a curve that has the middle of the day at the end.

To get a nice value that goes from 0 at midnight and 0.5 at noon we can use the Vector3.SignedAngle() method, which takes three vectors as arguments instead of two. This allows us to differentiate between morning and evening. SignedAngle() returns a value that goes from -180 to 180 so by dividing it by 360 we get a range from -0.5 to 0.5. By adding 0.5 we get a range from 0 to 1:

...

    void SetSunBrightness()
    {
        float sunAngle = Vector3.SignedAngle(Vector3.down,sun.forward,sun.right);
        sunAngle = sunAngle/360+0.5f;
    }

...

Now we can use the angle to Evaluate() the sunBrightness curve:

...

    void SetSunBrightness()
    { 
        float sunAngle = Vector3.SignedAngle(Vector3.down,sun.forward,sun.right);
        sunAngle = sunAngle/360+0.5f;

        // Adjust sun brightness by the angle at which the sun is rotated.
        sunLight.intensity = sunBrightness.Evaluate(sunAngle);
    }

...

Right now, the DayNightCycle controls the rotation of the sun, and the angle of the sun controls the brightness/intensity of the sunlight.
If you play the scene now the sun will rotate and you can see the intensity go up and down according to the brightness curve.
To make testing and finding good values easier we can make the script run outside of play mode so we can test settings just by rotating the sun on the x-axis.
To make the script run outside of play mode add the [ExecuteInEditMode] class attribute at the top of the script:

using System.Collections;
using System.Collections.Generic;
using UnityEngine;

[ExecuteInEditMode]
public class DayNightCycle : MonoBehaviour
{

...

Because the script now runs in edit mode when you save it the sun will also rotate when the game is not playing, which maybe isn’t ideal, so we can use the Application.isPlaying boolean from the Application class to check if we’re inside of play-mode with an if-statement:

    ...

    void Update()
    {
        if(Application.isPlaying)
        {
            RotateSun();
        }
        SetSunBrightness();
    }

...

4.0 Sunlight Color Gradient

We can control the color of the sun trough out the day in almost the same way as the intensity, by using the angle that we already calculated. But instead of a curve that returns a float we can use a gradient that returns us a color based on where we evaluate the gradient at, which is a value between 0 on the beginning to 1 on the end of the gradient.

Create a variable of type Gradient named sunColor and assign default colors to the gradient by feeding the gradient’s constructor with two arrays, containing the color and alpha keys:

...
    
public Transform sun;
public float cycleInMinutes = 1;

public AnimationCurve sunBrightness = new AnimationCurve(new Keyframe(0,0.01f),new Keyframe(0.45f,0.01f),new Keyframe(1,1));

public Gradient sunColor = new Gradient(){
    colorKeys = new GradientColorKey[2]{
        new GradientColorKey(new Color(1, 0.75f, 0.3f), 0.45f),
        new GradientColorKey(new Color(0.95f, 0.95f, 1), 0.75f),
        },
    alphaKeys = new GradientAlphaKey[2]{
        new GradientAlphaKey(1, 0),
        new GradientAlphaKey(1, 1)
};

private Light sunLight;

...

Create a new method named SetSunColor that sets the sun light color by evaluating the gradient:

...
    
    void SetSunBrightness()
    {
        ...
    }

    void SetSunColor()
    {
        sunLight.color = sunColor.Evaluate(sunAngle);
    }

...

Place a another method call to SetSunColor in Update:

...

    void Update()
    {
        if(Application.isPlaying)
        {
            RotateSun();
        }
        SetSunBrightness();
        SetSunColor();
    }

...

Because the sunAngle variable is created inside the scope of the SetSunBrightness method we cannot access it from outside that scope, so if you would run the script now it would return an error in the console. To make the sunAngle accessible to other methods in the class its better to create sunAngle at the top of the script with the other class level variables:

...
    
public Transform sun;
public float cycleInMinutes = 1;
public AnimationCurve sunBrightness = new AnimationCurve(new Keyframe(0,0.01f),new Keyframe(0.45f,0.01f),new Keyframe(1,1));

public Gradient sunColor = new Gradient(){
    colorKeys = new GradientColorKey[2]{
        new GradientColorKey(new Color(1, 0.75f, 0.3f), 0.45f),
        new GradientColorKey(new Color(0.95f, 0.95f, 1), 0.75f),
        },
    alphaKeys = new GradientAlphaKey[2]{
        new GradientAlphaKey(1, 0),
        new GradientAlphaKey(1, 1)
};

private Light sunLight;
private float sunAngle;

...

Create a method named UpdateSunAngle and move the code for the sun angle from SetSunBrightness into UpdateSunAngle:

...
    
    void SetSunBrightness()
    {
        sunAngle = Vector3.SignedAngle(Vector3.down,sun.forward,sun.right); 
        sunAngle = sunAngle/360+0.5f;

        // Adjust sun brightness by the angle at which the sun is rotated
        sunLight.intensity = sunBrightness.Evaluate(sunAngle);
    }

    void SetSunColor()
    {
        sunLight.color = sunColor.Evaluate(sunAngle);
    }

    UpdateSunAngle()
    {
        sunAngle = Vector3.SignedAngle(Vector3.down,sun.forward,sun.right);
        sunAngle = sunAngle/360+0.5f;
    }

...

And place the call in Update:

...

    void Update()
    {
        UpdateSunAngle();
        if(Application.isPlaying)
        {
            RotateSun();
        }
        SetSunBrightness();
        SetSunColor();
    }

...

Great! Now that we have placed the sunAngle variable at class level we can also use it from the SetSunColor method and from other methods too, so let’s progress and also change the colors of the sky according to a gradient and the angle of the sun as well.

5.0 Sky Colors

For this step we are going to need access to the properties/variables of the skybox shader created in part 1 of this tutorial. First we have to make sure that they’re made public on the ShaderGraph blackboard and that they have easy to use names.

Open the skybox shader and unfold the SkyColor2 and SkyColorNight2 properties in the blackboard.
Properties can be accessed from C# scripts by using the reference ID Unity provides but the ID can also be changed to something else.
Change the references to _SkyColor2 and _SkyColorNight2 and when done press Save Asset to save the changes made to the shader:

Back in the script create two new Gradient variables underneath sunColor, with default keys for the day and night sky colors:

     
...
    public Gradient sunColor = new Gradient(){
        ...
    };

    [GradientUsage(true)]
    public Gradient skyColorDay = new Gradient(){
        colorKeys = new GradientColorKey[3]{
            new GradientColorKey(new Color(0.75f, 0.3f, 0.17f), 0),
            new GradientColorKey(new Color(0.7f, 1.4f, 3), 0.5f),
            new GradientColorKey(new Color(0.75f, 0.3f, 0.17f), 1),
        },
        alphaKeys = new GradientAlphaKey[2]{
            new GradientAlphaKey(1, 0),
            new GradientAlphaKey(1, 1)
        }
    };

    [GradientUsage(true)]
    public Gradient skyColorNight = new Gradient(){
        colorKeys = new GradientColorKey[3]{
            new GradientColorKey(new Color(0.75f, 0.3f, 0.17f), 0),
            new GradientColorKey(new Color(0.44f, 1, 1), 0.5f),
            new GradientColorKey(new Color(0.75f, 0.3f, 0.17f), 1),
        },
        alphaKeys = new GradientAlphaKey[2]{
            new GradientAlphaKey(1, 0),
            new GradientAlphaKey(1, 1)
        }
    };
...

To make the gradients use HDR colors just like the SkyColor2 and SkyColorNight2 properties from the shader, simply add the [GradientUsage(true)] attribute before the gradient variable declarations:

...
    [GradientUsage(true)]
    public Gradient skyColorDay = new Gradient(){
        colorKeys = new GradientColorKey[3]{
            new GradientColorKey(new Color(0.75f, 0.3f, 0.17f), 0),
            new GradientColorKey(new Color(0.7f, 1.4f, 3), 0.5f),
            new GradientColorKey(new Color(0.75f, 0.3f, 0.17f), 1),
        },
        alphaKeys = new GradientAlphaKey[2]{
            new GradientAlphaKey(1, 0),
            new GradientAlphaKey(1, 1)
        }
    };
    
    [GradientUsage(true)]
    public Gradient skyColorNight = new Gradient(){
        colorKeys = new GradientColorKey[3]{
            new GradientColorKey(new Color(0.75f, 0.3f, 0.17f), 0),
            new GradientColorKey(new Color(0.44f, 1, 1), 0.5f),
            new GradientColorKey(new Color(0.75f, 0.3f, 0.17f), 1),
        },
        alphaKeys = new GradientAlphaKey[2]{
            new GradientAlphaKey(1, 0),
            new GradientAlphaKey(1, 1)
        }
    };
...

Add a new SetSkyColor() method to evaluate the gradients. If/else-statements are used here to check if it’s day or night and the angle is remapped to go from 0 to 1 from sunrise to sunset for the day color and from 0 to 1 from sunset to sunrise for the night color.

To get a reference to the skybox shader we can use the RenderSettings.skybox variable to get the Skybox Material that’s configured in the scene’s lighting settings window (Window > Rendering > Lighting Settings) :

The Material.SetColor() method is used to set the properties of the shader.
SetColor takes in two arguments; the first is the name of the property and the second the value we want to set it to.
(The blending between the day and night colors is done by the shader itself.):

...
    
    void UpdateSunAngle()
    {
       ...
    }

    void SetSkyColor()
    {
        if(sunAngle >= 0.25f && sunAngle < 0.75f)
RenderSettings.skybox.SetColor("_SkyColor2",skyColorDay.Evaluate(sunAngle*2f-0.5f));
        else if(sunAngle > 0.75f)
Renif(sunAngle >= 0.25f && sunAngle < 0.75f)
{
RenderSettings.skybox.SetColor("_SkyColor2",skyColorDay.Evaluate(sunAngle*2f-0.5f));
}
else if(sunAngle > 0.75f)
{
RenderSettings.skybox.SetColor("_SkyColorNight2",skyColorNight.Evaluate(sunAngle*2f-1.5f));
}
else
{
RenderSettings.skybox.SetColor("_SkyColorNight2",skyColorNight.Evaluate(sunAngle*2f+0.5f));
}
    }

....

And don’t forget to call SetSkyColor from Update:

...

    void Update()
    {
        UpdateSunAngle();
        if(Application.isPlaying)
        {
            RotateSun();
        }
        SetSunBrightness();
        SetSunColor();
        SetSkyColor();
    }

...

For the sake of the length of this tutorial I only showed how to add gradients for two of the six sky color properties of the skybox shader, but you could easily add another four HDR gradients to the script using the same methods.

Another good property you could control from the DayNightCycle script would be the the SkyIntensity. You can use an AnimationCurve for it with the same methods used for the SunBrightness in step 3.
To set the SkyIntensity from the script you can use the Material.SetFloat() method like this:

RenderSettings.skybox.SetFloat("_SkyIntensity",skyIntensity.Evaluate(sunAngle));

6.0 Stars Movement

For the stars movement we can also use the sun’s rotation. Because we’re rotating the sun around the x-axis we only have to offset the shader’s stars texture on the x-axis by a certain speed. The speed depends on the stars texture that you’re using so you may have to adjust it until it looks like the sun and the stars are moving at the same speed.

First create a new public starsSpeed variable at the top with a default speed that is probably around 8:

...

    [GradientUsage(true)]
    public Gradient skyColorNight = new Gradient(){
       ...
    };

    public float starsSpeed = 8;

...

Then create the MoveStars method:

...    

    void SetSkyColor()
    {
       ...
    }

    void MoveStars()
    {
        RenderSettings.skybox.SetVector("_StarsOffset",new Vector2(sunAngle * starsSpeed,0));
    }

...

And then add the method call in Update and save the script :

...

    void Update()
    {
        UpdateSunAngle();
        if(Application.isPlaying)
        {
            RotateSun();
        }
        SetSunBrightness();
        SetSunColor();
        SetSkyColor();
        MoveStars();
    }

...

As you can see in the MoveStars method we are trying to use SetVector() to set the _StarsOffset property of the skybox shader so make sure you have the reference named properly in the blackboard of the shader graph, and save the graph afterwards:

Your stars should now appear to be moving when you rotate the sun!

7.0 Clouds Movement

It wouldn’t make a lot of sense to base the clouds movement on the sun so for the clouds we can just offset the shader’s cloud texture based on the time multiplied by a speed for the x and y texture offset.

Create a public Vector2 variable named cloudsSpeed at the top of the class:

...

    public float starsSpeed = 8;
    public Vector2 cloudsSpeed = new Vector2(1,1);
...

Create the MoveClouds method:

...

    void MoveStars()
    {
        ...
    }

    void MoveClouds()
    {
        RenderSettings.skybox.SetVector("_CloudsOffset", (Vector2)RenderSettings.skybox.GetVector("_CloudsOffset") + Time.deltaTime * cloudsSpeed);
    }

...

Call this method during play mode only and save the script:

...

    void Update()
    {
        UpdateSunAngle();
        if(Application.isPlaying)
        {
            RotateSun();
            MoveClouds();
        }
        SetSunBrightness();
        SetSunColor();
        SetSkyColor();
        MoveStars();
    }

...

And don’t forget to make the property exposed, name the reference on the shader graph blackboard and to save the shader graph again:

Your clouds should now appear to be moving when you press play!

Because we’re only using one texture and one layer for the clouds it doesn’t look great yet but it’s easy to add another clouds layer with larger or smaller clouds and have it scroll slightly slower or faster. Another thing you could do is to add a cloud particle system somewhere high in the scene and only use the texture from the shader for the higher clouds in the background.
A third idea would be to use noise with different octaves instead of a texture in the skybox shader graph (noise was also used to create the clouds texture in the first place), this would make it look a lot better but is also less performant. Just remember that looking up data from texture files is very efficient.

8.0 Fog Color

Fog is something that can have a great visual impact on the scene because it can help a lot with desaturating and blending colors towards the horizon, giving a great sense of athmosphere and depth to your scene. If you change the sky- and the light color with the day/night cycle but not the fog color it can look very off.

Start with creating another gradient variable at the top of the class. This gradient doesn’t have to be HDR because we cannot set fog to an HDR color:

...    

    public float starsSpeed = 8;
    public Vector2 cloudsSpeed = new Vector2(1,-1);

    public Gradient fogColor = new Gradient(){
        colorKeys = new GradientColorKey[5]{
            new GradientColorKey(new Color(0.83f, 0.9f, 0.9f), 0),
            new GradientColorKey(new Color(1, 0.54f, 0.37f), 0.25f),
            new GradientColorKey(new Color(0.95f, 0.95f, 1), 0.5f),
            new GradientColorKey(new Color(1, 0.54f, 0.37f), 0.75f),
            new GradientColorKey(new Color(1, 0.9f, 0.9f), 1),
        },
        alphaKeys = new GradientAlphaKey[2]{
            new GradientAlphaKey(1, 0),
            new GradientAlphaKey(1, 1)
        }
    };

...

Create the SetFogColor() method and set the scene’s fog color using the same RenderSettings class we used to get the scene’s skybox material:

    
...
   
    void MoveClouds()
    {
        ...
    }
    
    void SetFogColor()
    {
        RenderSettings.fogColor = fogColor.Evaluate(sunAngle);
    }

...

Call it from update and save the script:

...

    void Update()
    {
        UpdateSunAngle();
        if(Application.isPlaying)
        {
            RotateSun();
            MoveClouds();
        }
        SetSunBrightness();
        SetSunColor();
        SetSkyColor();
        MoveStars();
        SetFogColor();
    }

...

Another thing you can do with the fog is changing the fog density based on the rotation of the sun. You can use another AnimationCurve for it just like with the sunBrightness.
All the variables you see in the Lighting Settings window can be accessed trough the RenderSettings class:

RenderSettings.fogDensity = fogDensity.Evaluate(sunAngle);

9.0 UnityEvents

For this step I think it will be nice to see how we can use the DayNightCycle component for things that have nothing to do with the sun or the skybox shader. A good example for this is having other lights in the scene, like streetlights etc, turn on and off based on the time of the day.
An easy way to do this is by using different ‘time of the day’ events for morning, noon, evening and night in the DayNightCycle script, that other scripts can subscribe methods to.

Because it doesn’t make sense to base time of the day on the direction of the sun in this case, we will just use decimal time for this. Decimal time is just time that goes from 0 midnight to 0.5 at noon, so it has a range of 0 <> 1.
Having decimal time in a day/night cycle component is really useful. Because it goes from zero to one it can easily be used to directly evaluate gradients and animation curves just like we did with the sun angle. So if you’d rather control some value based on the game time instead of the angle of the sun you can just use the decimal time value for it!:

RenderSettings.fogColor = fogColor.Evaluate(decimalTime);

Create a float variable named decimalTime at the top of the class and also create a float variable named DecimalTime with a getter and setter.
By making ‘set’ private we can make sure other scripts cannot set the value but can still get it by using DayNightCycle.DecimalTime :

    // Fractional game time, range 0 <> 1. 0 is midnight, 0.5 is noon..
    private float decimalTime = 0.0f;
    // Get time from other scripts by using DayNightCycle.DecimalTime.
    public float DecimalTime{get{return decimalTime;} private set{decimalTime = value;}}

Add the UpdateDecimalTime method:

...    

    void SetFogColor()
    {
       ...
    }

    void UpdatedecimalTime()
    {
        // 0.25 because the day starts at morning. Time.time times 6 because 360 degrees in a full rotation.
        // Modulo(%) 1 makes the value go from 0 to 1 repeatedly.
        decimalTime = (0.25f + Time.time * 6 / cycleInMinutes / 360)%1;
        // Uncomment to see decimal time in the console
        // Debug.Log(decimalTime); 
    }
}

And call it from Update:

...    

    void Update()
    {
        UpdateSunAngle();

        if(Application.isPlaying)
        {   
            UpdatedecimalTime();
            RotateSun();
            MoveClouds();
        }
        SetSunBrightness();
        SetSunColor();
        SetSkyColor();
        MoveStars();
        SetFogColor();
    }

...

For the time of the day events start with adding a using statement for the UnityEvents at the top of the script above the class:

using System.Collections;
using System.Collections.Generic;
using UnityEngine;
using UnityEngine.Events;

[ExecuteInEditMode]
public class DayNightCycle : MonoBehaviour
{

...

Create four UnityEvent variables for midnight, morning, noon and evening:

...
    
    public Gradient fogColor = new Gradient(){
        ...
    };
    
    public UnityEvent onMidnight;
    public UnityEvent onMorning;
    public UnityEvent onNoon;
    public UnityEvent onEvening;

...

Also create a new enum for the time of the day (‘enum value type data type’ is what they’re called. I just think of them as switches with names for the positions.) and two variables off that enum data type. One for the time of the day itself and one to check against if the time of the day has changed:

...
    
    public UnityEvent onMidnight;
    public UnityEvent onMorning;
    public UnityEvent onNoon;
    public UnityEvent onEvening;

    // enum value type data type
    private enum TimeOfDay{Night,Morning,Noon,Evening}
    
    // variables of enum type TimeOfDay
    private TimeOfDay timeOfDay = TimeOfDay.Night;
    private TimeOfDay TODMessageCheck = TimeOfDay.Night;

...

If you save the script now, you can see that the UnityEvents (unlike normal C# Events) are neatly displayed in the inspector allowing us to simply drag other GameObjects and components into them to have them subscribe methods to the events. You can press on one of the event’s plus buttons, drag any GameObject into the created slot and then choose any public method that you want to be called when the event happens:

Now that we created the messages/events, we still have to send/fire them.
Because we have the decimalTime we can easily check if it’s time to trigger the UnityEvents and then trigger them, so subscribers get the messages.
Inside Update we can check what the decimal time is and use it to change boolean flags or in this case enums to keep track of what part of the day it currently is. I chose enums for this because it is easier to change one enum instead of four booleans for night, morning, noon and evening.

Create a UpdateTimeOfDay method:

...

    void UpdatedecimalTime() {
        ...
    }

    void UpdateTimeOfDay() {

        if(decimalTime > 0.25 && decimalTime < 0.5f) {
            timeOfDay = TimeOfDay.Morning;
        }
        else if(decimalTime > 0.5f && decimalTime < 0.75f) {
            timeOfDay = TimeOfDay.Noon;
        }
        else if(decimalTime > 0.75f) {
            timeOfDay = TimeOfDay.Evening;
        }
        else {
            timeOfDay = TimeOfDay.Night;
        }
    }
}

To make sure the time of the day messages are only sent once a day we can check if the time of day has changed by comparing it to the extra enum variable:

    void UpdateTimeOfDay() {

        if(decimalTime > 0.25 && decimalTime < 0.5f) {
            timeOfDay = TimeOfDay.Morning;
        }
        else if(decimalTime > 0.5f && decimalTime < 0.75f) {
            timeOfDay = TimeOfDay.Noon;
        }
        else if(decimalTime > 0.75f) {
            timeOfDay = TimeOfDay.Evening;
        }
        else {
            timeOfDay = TimeOfDay.Night;
        }

        // Check if the timeOfDay has changed. If so, invoke the event.
        if(TODMessageCheck != timeOfDay) {
            InvokeTimeOfDayEvent();
            TODMessageCheck = timeOfDay;
        }
    }
}

Create the InvokeTimeOfDayEvent() method that we are already calling from UpdateTimeOfDay() to trigger the events.
We can use a switch statement instead of if-else statements to check what time of day it is because it is more efficient.
Because it’s better to not trigger the events when there are no subscribers we can use if-statements to check if subscribers isn’t ‘null’:

...

    void UpdateTimeOfDay() {
        ...
    }

    void InvokeTimeOfDayEvent()
    {
        switch (timeOfDay) {
            case TimeOfDay.Night:
                if(onMidnight != null) onMidnight.Invoke();
                Debug.Log("OnMidnight");
                break;
            case TimeOfDay.Morning:
                if(onMorning != null) onMorning.Invoke();
                Debug.Log("OnMorning");
                break;
            case TimeOfDay.Noon:
                if (onNoon != null) onNoon.Invoke();
                Debug.Log("OnNoon");
                break;
            case TimeOfDay.Evening:
                if(onEvening != null) onEvening.Invoke();
                Debug.Log("OnEvening");
                break;
        }
    }
}

Call the UpdateTimeOfDay method from Update:

...    

    void Update()
    {
        UpdateSunAngle();

        if(Application.isPlaying)
        {   
            UpdatedecimalTime();
            UpdateTimeOfDay();
            RotateSun();
            MoveClouds();
        }
        SetSunBrightness();
        SetSunColor();
        SetSkyColor();
        MoveStars();
        SetFogColor();
    }

...

If you press play now you can see in the console that the events are only triggered once with every time of the day change!:

10.0 Timed Lights and Particle Systems

Now that we have a nice system for time of the day events we can easily make other stuff happen when those timed events are triggered.
There are two ways to do this, the drag and drop in the inspector method using the public UnityEvents we created or the regular C# script method of subscribing and unsubscribing.
We are going to use both methods, for the lights we’ll use the drag and drop method and for the particle systems we’ll use the subscribing from script method.

10.1 Lights

Subscribing methods to UnityEvents in the Inspector

First make sure you have a point- or a spot-light in your scene. You can create a new light with the menu bar GameObject > Light > and then choose the type that you want. For the demo scene that I’m using I created a model of a street light in Blender with a light component attached to it as a child object in Unity and also an emissive material which coupled with the bloom post processing camera effect gives a nice glow around the streetlight:

Keep in mind that point or spotlights are essentially extra camera’s in your scene so they are pretty expensive to use. If you’re using the URP there is a max of 8 to the amount of pixel lights a texture can receive. Which means that if you have objects being hit with light from more than 8 lights, those extra lights will be culled and you will start to see artifacts. For the image above the object limit was set to eight and for the image below to three:

To try out the public UnityEvents in the inspector with one light we can simply create a new slot for the light in the OnEvening event and drag the GameObject that has the Light component in there:

Then we can select the Light component from the ‘No Function’ drop down menu and then choose ‘bool enabled’. We can simply say that the light should be enabled OnEvening:

Then we can do the same for the OnMorning event but instead of enabling the light we disable it:

If you press play now you should see your light be turned of and on with the corresponding events and this method is fine if we only have one or a couple of lights in the scene. To do this for a whole bunch of lights we can create our own SwitchStreetLights() method and call that from the public UnityEvent..

First create a new C# script called DNC_StreetLights.cs and drag it onto a new empty GameObject named StreetLights in your scene:

Then parent all your streetlights to the empty GameObject:

Inside StreetLights.cs create a new public method called SwitchStreetLights() with a type bool parameter named enabled. This bool will show up in the inspector inside of the public UnityEvent when we select the method, just like the ‘enabled’ bool from the Light component :

using System.Collections;
using System.Collections.Generic;
using UnityEngine;

public class DNC_StreetLights : MonoBehaviour
{
    // Start is called before the first frame update
    void Start()
    {
        
    }

    // Update is called once per frame
    void Update()
    {
        
    }

    public void SwitchStreetLights(bool enabled)
    {

    }
}

Inside the StreetLight.cs method we can simply use a foreach loop to enable or disable all the light components that we find on the child GameObjects.

If you need to disable the emission property on the material from the streetlights you can loop trough all the child GameObjects mesh renderers and then loop again trough all the mesh renderer’s materials if the mesh has multiple materials assigned:

using System.Collections;
using System.Collections.Generic;
using UnityEngine;

public class DNC_StreetLights : MonoBehaviour
{
    public void SwitchStreetLights(bool enabled)
    {
        foreach(Light light in GetComponentsInChildren())
        {
            light.enabled = enabled;
        }

        foreach(Renderer renderer in GetComponentsInChildren())
        {
            foreach(Material material in renderer.materials)
            {
                if(enabled) 
                {
                    material.EnableKeyword("_EMISSION");
                }
                else 
                { 
                    material.DisableKeyword("_EMISSION");
                }
            }
        }
    }
}

Now instead of the single light we can drag in the StreetLights GameObject into the public UnityEvents from the DayNightCycle script and select the public SwitchStreetLights() method:

Great! Now all the lights turn/on and off when the UnityEvents are triggered!

10.2 Particles

Subscribing to UnityEvents from scripts

To see how we can subscribe methods directly to the UnityEvents from other scripts we can use some particle systems as an example. In the demo scene the cooling towers have a particle system that emits steam-like particles so maybe it makes sense to have them emit less steam at night when people are using less power.. You could also use this for some fog particle system in the morning for instance..

Create a particle system or multiple particle systems that you want to control with the DayNightCycle time of day events and create an empty parent object to hold them:

Also create a new script named DNC_ParticleSystems.cs or something like that and add it to the ParticleSystems GameObject:

Inside the ParticleSystem.cs file create a new public method named SetParticleEmission() that loops trough all the child particle systems and changes the emission to a low or a high amount. This time instead of having a bool parameter we can use an int for the amount of particles that we want to emit:

using System.Collections;
using System.Collections.Generic;
using UnityEngine;

public class DNC_ParticleSystems : MonoBehaviour
{
    // Start is called before the first frame update
    void Start()
    {
            
    }

    // Update is called once per frame
    void Update()
    {

    }

    public void SetParticleEmission(int amount)
    {
        foreach(ParticleSystem ps in GetComponentsInChildren())
        {
            var em = ps.emission;
            em.rateOverTime = amount;
        }
    }

}

To subscribe the SwitchParticleEmission() method to the DayNightCycle components morning and evening events we first need a reference to the DayNightCycle component in our scene so create a private variable of type DayNightCycle at the top of the class to store it and initialise its value in the Awake() method:

...

public class DNC_ParticleSystems : MonoBehaviour
{
    private DayNightCycle dayNightCycle;

    void Awake()
    {
        dayNightCycle = FindObjectOfType<DayNightCycle>();
    }

    public void SetParticleEmission(int amount)
    {
        ...
    }
}

Now we can subscribe to the morning and night events inside of the OnEnable() method, and unsubscribe in the OnDisable() method. This way we don’t get any errors when the particle systems GameObject is destroyed or inactive in the scene. When subscribing to events from script like this you always have to make sure that you also unsubscribe and usually it is best to do this in OnEnable and OnDisable.
The way that we can (un)subscribe methods to UnityEvents is by using the events AddListener() and RemoveListener() methods. Subscribing a method without any parameters would look like this:


    void OnEnable()
    {
        dayNightCycle.onMorning.AddListener(SetParticleEmission);
    }

    void OnDisable()
    {
        dayNightCycle.onMorning.RemoveListener(SetParticleEmission);

    }

But since our method has an int parameter we need to subscribe the call with amount argument/value wrapped as a delegate like so:

dayNightCycle.onMorning.AddListener(delegate{SetParticleEmission(100);});

This would work fine if we only needed to subscribe the method but we also want to unsubscribe it and since we’re not subscribing the method call directly but as a delegate, we need to store a reference to that delegate. For this we can store it as a UnityAction that we can subscribe one or multiple methods or delegates to. Instead adding the method call as a listener we can subscribe the unity actions.. (It’s a little bit more complex as to using C# Events from script but then again, those don’t show up neatly in the Inspector like UnityEvents.)

First add a using UnityEngine.Events directive outside of the class so we can use UnityActions in this script. Then create two UnityAction variables at the top inside of the class:

using System.Collections;
using System.Collections.Generic;
using UnityEngine;
using UnityEngine.Events;

public class DNC_ParticleSystems : MonoBehaviour
{
    private DayNightCycle dayNightCycle;
    private UnityAction setEmissionLow;
    private UnityAction setEmissionHigh;

    ...

Now we can subscribe the delegates to the actions in the OnEnable() method and add the actions as listeners to the unity events afterwards. This way we can also unsubscribe them without any errors in the OnDisable() method:

...

    void Awake()
    {
        dayNightCycle = FindObjectOfType();
    }

    void OnEnable()
    {
        setEmissionHigh += delegate{SetParticleEmission(100);};
        setEmissionLow += delegate{SetParticleEmission(20);};

        dayNightCycle.onMorning.AddListener(setEmissionHigh);
        dayNightCycle.onMidnight.AddListener(setEmissionLow);
    }

    void OnDisable()
    {
        dayNightCycle.onMorning.RemoveListener(setEmissionHigh);
        dayNightCycle.onMidnight.RemoveListener(setEmissionLow);
    }

...

Great now we’re controlling both the lights and the particles in the scene and we’ve learned both ways of subscribing to UnityEvents.

If you need particle systems to change gradually then something you can try out for yourself is to create an AnimationCurve for the particle emission and have the particle systems gradually change throughout the day by evaluating the curve in the same way as with the sun’s brightness. Since there already is a reference to the DayNightCycle component in the ParticleSystems.cs script you can get the decimal time for the evaluation of the curve like this:
particlesCurve.Evaluate(dayNightCycle.DecimalTime);

11.0 Finally

Below you can find the DayNightCycle, DNC_StreetLights and DNC_ParticleSystems scripts in their entirety, including some small improvements that aren’t mentioned in the tutorial;

I placed [Header("")] tags above some of the variables of the DayNightCycle script to make it easier to read in the inspector:

I also created a public static reference to the DayNightCycle instance so other scripts don’t need a reference to the DayNightCycle component and can get the Decimal time directly like this:
Debug.Log( DayNightCycle.instance.DecimalTime );
But this only works if you have one DayNightCycle in your scene.

There is still a lot that can be improved and added to this DayNightCycle script that would be too much for the length of this tutorial. Some ideas and things that can be improved:

  • More gradients could be added for the other sky colors of the shader and to other shader properties.
  • A better way to control the start time of the day.
  • Random weather events that don’t happen everyday.
  • A second directional light for the moonlight.
  • In-game clocks that show the in-game time.

Finally,..What would you like to learn next in maybe another part to this tutorial, or what did you miss in this tutorial? Feel free to add any suggestions that you might have in the comments!

BUY ME A COFFEE

Donate $5 to buy me a coffee so I have the fuel I need to keep producing great tutorials!

$5.00

DayNightCycle.cs

using System.Collections;
using System.Collections.Generic;
using UnityEngine;
using UnityEngine.Events;

[ExecuteInEditMode]
public class DayNightCycle : MonoBehaviour
{
public static DayNightCycle instance;

[Header("Time")]
public float cycleInMinutes = 1;

// Fractional game time, range 0 <> 1. 0 is midnight, 0.5 is noon..
private float decimalTime = 0.0f;
// Get time from other scripts by using DayNightCycle.DecimalTime.
public float DecimalTime{get{return decimalTime;} private set{decimalTime = value;}}

[Header("Sun")]
public Transform sun;
public AnimationCurve sunBrightness = new AnimationCurve(
new Keyframe(0 ,0.01f),
new Keyframe(0.15f,0.01f),
new Keyframe(0.35f,1),
new Keyframe(0.65f,1),
new Keyframe(0.85f,0.01f),
new Keyframe(1 ,0.01f)
);
public Gradient sunColor = new Gradient(){
colorKeys = new GradientColorKey[3]{
new GradientColorKey(new Color(1, 0.75f, 0.3f), 0),
new GradientColorKey(new Color(0.95f, 0.95f, 1), 0.5f),
new GradientColorKey(new Color(1, 0.75f, 0.3f), 1),
},
alphaKeys = new GradientAlphaKey[2]{
new GradientAlphaKey(1, 0),
new GradientAlphaKey(1, 1)
}
};

[Header("Sky")]
[GradientUsage(true)]
public Gradient skyColorDay = new Gradient(){
colorKeys = new GradientColorKey[3]{
new GradientColorKey(new Color(0.75f, 0.3f, 0.17f), 0),
new GradientColorKey(new Color(0.7f, 1.4f, 3), 0.5f),
new GradientColorKey(new Color(0.75f, 0.3f, 0.17f), 1),
},
alphaKeys = new GradientAlphaKey[2]{
new GradientAlphaKey(1, 0),
new GradientAlphaKey(1, 1)
}
};

[GradientUsage(true)]
public Gradient skyColorNight = new Gradient(){
colorKeys = new GradientColorKey[3]{
new GradientColorKey(new Color(0.75f, 0.3f, 0.17f), 0),
new GradientColorKey(new Color(0.44f, 1, 1), 0.5f),
new GradientColorKey(new Color(0.75f, 0.3f, 0.17f), 1),
},
alphaKeys = new GradientAlphaKey[2]{
new GradientAlphaKey(1, 0),
new GradientAlphaKey(1, 1)
}
};

[Header("Stars")]
public float starsSpeed = 8;
[Header("Clouds")]
public Vector2 cloudsSpeed = new Vector2(1,-1);

[Header("Fog")]
public Gradient fogColor = new Gradient(){
colorKeys = new GradientColorKey[5]{
new GradientColorKey(new Color(0.66f, 1, 1), 0),
new GradientColorKey(new Color(0.88f, 0.62f, 0.43f), 0.25f),
new GradientColorKey(new Color(0.88f, 0.88f, 1), 0.5f),
new GradientColorKey(new Color(0.88f, 0.62f, 0.43f), 0.75f),
new GradientColorKey(new Color(0.66f, 1, 1), 1),
},
alphaKeys = new GradientAlphaKey[2]{
new GradientAlphaKey(1, 0),
new GradientAlphaKey(1, 1)
}
};

[Header("Time Of Day Events")]
public UnityEvent onMidnight;
public UnityEvent onMorning;
public UnityEvent onNoon;
public UnityEvent onEvening;

// enum value type data type
private enum TimeOfDay{Night,Morning,Noon,Evening}

// variables of enum type TimeOfDay
private TimeOfDay timeOfDay = TimeOfDay.Night;
private TimeOfDay TODMessageCheck = TimeOfDay.Night;

private Light sunLight;
private float sunAngle;


void Awake()
{
if (DayNightCycle.instance == null) instance = this;
else Debug.Log("Warning; Multiples instances found of {0}, only one instance of {0} allowed.",this);
}

void Start()
{
sun.rotation = Quaternion.Euler(0,-90,0);
sunLight = sun.GetComponent<Light>();
}

void Update()
{
UpdateSunAngle();

if(Application.isPlaying)
{
UpdatedecimalTime();
UpdateTimeOfDay();
RotateSun();
MoveClouds();
}
SetSunBrightness();
SetSunColor();
SetSkyColor();
MoveStars();
SetFogColor();

//print ((Time.time * 6 / cycleInMinutes / 360)%1);

}


void RotateSun()
{
// Rotate 360 degrees every cycleInMinutes minutes.
sun.Rotate(Vector3.right * Time.deltaTime * 6 / cycleInMinutes);
}

void SetSunBrightness()
{
// angle = Vector3.Dot(Vector3.down,sun.forward); // range -1 <> 1 but with non-linear progression, meaning it will go up and down between -1 and 1. Not very usefull because then we don't know the difference between sunrise and sunset.
sunAngle = Vector3.SignedAngle(Vector3.down,sun.forward,sun.right); // range -180 <> 180 with linear progression, meaning -180 is midnight -90 is morning 0 is midday and 90 is sunset.
sunAngle = sunAngle/360+0.5f;

// Adjust sun brightness by the angle at which the sun is rotated
sunLight.intensity = sunBrightness.Evaluate(sunAngle);
}

void SetSunColor()
{
sunLight.color = sunColor.Evaluate(sunAngle);
}

void UpdateSunAngle()
{
sunAngle = Vector3.SignedAngle(Vector3.down,sun.forward,sun.right);
sunAngle = sunAngle/360+0.5f;
}

void SetSkyColor()
{
if(sunAngle >= 0.25f && sunAngle < 0.75f)
{
RenderSettings.skybox.SetColor("_SkyColor2",skyColorDay.Evaluate(sunAngle*2f-0.5f));
}
else if(sunAngle > 0.75f)
{
RenderSettings.skybox.SetColor("_SkyColorNight2",skyColorNight.Evaluate(sunAngle*2f-1.5f));
}
else
{
RenderSettings.skybox.SetColor("_SkyColorNight2",skyColorNight.Evaluate(sunAngle*2f+0.5f));
}
}

void MoveStars()
{
RenderSettings.skybox.SetVector("_StarsOffset",new Vector2(sunAngle * starsSpeed,0));
}

void MoveClouds()
{
RenderSettings.skybox.SetVector("_CloudsOffset", (Vector2)RenderSettings.skybox.GetVector("_CloudsOffset") + Time.deltaTime * cloudsSpeed);
}

void SetFogColor()
{
RenderSettings.fogColor = fogColor.Evaluate(sunAngle);
// Debug.Log(sunAngle);
}

void UpdatedecimalTime()
{
// 0.25 because the day starts at morning. Time.time times 6 because 360 degrees in a full rotation.
// Modulo(%) 1 makes the value go from 0 to 1 repeatedly.
decimalTime = (0.25f + Time.time * 6 / cycleInMinutes / 360)%1;
// Debug.Log(decimalTime); // Uncomment to see decimal time in the console
}

void UpdateTimeOfDay()
{
if(decimalTime > 0.25 && decimalTime < 0.5f)
{
timeOfDay = TimeOfDay.Morning;
}
else if(decimalTime > 0.5f && decimalTime < 0.75f)
{
timeOfDay = TimeOfDay.Noon;
}
else if(decimalTime > 0.75f)
{
timeOfDay = TimeOfDay.Evening;
}
else
{
timeOfDay = TimeOfDay.Night;
}

// Check if the timeOfDay has changed. If so, invoke the event.
if(TODMessageCheck != timeOfDay)
{
InvokeTimeOfDayEvent();
TODMessageCheck = timeOfDay;
}
}

void InvokeTimeOfDayEvent()
{
switch (timeOfDay) {
case TimeOfDay.Night:
if(onMidnight != null) onMidnight.Invoke();
Debug.Log("OnMidnight");
break;
case TimeOfDay.Morning:
if(onMorning != null) onMorning.Invoke();
Debug.Log("OnMorning");
break;
case TimeOfDay.Noon:
if (onNoon != null) onNoon.Invoke();
Debug.Log("OnNoon");
break;
case TimeOfDay.Evening:
if(onEvening != null) onEvening.Invoke();
Debug.Log("OnEvening");
break;
}
}
}


DNC_StreetLights.cs

using System.Collections;
using System.Collections.Generic;
using UnityEngine;

public class DNC_StreetLights : MonoBehaviour
{
public void SwitchStreetLights(bool enabled)
{
foreach(Light light in GetComponentsInChildren<Light>())
{
light.enabled = enabled;
}

foreach(Renderer renderer in GetComponentsInChildren<Renderer>())
{
foreach(Material material in renderer.materials)
{
if(enabled)
{
material.EnableKeyword("_EMISSION");
}
else
{
material.DisableKeyword("_EMISSION");
}
}
}
}
}


DNC_ParticleSystems.cs

using System.Collections;
using System.Collections.Generic;
using UnityEngine;
using UnityEngine.Events;

public class DNC_ParticleSystems : MonoBehaviour
{
private DayNightCycle dayNightCycle;
private UnityAction setEmissionLow;
private UnityAction setEmissionHigh;

void Awake()
{
dayNightCycle = FindObjectOfType<DayNightCycle>();
}

void OnEnable()
{
setEmissionHigh += delegate{SetParticleEmission(100);};
setEmissionLow += delegate{SetParticleEmission(20);};

dayNightCycle.onMorning.AddListener(setEmissionHigh);
dayNightCycle.onMidnight.AddListener(setEmissionLow);
}

void OnDisable()
{
dayNightCycle.onMorning.RemoveListener(setEmissionHigh);
dayNightCycle.onMidnight.RemoveListener(setEmissionLow);
}

public void SetParticleEmission(int amount)
{
foreach(ParticleSystem ps in GetComponentsInChildren<ParticleSystem>())
{
var em = ps.emission;
em.rateOverTime = amount;
}
}
}


Website Powered by WordPress.com.