A Guide to a Better Looking and More Realistic Day/Night Cycle

Preface

Of the various tools available to make a game look better, lighting is amongst the most powerful. Im not going to kid myself and say that Roblox’s lighting engine is amazing, but its vastly improved over what we had even a few years ago to the point where you can make things that look “good”, and not just “good for a roblox game”.

While many games will spend the time making there lighting look good using all the features available, it seems that as soon as the game incorporates any form of day cycle, the lighting reverts to basically stock settings. Im not going to show you how to make good looking lighting, this tutorial does a far better job then I could. Instead my goal is to hopefully show you how to keep your lighting at that standard, regardless of the time.

Note1: I am not a lua scripter, but I did my best to strike a balance between readability and optimization.

Note2: While you can apply this tutorial to compatibility, I would defiantly recommend using shadow map to get the most out of the game visually. If for some reason shadows are an issue for your game, you can still get very good results by using voxel.

Note3:I have also made the finished script available here to allow easier following along with this guide: https://www.roblox.com/library/4413337933/Day-Night-Cycle

Changing the time of day

The most basic role a day/night script can have it to advance the time. There are several ways of doing this, but I have opted to use the old fashion GetMinutesAfterMidnight+1 technique.

Before we can do this though, we need to know both how long we want one day to take, and how often we want to adjust it. In my example, I want my day to take 15 seconds and I am going to want it to update 30 times a second, though in an actual game I would be updating no more then once per second.

Once we found these numbers, we are going to use those numbers to calculate our timeShift, or how long many “minutes” you want to progress every time you update the time. This is calculated by

(“how long a day takes (in seconds)”/ “how long between updates”)/1440= how many times you need to update

In my example that’s going to be

(15/(1/30))/1440 = 5

Now that we have that number, we can start scripting. This is a basic script that finds the current time (using Roblox’s minutes after midnight framework), adds time shift to it, and waits however long you have specified tick to be before repeating. While you can do this in just three lines of code, I have expanded it out to work with future features.

--time changer
local mam --minutes after midnight
local timeShift = 2 --how many minutes you shift every "tick"
local waitTime = 1/30 --length of the tick
local pi = math.pi

while true do
     --time changer
     mam = game.Lighting:GetMinutesAfterMidnight() + timeShift
     game.Lighting:SetMinutesAfterMidnight(mam)
     mam = mam/60

     --tick
     wait(waitTime)
end

This will generate a day/night cycle that looks like this which is entirely usable, but we can do better.

Note: Due to compression, the gifs ended up looking far worse then the original video I took. Unfortunately I cant upload those here, so you are stuck with this.

If you want to use a different method of setting the time, you can use the following instead
--time changer
local mam --minutes after midnight
local pi = math.pi

--Time Setting Code Goes Here

--time changer
mam = game.Lighting:GetMinutesAfterMidnight()
mam = mam/60

--rest of the tutorial features go here

Adjusting brightness

In the real world, night time is darker then day time. In Roblox, there are two ways of achieving this effect, ether changing the brightness, or changing the outdoor ambient. While I prefer setting the outdoor ambient to a set value and just adjusting the brightness, I will be showing how you can adjust outdoor ambient later.

To achieve the effect, I will be using a sine wave. For those of you who are not familiar with what a sine wave is, they basically look like this.
image
As this is a tutorial about how to make a day/night cycle script and not a tutorial on how sine waves work, I have generated a suitable sine wave for you. If you would like a full tutorial on manipulating sine waves and how they can be used for game development, let me know in the comments.

First, you are going to need to find two numbers. Set the time to 00:00 and adjust the brightness until you are satisfied with how dark it is. Remember what you set it to, this will be your darkest value. Note: I do not suggest having a darkest value of less then one. If you find that 1 is not dark enough, set the brightness to 1 and reduce the outdoor ambient until you get the desired lighting conditions. Once you have the darkest value sorted, set the time to 12:00 and tune the brightness until satisfied. This will be your brightest value. In my example my darkest value is 1, and my brightest value is 3.

Once you found the brightest and darkest value, you are going to use them to generate two new values.

Amplitude: This can be found by (“brightest” - “darkest”)/2. In my example this would be, (3-1)/2 which will equal 1

Offset: This can be found by “darkest ”+“amplitude”. In my example this would be 1+1 which will equal 2.

