using UnityEngine; using UnityEngine.Profiling; using System; namespace OccaSoftware.Altos { public static class StaticTimeOfDayManager { public static Material activeSkyboxMaterial; public static Action UpdateSunIntensityEvent; public static Action UpdateTimeOfDayEvent; public static void OnSunLightIntensityChangeEvent(float sunBrightnessRelative) { if(UpdateSunIntensityEvent != null) UpdateSunIntensityEvent(sunBrightnessRelative); } public static void OnTimeOfDayChange(float newTimeOfDay) { if (UpdateTimeOfDayEvent != null) UpdateTimeOfDayEvent(newTimeOfDay); } public static float SetLightIntensity(Light light, float peakIntensity, float horizonAngle) { float lightAngle = light.transform.eulerAngles.x; lightAngle = lightAngle > 180f ? lightAngle - 360f : lightAngle; if (lightAngle >= horizonAngle) { return peakIntensity; } if (lightAngle < -horizonAngle) { return 0f; } if (lightAngle >= -horizonAngle && lightAngle < horizonAngle) { float t = Helpers.Remap(lightAngle, -horizonAngle, horizonAngle, 0f, 1f); return Mathf.Lerp(0f, peakIntensity, t); } return light.intensity; } } [ExecuteAlways] public class TimeOfDayManager : MonoBehaviour { public SkyboxDefinitionScriptableObject skyboxDefinition = null; private Material skyboxMaterial = null; public Light sun = null; private int periodIndex = 0; private int nextPeriodIndex = 0; private float timeHours = 0f; private Color fogColor = Color.white; private Color zenithAmbient = Color.white; private bool setupCorrect = false; private float cachedTimeOfDay = 0f; public float SunLightIntensityRelative { get { if (sun == null || skyboxDefinition == null) return 0; return Helpers.Remap(sun.intensity, 0f, skyboxDefinition.sunLightIntensity, 0f, 1f); } } public void Awake() { InitializeSkybox(); } public void InitializeSkybox() { //Debug.Log("Initializing Skybox..."); // Error Handling setupCorrect = ValidateSetup(); } private bool ValidateSetup() { #if !UNITY_2021_3_OR_NEWER Debug.LogError("This version of Altos is designed for Unity 2021.3 or newer. Please upgrade your Unity Editor to ensure that Altos is compatible with your Unity version."); #endif if (skyboxDefinition == null) { Debug.Log("Must set Skybox Definition"); return false; } if (skyboxMaterial == null) { //Debug.Log("Setting up Skybox Material..."); skyboxMaterial = GetSkyboxMaterial(); StaticTimeOfDayManager.activeSkyboxMaterial = skyboxMaterial; if (skyboxMaterial == null) { Debug.Log("Failed to set up Skybox Material. Exiting."); return false; } } if (RenderSettings.skybox != skyboxMaterial) { //Debug.Log("Setting up Skybox in Lighting settings..."); SetLightingEnvironment(skyboxMaterial); if (RenderSettings.skybox != skyboxMaterial) { Debug.Log("Failed to set up Skybox in Lighting settings. Exiting."); return false; } } if (sun == null) { Debug.Log("Sun Lamp is not set. Exiting."); return false; } RenderSettings.sun = sun; return true; } private void OnDestroy() { if (RenderSettings.skybox = skyboxMaterial) RenderSettings.skybox = null; } void Start() { if (setupCorrect) { //Debug.Log("Skybox Initialized Successfully."); InitialSetup(); } } public void InitialSetup() { if (setupCorrect) { skyboxDefinition.activeTimeOfDay = skyboxDefinition.timeOfDay; cachedTimeOfDay = skyboxDefinition.activeTimeOfDay; HandleSunLight(); SetPeriod(); SetSkyColors(); UpdateActiveShaderProperties(); UpdateStaticShaderProperties(); } } private Material GetSkyboxMaterial() { return new Material(Shader.Find("Shader Graphs/Skybox Shader_OS")); } private void SetLightingEnvironment(Material skybox) { RenderSettings.skybox = skybox; } public void SetSkyboxDefinition(SkyboxDefinitionScriptableObject skyboxDefinition) { this.skyboxDefinition = skyboxDefinition; } public void SetSunLamp(Light sun) { this.sun = sun; } public void UpdateStaticShaderProperties() { skyboxMaterial.SetFloat(ShaderParams.sunSize, skyboxDefinition.sunSize); skyboxMaterial.SetColor(ShaderParams.sunColor, skyboxDefinition.sunColor); skyboxMaterial.SetFloat(ShaderParams.sunInfluenceSize, skyboxDefinition.sunInfluenceSize); skyboxMaterial.SetFloat(ShaderParams.sunInfluenceIntensity, skyboxDefinition.sunInfluenceIntensity); skyboxMaterial.SetTexture(ShaderParams.cloudTex1, skyboxDefinition.cloudTexture1); skyboxMaterial.SetTexture(ShaderParams.cloudTex2, skyboxDefinition.cloudTexture2); skyboxMaterial.SetVector(ShaderParams.cloudTex1ZenithTiling, skyboxDefinition.texture1ZenithTiling); skyboxMaterial.SetVector(ShaderParams.cloudTex2ZenithTiling, skyboxDefinition.texture2ZenithTiling); skyboxMaterial.SetVector(ShaderParams.cloudTex1HorizonTiling, skyboxDefinition.texture1HorizonTilingSterile); skyboxMaterial.SetVector(ShaderParams.cloudTex2HorizonTiling, skyboxDefinition.texture2HorizonTilingSterile); skyboxMaterial.SetFloat(ShaderParams.cloudSharpness, skyboxDefinition.cloudSharpness); skyboxMaterial.SetColor(ShaderParams.cloudColor, skyboxDefinition.cloudColor); skyboxMaterial.SetFloat(ShaderParams.cloudOpacity, skyboxDefinition.cloudOpacity); skyboxMaterial.SetColor(ShaderParams.cloudShadingColor, skyboxDefinition.cloudShadingColor); skyboxMaterial.SetFloat(ShaderParams.cloudShadingThreshold, skyboxDefinition.cloudShadingThreshold); skyboxMaterial.SetFloat(ShaderParams.cloudShadingSharpness, skyboxDefinition.cloudShadingSharpness); skyboxMaterial.SetFloat(ShaderParams.cloudShadingStrength, skyboxDefinition.cloudShadingStrength); skyboxMaterial.SetFloat(ShaderParams.cloudSpeed, skyboxDefinition.cloudSpeed); skyboxMaterial.SetFloat(ShaderParams.cloudThreshold, 1f - skyboxDefinition.cloudiness); skyboxMaterial.SetFloat(ShaderParams.alternateUVAtZenith, skyboxDefinition.alternateUVAtZenith); skyboxMaterial.SetFloat(ShaderParams.sunInfluence, skyboxDefinition.sunCloudInfluence); skyboxMaterial.SetFloat(ShaderParams.skyColorInfluence, skyboxDefinition.skyColorCloudInfluence); skyboxMaterial.SetFloat(ShaderParams.blueNoiseStrength, skyboxDefinition.ditherStrength); skyboxMaterial.SetFloat(ShaderParams.fogHeightPower, skyboxDefinition.fogHeightPower); Shader.SetGlobalFloat(ShaderParams.depthFogStart, skyboxDefinition.fogStart); Shader.SetGlobalFloat(ShaderParams.depthFogEnd, skyboxDefinition.fogEnd); Shader.SetGlobalFloat(ShaderParams.depthFogDithering, skyboxDefinition.fogDithering); } private static class ShaderParams { public static int sunDirection = Shader.PropertyToID("_SUN_DIRECTION"); public static int horizonColor = Shader.PropertyToID("_HORIZONCOLOR"); public static int zenithColor = Shader.PropertyToID("_ZENITHCOLOR"); public static int groundColor = Shader.PropertyToID("_GROUNDCOLOR"); public static int cloudThreshold = Shader.PropertyToID("_CLOUDTHRESHOLD"); public static int cloudSpeed = Shader.PropertyToID("_CLOUDTEXTURESPEED"); public static int nightLuminance = Shader.PropertyToID("_CLOUD_NIGHT_LUMINANCE_MULTIPLIER"); public static int fogColor = Shader.PropertyToID("_FOG_COLOR"); public static int volumetricsFogColor = Shader.PropertyToID("_VOLUMETRICS_FOG_COLOR"); public static int zenithAmbientColor = Shader.PropertyToID("_ZENITH_AMBIENT_COLOR"); public static int depthFogColor = Shader.PropertyToID("_DEPTH_FOG_COLOR"); public static int depthFogStart = Shader.PropertyToID("_DEPTH_FOG_START"); public static int depthFogEnd = Shader.PropertyToID("_DEPTH_FOG_END"); public static int depthFogDithering = Shader.PropertyToID("_DEPTH_FOG_DITHERING"); public static int sunSize = Shader.PropertyToID("_SUNSIZE"); public static int sunColor = Shader.PropertyToID("_SUNCOLOR"); public static int sunInfluenceSize = Shader.PropertyToID("_SUNINFLUENCESIZE"); public static int sunInfluenceIntensity = Shader.PropertyToID("_SUNINFLUENCEINTENSITY"); public static int sunIntensity = Shader.PropertyToID("_SUN_INTENSITY"); public static int cloudTex1 = Shader.PropertyToID("_CLOUDTEXTURE1"); public static int cloudTex2 = Shader.PropertyToID("_CLOUDTEXTURE2"); public static int cloudTex1ZenithTiling = Shader.PropertyToID("_TEXTURE1ZENITHTILING"); public static int cloudTex2ZenithTiling = Shader.PropertyToID("_TEXTURE2ZENITHTILING"); public static int cloudTex1HorizonTiling = Shader.PropertyToID("_TEXTURE1HORIZONTILING"); public static int cloudTex2HorizonTiling = Shader.PropertyToID("_TEXTURE2HORIZONTILING"); public static int cloudSharpness = Shader.PropertyToID("_CLOUDSHARPNESS"); public static int cloudColor = Shader.PropertyToID("_CLOUDCOLOR"); public static int cloudOpacity = Shader.PropertyToID("_CLOUDOPACITY"); public static int cloudShadingColor = Shader.PropertyToID("_CLOUDSHADINGCOLOR"); public static int cloudShadingThreshold = Shader.PropertyToID("_CLOUDSHADINGTHRESHOLD"); public static int cloudShadingSharpness = Shader.PropertyToID("_CLOUDSHADINGSHARPNESS"); public static int cloudShadingStrength = Shader.PropertyToID("_CLOUDSHADINGSTRENGTH"); public static int alternateUVAtZenith = Shader.PropertyToID("_ALTERNATEUVATZENITH"); public static int sunInfluence = Shader.PropertyToID("_SUNINFLUENCE"); public static int skyColorInfluence = Shader.PropertyToID("_SKYCOLORINFLUENCE"); public static int blueNoiseStrength = Shader.PropertyToID("_BLUENOISESTRENGTH"); public static int fogHeightPower = Shader.PropertyToID("_FOG_HEIGHT_POWER"); } public void UpdateActiveShaderProperties() { skyboxMaterial.SetColor(ShaderParams.fogColor, fogColor); Shader.SetGlobalColor(ShaderParams.volumetricsFogColor, fogColor); Shader.SetGlobalColor(ShaderParams.zenithAmbientColor, zenithAmbient); Shader.SetGlobalColor(ShaderParams.depthFogColor, fogColor); if(sun != null) Shader.SetGlobalVector(ShaderParams.sunDirection, sun.transform.forward); } // Update is called once per frame public void Update() { Profiler.BeginSample("TimeOfDayManager: Update Execution"); if (!setupCorrect) return; if (skyboxDefinition == null) return; if (skyboxMaterial == null) { skyboxMaterial = GetSkyboxMaterial(); SetLightingEnvironment(skyboxMaterial); } if (Application.isPlaying) { SetTimeOfDay(); } else { HandleSunLight(); SetPeriod(); SetSkyColors(); } if(Mathf.Abs(skyboxDefinition.activeTimeOfDay - cachedTimeOfDay) > 0.01f) { cachedTimeOfDay = skyboxDefinition.activeTimeOfDay; HandleSunLight(); SetPeriod(); SetSkyColors(); } UpdateActiveShaderProperties(); UpdateStaticShaderProperties(); Profiler.EndSample(); } private void SetTimeOfDay() { if (skyboxDefinition.activeTimeOfDay > 24f) { skyboxDefinition.activeTimeOfDay = skyboxDefinition.activeTimeOfDay - 24f; } if(skyboxDefinition.realSecondsToGameHours > 0f) { float t = Time.deltaTime * skyboxDefinition.realSecondsToGameHours; // One second in real life is one hour in-game. This can be modified with the realSecondsToGameHours variable (e.g., if realSecondsToGameHours is set to 2, then 1 second in real life corresponds to 2 hours in-game). skyboxDefinition.activeTimeOfDay += t; timeHours += t; // Tracking timehours separately for sampling perlin noise (we don't reset it at 24). } } void HandleSunLight() { if (sun == null) return; SetSunLightDirection(); float oldIntensity = sun.intensity; sun.intensity = StaticTimeOfDayManager.SetLightIntensity(sun, skyboxDefinition.sunLightIntensity, 20f); if(sun.intensity != oldIntensity) { StaticTimeOfDayManager.OnSunLightIntensityChangeEvent(sun.intensity / skyboxDefinition.sunLightIntensity); } float cloudLuminance = Helpers.Remap(sun.intensity, 0f, skyboxDefinition.sunLightIntensity, skyboxDefinition.cloudNightLuminanceMultiplier, 1f); skyboxMaterial.SetFloat(ShaderParams.nightLuminance, cloudLuminance); skyboxMaterial.SetFloat(ShaderParams.sunIntensity, SunLightIntensityRelative); } // Basically, the light direction is derived from the current time of day such that the sun is perfectly horizontal at 6am and 6pm for a 12hr "day" cycle and 12hr "night" cycle. // Not currently configurable. private void SetSunLightDirection() { if (sun == null) return; float remappedTimeOfDay = skyboxDefinition.activeTimeOfDay / 24f; float sunAngle = (remappedTimeOfDay * 360f) - 90f; sun.transform.rotation = Quaternion.Euler(sunAngle, skyboxDefinition.sunBaseRotationY, 0f); // Sun rotation is based on time of day } // The SetSkyColors function depends on this to set the correct period. // Normally, this is straightforward. Transitioning from one day to the next is the tricky bit. private void SetPeriod() { if (Application.isPlaying) { HandlePeriod(skyboxDefinition.activeTimeOfDay); } else { HandlePeriod(skyboxDefinition.timeOfDay); } nextPeriodIndex = periodIndex + 1; if (nextPeriodIndex >= skyboxDefinition.periodsOfDay.Count) { nextPeriodIndex = 0; } } void HandlePeriod(float time) { if (time < skyboxDefinition.periodsOfDay[0].startTime) { periodIndex = skyboxDefinition.periodsOfDay.Count - 1; } else { for (int i = 0; i < skyboxDefinition.periodsOfDay.Count; i++) { if (time >= skyboxDefinition.periodsOfDay[i].startTime) { periodIndex = i; } } } } // Sets the Horizon and Zenith colors based on the current time of day. // As we approach the next time of day, we transition smoothly to the Horizon and Zenith colors for that time of day. private void SetSkyColors() { if (Application.isPlaying) { HandleSkyColor(skyboxDefinition.activeTimeOfDay); } else { HandleSkyColor(skyboxDefinition.timeOfDay); } } private void HandleSkyColor(float time) { if (time >= skyboxDefinition.periodsOfDay[nextPeriodIndex].startTime - skyboxDefinition.hoursForTransition && time < skyboxDefinition.periodsOfDay[nextPeriodIndex].startTime) { float t = Helpers.Remap(time, skyboxDefinition.periodsOfDay[nextPeriodIndex].startTime - skyboxDefinition.hoursForTransition, skyboxDefinition.periodsOfDay[nextPeriodIndex].startTime, 0, 1); Color tempHorizon = Color.Lerp(skyboxDefinition.periodsOfDay[periodIndex].horizonColor, skyboxDefinition.periodsOfDay[nextPeriodIndex].horizonColor, t); skyboxMaterial.SetColor(ShaderParams.horizonColor, tempHorizon); Color tempZenith = Color.Lerp(skyboxDefinition.periodsOfDay[periodIndex].zenithColor, skyboxDefinition.periodsOfDay[nextPeriodIndex].zenithColor, t); skyboxMaterial.SetColor(ShaderParams.zenithColor, tempZenith); Color tempGround = Color.Lerp(skyboxDefinition.periodsOfDay[periodIndex].groundColor, skyboxDefinition.periodsOfDay[nextPeriodIndex].groundColor, t); skyboxMaterial.SetColor(ShaderParams.groundColor, tempGround); fogColor = tempHorizon; zenithAmbient = Color.Lerp(tempHorizon, tempZenith, 0.5f); } else { skyboxMaterial.SetColor(ShaderParams.horizonColor, skyboxDefinition.periodsOfDay[periodIndex].horizonColor); skyboxMaterial.SetColor(ShaderParams.zenithColor, skyboxDefinition.periodsOfDay[periodIndex].zenithColor); skyboxMaterial.SetColor(ShaderParams.groundColor, skyboxDefinition.periodsOfDay[periodIndex].groundColor); fogColor = skyboxDefinition.periodsOfDay[periodIndex].horizonColor; zenithAmbient = Color.Lerp(skyboxDefinition.periodsOfDay[periodIndex].horizonColor, skyboxDefinition.periodsOfDay[periodIndex].zenithColor, 0.5f); } fogColor = Color.Lerp(skyboxDefinition.baseFogColor, fogColor, skyboxDefinition.fogColorBlend); } } // Internal class used to include all PeriodOfDay information. // I plan to replace this later with ScriptableObjects for ease of use. [System.Serializable] public class PeriodOfDay { [SerializeField] [Tooltip("(Optional) Descriptive Name")] public string description; [SerializeField, Range(0f, 24f)] [Tooltip("Set the Start Time for this Period of Day")] public float startTime; [SerializeField, ColorUsage(false, true)] [Tooltip("Set the Horizon Color for this Period of Day")] public Color horizonColor; [SerializeField, ColorUsage(false, true)] [Tooltip("Set the Zenith Color for this Period of Day")] public Color zenithColor; [SerializeField, ColorUsage(false, true)] [Tooltip("Set the Ground Color for this Period of Day")] public Color groundColor; public PeriodOfDay(string desc, float start, Color horizon, Color zenith, Color ground) { description = desc; startTime = start; horizonColor = horizon; zenithColor = zenith; groundColor = ground; } } }