Skip to content

Instantly share code, notes, and snippets.

@paulhayes
Last active July 3, 2024 10:34
Show Gist options
  • Save paulhayes/54a7aa2ee3cccad4d37bb65977eb19e2 to your computer and use it in GitHub Desktop.
Save paulhayes/54a7aa2ee3cccad4d37bb65977eb19e2 to your computer and use it in GitHub Desktop.
Rotates a Unity directional light based on location and time. Includes time scale, and frame stepping for continuous use.
using System;
using System.Collections;
using System.Collections.Generic;
using UnityEngine;
namespace Entropedia
{
[RequireComponent(typeof(Light))]
[ExecuteInEditMode]
public class Sun : MonoBehaviour
{
[SerializeField]
float longitude;
[SerializeField]
float latitude;
[SerializeField]
[Range(0, 24)]
int hour;
[SerializeField]
[Range(0, 60)]
int minutes;
DateTime time;
Light light;
[SerializeField]
float timeSpeed = 1;
[SerializeField]
int frameSteps = 1;
int frameStep;
[SerializeField]
DateTime date;
public void SetTime(int hour, int minutes) {
this.hour = hour;
this.minutes = minutes;
OnValidate();
}
public void SetLocation(float longitude, float latitude){
this.longitude = longitude;
this.latitude = latitude;
}
public void SetDate(DateTime dateTime){
this.hour = dateTime.Hour;
this.minutes = dateTime.Minute;
this.date = dateTime.Date;
OnValidate();
}
public void SetUpdateSteps(int i) {
frameSteps = i;
}
public void SetTimeSpeed(float speed) {
timeSpeed = speed;
}
private void Awake()
{
light = GetComponent<Light>();
time = DateTime.Now;
hour = time.Hour;
minutes = time.Minute;
date = time.Date;
}
private void OnValidate()
{
time = date + new TimeSpan(hour, minutes, 0);
Debug.Log(time);
}
private void Update()
{
time = time.AddSeconds(timeSpeed * Time.deltaTime);
if (frameStep==0) {
SetPosition();
}
frameStep = (frameStep + 1) % frameSteps;
}
void SetPosition()
{
Vector3 angles = new Vector3();
double alt;
double azi;
SunPosition.CalculateSunPosition(time, (double)latitude, (double)longitude, out azi, out alt);
angles.x = (float)alt * Mathf.Rad2Deg;
angles.y = (float)azi * Mathf.Rad2Deg;
//UnityEngine.Debug.Log(angles);
transform.localRotation = Quaternion.Euler(angles);
light.intensity = Mathf.InverseLerp(-12, 0, angles.x);
}
}
/*
* The following source came from this blog:
* http://guideving.blogspot.co.uk/2010/08/sun-position-in-c.html
*/
public static class SunPosition
{
private const double Deg2Rad = Math.PI / 180.0;
private const double Rad2Deg = 180.0 / Math.PI;
/*!
* \brief Calculates the sun light.
*
* CalcSunPosition calculates the suns "position" based on a
* given date and time in local time, latitude and longitude
* expressed in decimal degrees. It is based on the method
* found here:
* http://www.astro.uio.no/~bgranslo/aares/calculate.html
* The calculation is only satisfiably correct for dates in
* the range March 1 1900 to February 28 2100.
* \param dateTime Time and date in local time.
* \param latitude Latitude expressed in decimal degrees.
* \param longitude Longitude expressed in decimal degrees.
*/
public static void CalculateSunPosition(
DateTime dateTime, double latitude, double longitude, out double outAzimuth, out double outAltitude)
{
// Convert to UTC
dateTime = dateTime.ToUniversalTime();
// Number of days from J2000.0.
double julianDate = 367 * dateTime.Year -
(int)((7.0 / 4.0) * (dateTime.Year +
(int)((dateTime.Month + 9.0) / 12.0))) +
(int)((275.0 * dateTime.Month) / 9.0) +
dateTime.Day - 730531.5;
double julianCenturies = julianDate / 36525.0;
// Sidereal Time
double siderealTimeHours = 6.6974 + 2400.0513 * julianCenturies;
double siderealTimeUT = siderealTimeHours +
(366.2422 / 365.2422) * (double)dateTime.TimeOfDay.TotalHours;
double siderealTime = siderealTimeUT * 15 + longitude;
// Refine to number of days (fractional) to specific time.
julianDate += (double)dateTime.TimeOfDay.TotalHours / 24.0;
julianCenturies = julianDate / 36525.0;
// Solar Coordinates
double meanLongitude = CorrectAngle(Deg2Rad *
(280.466 + 36000.77 * julianCenturies));
double meanAnomaly = CorrectAngle(Deg2Rad *
(357.529 + 35999.05 * julianCenturies));
double equationOfCenter = Deg2Rad * ((1.915 - 0.005 * julianCenturies) *
Math.Sin(meanAnomaly) + 0.02 * Math.Sin(2 * meanAnomaly));
double elipticalLongitude =
CorrectAngle(meanLongitude + equationOfCenter);
double obliquity = (23.439 - 0.013 * julianCenturies) * Deg2Rad;
// Right Ascension
double rightAscension = Math.Atan2(
Math.Cos(obliquity) * Math.Sin(elipticalLongitude),
Math.Cos(elipticalLongitude));
double declination = Math.Asin(
Math.Sin(rightAscension) * Math.Sin(obliquity));
// Horizontal Coordinates
double hourAngle = CorrectAngle(siderealTime * Deg2Rad) - rightAscension;
if (hourAngle > Math.PI)
{
hourAngle -= 2 * Math.PI;
}
double altitude = Math.Asin(Math.Sin(latitude * Deg2Rad) *
Math.Sin(declination) + Math.Cos(latitude * Deg2Rad) *
Math.Cos(declination) * Math.Cos(hourAngle));
// Nominator and denominator for calculating Azimuth
// angle. Needed to test which quadrant the angle is in.
double aziNom = -Math.Sin(hourAngle);
double aziDenom =
Math.Tan(declination) * Math.Cos(latitude * Deg2Rad) -
Math.Sin(latitude * Deg2Rad) * Math.Cos(hourAngle);
double azimuth = Math.Atan(aziNom / aziDenom);
if (aziDenom < 0) // In 2nd or 3rd quadrant
{
azimuth += Math.PI;
}
else if (aziNom < 0) // In 4th quadrant
{
azimuth += 2 * Math.PI;
}
outAltitude = altitude;
outAzimuth = azimuth;
}
/*!
* \brief Corrects an angle.
*
* \param angleInRadians An angle expressed in radians.
* \return An angle in the range 0 to 2*PI.
*/
private static double CorrectAngle(double angleInRadians)
{
if (angleInRadians < 0)
{
return 2 * Math.PI - (Math.Abs(angleInRadians) % (2 * Math.PI));
}
else if (angleInRadians > 2 * Math.PI)
{
return angleInRadians % (2 * Math.PI);
}
else
{
return angleInRadians;
}
}
}
}
using System;
using System.Collections;
using System.Collections.Generic;
using UnityEngine;
namespace Entropedia
{
public class SunSetDate : MonoBehaviour
{
public Sun sun;
public int year = 2020;
[Range(1, 12)]
public int month = 1;
[Range(0, 31)]
public int day = 1;
private void OnValidate()
{
try{
DateTime d = new DateTime(year,month,day,DateTime.Now.Hour,DateTime.Now.Minute,0);
Debug.Log(d);
if(sun) sun.SetDate(d);
}
catch(System.ArgumentOutOfRangeException e){
Debug.LogWarning("bad date");
}
}
}
}
using UnityEngine;
using System.Collections;
namespace Entropedia {
public class SunSetLocation : MonoBehaviour
{
[SerializeField]
Sun sun;
public void Start()
{
StartCoroutine(SetLocation());
}
public IEnumerator SetLocation()
{
if (!Input.location.isEnabledByUser){
Debug.LogWarning("location disabled by user");
yield break;
}
Input.location.Start();
while (Input.location.status == LocationServiceStatus.Initializing){
yield return new WaitForSeconds(0.5f);
}
if (Input.location.status == LocationServiceStatus.Failed){
Debug.LogWarning("Unable to determine device location");
yield break;
}
if(Input.location.status==LocationServiceStatus.Running){
var locInfo = Input.location.lastData;
Debug.LogFormat("long={0} lat={1}",locInfo.longitude,locInfo.latitude);
sun.SetLocation( locInfo.longitude, locInfo.latitude );
}
Input.location.Stop();
}
}
}
@Rairey233
Copy link