With these numbers, you can now build a sine wave. For a visual example of how it works, here is the equation with my example variables plugged into it.

image

As you can see, at zero the brightness will be 1, and it will get brighter until 12, at which point it will start getting darker until it gets back to 24 (which is the same as zero on a 24 hour clock).

Writing this formula as a script looks like this game.Lighting.Brightness = amplitude*math.cos(mam*(math.pi/12)+math.pi)+offset. However, as we will be dealing with more then one sine wave I have written it into the finished script as this

--time changer
local mam --minutes after midnight
local timeShift = 3.2 --how many minutes you shift every "tick"
local waitTime = 1/30 --legnth of the tick
local pi = math.pi

--brightness
local amplitudeB = 1
local offsetB = 2

while true do
     --time changer
     mam = game.Lighting:GetMinutesAfterMidnight() + timeShift
     game.Lighting:SetMinutesAfterMidnight(mam)
     mam = mam/60

     --brightness
     game.Lighting.Brightness = amplitudeB*math.cos(mam*(pi/12)+pi)+offset
     --tick
     wait(waitTime)
end

With this implemented, you should be getting a similar effect to this.

Adjusting outdoor ambient

If for some reason your game is still using compatibility, outdoor ambient is likely the better choice. Outside this though, I would not be using it as a primary source for changing the ingame brightness, though if you want to use both there is nothing really stopping you.

Should you decide you want to use outdoor ambient as your brightness controller, the process is identical to setting up brightness, except you swap brightness for outdoor ambient. The main difference is in how you script it.

Outdoor ambient takes a color 3 value, so we will have to declare our sine wave output to a variable first. Our resulting script should now look like this.


--time changer
local mam --minutes after midnight
local timeShift = 3.2 --how many minutes you shift every "tick"
local waitTime = 1/30 --legnth of the tick
local pi = math.pi

--brightness
local amplitudeB = 1
local offsetB = 2

--outdoor ambieant
local var
local amplitudeO = 20
local offsetO = 100

while true do
     --time changer
     mam = game.Lighting:GetMinutesAfterMidnight() + timeShift
     game.Lighting:SetMinutesAfterMidnight(mam)
     mam = mam/60

     --brightness
     game.Lighting.Brightness = amplitudeB*math.cos(mam*(pi/12)+pi)+offsetB

     --outdoor ambient
     var=amplitudeO*math.cos(mam*(pi/12)+pi)+offsetO
     game.Lighting.OutdoorAmbient = Color3.fromRGB(var,var,var)

     --tick
     wait(waitTime)
end

Shadow Softness

While not something I would generally bother with, in the real-world shadows tend to be crisper around noon and less crisp around sunrise and sunset. This can again be achieved with a sine wave. As before you are going to want a min and a max value and convert them to amplitude and offset. In my example I will have a min of .6 and a max of 1, which will convert to an amplitude of .2 and an offset of .8. However we are going to want a different equation this time.
image
With this equation I have doubled the frequency and phase shifted it half a wavelength so that both 0:00 and 12:00 have the sharpest shadows, and it dips down to the minimum values at 6:00 and 18:00.

With this applied, the script now looks like this

--time changer
local mam --minutes after midnight
local timeShift = 2 --how many minutes you shift every "tick"
local waitTime = 1/30 --legnth of the tick
local pi = math.pi

--brightness
local amplitudeB = 1
local offsetB = 2
--outdoor ambieant

local var
local amplitudeO = 20
local offsetO = 100

--shadow softness
local amplitudeS = .2
local offsetS = .8

while true do
     --time changer
     mam = game.Lighting:GetMinutesAfterMidnight() + timeShift
     game.Lighting:SetMinutesAfterMidnight(mam)
     mam = mam/60

     --brightness
     game.Lighting.Brightness = amplitudeB*math.cos(mam*(pi/12)+pi)+offsetB

     --outdoor ambient
     var=amplitudeO*math.cos(mam*(pi/12)+pi)+offsetO
     game.Lighting.OutdoorAmbient = Color3.fromRGB(var,var,var)

     --shadow softness
     game.Lighting.ShadowSoftness = amplitudeS*math.cos(mam*(pi/6))+offsetS

     --tick
     wait(waitTime)
end

ColorShift_Top

One of the lesser utilized parameters in lighting, ColorShift_Top is none the less incredibly powerful. In effect, this allows you to tint the lighting, giving the light an orange glow in the morning or evening, and a cool blue light cast by the moon.

While one of the most visually striking of the features available, it is also the hardest to implement. While you can get fairly good results using sine waves, I have been unable to solve the issue where the light will take on a slightly green hue for a couple of hours every day. As such, the best solution I have come up with is to use an array that you lerp though.

I suggest starting by setting the time to 0:00 and configuring ColorShift_Top to your liking, recording the resulting RGB value. While possible to have finer controls, I am going to suggest one color3 value per hour. I find that the precision is good enough, and it makes the scripting a good deal easier, so that is what I am using in this tutorial.

Note: to limit any potential issues with colors blending weird, when shifting hues (say blue of night to orange of morning) its best to shift to a greyscale color first. Color Shift seems to have minimal effect shortly before sunrise and after sunset (6:00 and 18:00), so these are good times to hide this.

Once you have your values, you will want to put them in an array. While you probably can do it in just one array of color3 values, I have opted to have three arrays, one for each color channel.

                 -- 1   2   3   4   5   6   7   8   9   10  11  12  13  14  15  16  17  18  19  20  21  22  23  24
local rColorList = {000,000,000,000,000,255,255,255,255,255,255,255,255,255,255,255,255,255,255,000,000,000,000,000}
local gColorList = {165,165,165,165,165,255,215,230,255,255,255,255,255,255,255,245,230,215,255,165,165,165,165,165}
local bColorList = {255,255,255,255,255,255,110,135,255,255,255,255,255,255,255,215,135,110,255,255,255,255,255,255}

Once you have your array sorted, its time for the rest of the scripting. For readability sake, I have opted to write the leaping function for each color channel on there own line instead of combining them into a single function for easier reading. If you want to have it all on one line you can use this instead.

game.Lighting.ColorShift_Top=Color3.fromRGB(=((rColorList[pointer%24+1]-rColorList[pointer])*(mam-pointer+1))+rColorList[pointer], ((gColorList[pointer%24+1]-gColorList[pointer])*(mam-pointer+1))+gColorList[pointer], ((bColorList[pointer%24+1]-bColorList[pointer])*(mam-pointer+1))+bColorList[pointer])

but as that is rather messy and hard to read here is the completed code.

--time changer
local mam --minutes after midnight
local timeShift = 2 --how many minutes you shift every "tick"
local waitTime = 1/30 --legnth of the tick
local pi = math.pi

--brightness
local amplitudeB = 1
local offsetB = 2

--outdoor ambieant
local var
local amplitudeO = 20
local offsetO = 100

--shadow softness
local amplitudeS = .2
local offsetS = .8

--color shift top
local pointer
                 -- 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24
local rColorList = {000,000,000,000,000,255,255,255,255,255,255,255,255,255,255,255,255,255,255,000,000,000,000,000}
local gColorList = {165,165,165,165,165,255,215,230,255,255,255,255,255,255,255,245,230,215,255,165,165,165,165,165}
local bColorList = {255,255,255,255,255,255,110,135,255,255,255,255,255,255,255,215,135,110,255,255,255,255,255,255}
local r
local g
local b

while true do
     --time changer
     mam = game.Lighting:GetMinutesAfterMidnight() + timeShift
     game.Lighting:SetMinutesAfterMidnight(mam)
     mam = mam/60

     --brightness
     game.Lighting.Brightness = amplitudeB*math.cos(mam*(pi/12)+pi)+offsetB

     --outdoor ambient
     var=amplitudeO*math.cos(mam*(pi/12)+pi)+offsetO
     game.Lighting.OutdoorAmbient = Color3.fromRGB(var,var,var)

     --shadow softness
     game.Lighting.ShadowSoftness = amplitudeS*math.cos(mam*(pi/6))+offsetS

     --color shift top
     pointer= math.ceil(mam)
     r=((rColorList[pointer%24+1]-rColorList[pointer])*(mam-pointer+1))+rColorList[pointer]
     g=((gColorList[pointer%24+1]-gColorList[pointer])*(mam-pointer+1))+gColorList[pointer]
     b=((bColorList[pointer%24+1]-bColorList[pointer])*(mam-pointer+1))+bColorList[pointer]
     game.Lighting.ColorShift_Top=Color3.fromRGB(r,g,b)

     --tick
     wait(waitTime)
end

With this final step in place, your end result should end up looking something like this.

If you have any ideas on how to improve on this tutorial, be it workflow, quality, or readability, please leave a comment below. Criticisms and Critiques welcome.

38 Likes

I love that you have created this tutorial; it is very unique. The techniques you used are a significant improvement to the linear technique originally (where the darkness, shadows, etc. were linearly proportional to the time).

Just one thing though, I believe you meant to format this to code:

Minor errors aside, I find this tutorial very useful; I believe developers that seek to give their game some finishing touches will especially find this resourceful. I really loved how noticed even the tiniest details of the day-night cycle (such as the crispiness of shadows) in real life and you showed us how to implement that into Roblox. You know what they say: the littlest of the details may be the greatest (or something like that, at least).

2 Likes

Ah, really nice tutorial!

I was actually going to upload something similar to this myself (I might still do it) which is a script that also simulates dawn, twilight, and moonlight using tweens (configured to work for a more default day and night script). Nice tutorial!

Actually I was intended to just use inline highlighting, but seeing how that did not work, I have opted for your far more sensible suggestion of properly formatting it as a code block. I appreciate the feedback.

1 Like

I would be very interested in seeing how this effect would be achieved using the tween service. One advantage I think that this method would have over using tweens (Im not a lua scripter so correct me if I am wrong)is this doesn’t really care how the time is set, or what it is set to. If you want to manually set the time to a different time, or if you want time to go backwards (give it a negative time shift value), this script will continue to give you valid lighting for the chosen time.

A tutorial on sine waves would be great! Do you think you could provide more close up pictures of the equations you have on that website?

Would there be anything in particular you would like to see in a tutorial on sine waves? While incredibly powerful, the applications for them are rather niche, primarily being used in animation (though they are also good for accelerating and decelerating objects).

The forums image compression sure does a number on the readability of that text, so as requested

Brightness Graph:
image
Shadow Graph:
image
The site I use to show the graphs is https://www.desmos.com/calculator

I would really love just a general tutorial on using them. Doesn’t even matter the application. I was trying to script them earlier, and was confusing myself as to what “x” variable should be. Probably stupid, but I’ve never learned about sine waves so I’m very new to this, but I see this as helping in many ways for my current and future projects.

As it takes a few days for a tutorial to go though, here is a micro tutorial.
image
To calculate a sine wave you are going to need four things

Offset = (max-min)/2 +min

Amplitude = (max-min)/2

Wavelegnth = how long one cycle is

Phaseshift = where you want your wave to “start”. To phase shift a entire wave length is 2pi, half a wave length is pi, so on so forth.
image
Your then going to plug them into the following formula

1 Like

Thanks, nice little “cheat sheet” this is. Looking forward to full tutorial.

Actually once again, confused on what x, “input” would be? Let’s say I wanted to make a part bob up and down. What would X be?

Depends. Say you wanted a part to bob up and down once ever 10 seconds. Your wavelength would be 10, and your input would be how much time as passed in seconds.

In the context of the day and night cycle, X = MinutesAfterMidnight, or how far into the day one has progressed.

1 Like

Thanks! I will use this for a future project of mine.
By the way, how would I make it toggle on and off GlobalShadows? Toggling that will give my game a realistic look?

No clue why you would want to toggle global shadows on and off, global shadows is not something I would turn off unless your going for a more cartoon ascetic, and it is defiantly not something I would be toggling mid game. If you really wanted to though, I would probably use a if statement to check the time.

Have you seen global shadows + ClockTime : 1 + Brightness = 0 ?
It’s a really cool effect. Believe me

The script is nice, i needed this tipe of day/night cycles, but i found a bug: this error prints:
[Workspace.Day/Night Cycle:45: attempt to perform arithmetic on field ‘?’ (a nil value)]

It appears when 1 cycle is over

Turning global shadows also has the effect of disabling outdoor ambient. You will have identical effect by setting outdoor ambient to 0

I am going to guess that there is something wrong with your implementation then. Line 45 is basically identical to line 44 in the script I have.

Oh, thank you for this actually!