Rairey233 commented Jun 16, 2023

@paulhayes Hello , I'm not sure how active this thread is anymore, but I've been playing with the script and it doesn't seem to like me inputting my own date and time into the user input i.e it always resets it to the current date and time. Any ideas on how to fix this? I'm trying to essentially use these scripts as part of a simulation, so it isn't particularly useful to have it set to the current datetime. Maybe the order of the scripts is important?

@paulhayes
Copy link
Author

If you are setting your own date, you might want to forego the SetDate component and set the date on the sun directy by calling Sun.SetDate from your own script.

@paulhayes Hello , I'm not sure how active this thread is anymore, but I've been playing with the script and it doesn't seem to like me inputting my own date and time into the user input i.e it always resets it to the current date and time. Any ideas on how to fix this? I'm trying to essentially use these scripts as part of a simulation, so it isn't particularly useful to have it set to the current datetime. Maybe the order of the scripts is important?

@iyed-5
Copy link

iyed-5 commented Jul 11, 2023

great script thank you
but i think i have a problem which is daylight saving time or idk in the summer and in winter its normal hours,my longitude is 10.3
and i want all year +1

@philipbeadle
Copy link

Thanks Paul, got this working perfectly with CityGen3D. Now I'm going to use the rotation of the sun and do a raycast to see if a cloud is affecting the performance of a solar panel.

@INeatFreak
Copy link

This didn't worked for me, I've did everything right, gave the correct latitude, longitude but the results are completely inaccurate. Where I live, when looking towards the North at current date, the sun is suppose to come out from right side and go down on left, but the results show the complete opposite, the sunrise and sunset times are off too, it shows sunrise at 1 AM instead of 5 AM with the UTC offset applied for my timezone.

I've tested it with the tool @JWongRBG provided and in there it works perfectly with the same coordinates and date. If you plan on fixing this you can use that tool as working reference.

@paulhayes
Copy link
Author

Hi @INeatFreak, sorry it's not working for you. I'm honestly not sure why it would be so far off, it sounds like converting your time to UTC might be part of the problem. I don't have time to inviestigate, as I'm always spread thin across several projects. If you have any changes you'd like to make please can you fork the gist and make a pull request if you think it solves a general issue you discover.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment