78bfdf2828
# Conflicts: # Assets/External/Animated PBR Chest Demo/Materials/WoodChest.mat # Packages/com.distantlands.cozy.core/Content/Integration/Import for BiRP.unitypackage.meta # Packages/com.distantlands.cozy.core/Content/Integration/Import for HDRP.unitypackage.meta # Packages/com.distantlands.cozy.core/Content/Integration/Import for URP.unitypackage.meta
421 lines
18 KiB
C#
421 lines
18 KiB
C#
using UnityEngine;
|
|
using UnityEngine.Rendering;
|
|
|
|
namespace Ashwild.Environment
|
|
{
|
|
/// <summary>
|
|
/// Drives the day/night cycle for the self-contained "GAME/StylizedSky" skybox. Rotates
|
|
/// the sun Directional Light and evaluates one palette per moment of day (sky gradient,
|
|
/// sun/moon color, cloud colors, star opacity) straight onto the sky MATERIAL's
|
|
/// properties — the shader stays fully self-contained, so with no manager the material
|
|
/// still renders its own authored look and this component only overrides the
|
|
/// time-dependent values on top. Runs in edit mode (ExecuteAlways) so the sky updates
|
|
/// live while scrubbing the time slider.
|
|
///
|
|
/// Purely visual and LOCAL for now: like SeasonManager it is not yet networked. The
|
|
/// single seam to sync in co-op is the time value itself — feed it through
|
|
/// <see cref="SetTimeOfDay"/> from a server world-clock (SyncVar) instead of the local
|
|
/// auto-advance, and every client's sky and lighting follows (see CLAUDE.md §3).
|
|
/// </summary>
|
|
[ExecuteAlways]
|
|
[DisallowMultipleComponent]
|
|
[AddComponentMenu("GAME/Environment/Time Of Day Manager")]
|
|
public class TimeOfDayManager : MonoBehaviour
|
|
{
|
|
#region Singleton
|
|
|
|
public static TimeOfDayManager Instance { get; private set; }
|
|
|
|
#endregion
|
|
|
|
#region Serialized Fields
|
|
|
|
[Header("References")]
|
|
[Tooltip("Directional light rotated to act as the sun. Optional but recommended.")]
|
|
[SerializeField] private Light sunLight;
|
|
[Tooltip("Second directional light acting as the moon: cool, dim, lit only at night, aimed opposite the sun. Optional.")]
|
|
[SerializeField] private Light moonLight;
|
|
[Tooltip("Material using GAME/StylizedSky. Assigned to RenderSettings.skybox on enable.")]
|
|
[SerializeField] private Material skyMaterial;
|
|
|
|
[Header("Time")]
|
|
[Tooltip("0 = midnight, 0.25 = sunrise, 0.5 = noon, 0.75 = sunset.")]
|
|
[Range(0f, 1f)] [SerializeField] private float timeOfDay = 0.5f;
|
|
[SerializeField] private bool autoAdvance = true;
|
|
[Tooltip("Real seconds for one full day/night cycle. Only advances in Play.")]
|
|
[SerializeField] private float dayLengthSeconds = 300f;
|
|
[Tooltip("Compass offset of the sun's arc, in degrees.")]
|
|
[Range(0f, 360f)] [SerializeField] private float sunYaw = 20f;
|
|
|
|
[Header("Sun Light")]
|
|
[SerializeField] private Gradient sunColorOverDay = new Gradient();
|
|
[SerializeField] private AnimationCurve sunIntensityOverDay = AnimationCurve.Constant(0f, 1f, 1f);
|
|
[Tooltip("Peak directional-light intensity reached around noon.")]
|
|
[SerializeField] private float maxSunIntensity = 1.3f;
|
|
|
|
[Header("Sky Gradient Over Day")]
|
|
[SerializeField] private Gradient zenithColorOverDay = new Gradient();
|
|
[SerializeField] private Gradient horizonColorOverDay = new Gradient();
|
|
[SerializeField] private Gradient groundColorOverDay = new Gradient();
|
|
|
|
[Header("Clouds Over Day")]
|
|
[SerializeField] private Gradient cloudColorOverDay = new Gradient();
|
|
[SerializeField] private Gradient cloudShadowColorOverDay = new Gradient();
|
|
|
|
[Header("Moon and Stars")]
|
|
[SerializeField] private Color moonColor = new Color(0.85f, 0.88f, 0.96f);
|
|
[Tooltip("Cool tint of the moonlight cast on the scene at night.")]
|
|
[SerializeField] private Color moonLightColor = new Color(0.55f, 0.62f, 0.85f);
|
|
[Tooltip("Peak moonlight intensity reached in the dead of night.")]
|
|
[SerializeField] private float maxMoonIntensity = 0.3f;
|
|
[Tooltip("Sun height at which stars/moon start fading in (upper) and are fully visible (lower).")]
|
|
[SerializeField] private float starFadeInElevation = 0.08f;
|
|
[SerializeField] private float starFullElevation = -0.15f;
|
|
|
|
[Header("Fog")]
|
|
[Tooltip("Tint the built-in RenderSettings fog with the horizon color. Leave off when an external fog (e.g. Better Fog) is driven from CurrentHorizonColor instead.")]
|
|
[SerializeField] private bool driveFogColor = true;
|
|
|
|
#endregion
|
|
|
|
#region Shader Property IDs
|
|
|
|
private static readonly int ZenithColorId = Shader.PropertyToID("_ZenithColor");
|
|
private static readonly int HorizonColorId = Shader.PropertyToID("_HorizonColor");
|
|
private static readonly int GroundColorId = Shader.PropertyToID("_GroundColor");
|
|
private static readonly int SunDirectionId = Shader.PropertyToID("_SunDirection");
|
|
private static readonly int SunColorId = Shader.PropertyToID("_SunColor");
|
|
private static readonly int MoonDirectionId = Shader.PropertyToID("_MoonDirection");
|
|
private static readonly int MoonColorId = Shader.PropertyToID("_MoonColor");
|
|
private static readonly int CloudColorId = Shader.PropertyToID("_CloudColor");
|
|
private static readonly int CloudShadowColorId = Shader.PropertyToID("_CloudShadowColor");
|
|
private static readonly int StarOpacityId = Shader.PropertyToID("_StarOpacity");
|
|
|
|
#endregion
|
|
|
|
#region State
|
|
|
|
private float lastAmbientRefresh;
|
|
|
|
#endregion
|
|
|
|
#region Unity Lifecycle
|
|
|
|
/// <summary>
|
|
/// Registers the instance, makes the sky material active and pushes an initial state
|
|
/// so the sky is correct the moment the component becomes active (including in edit).
|
|
/// </summary>
|
|
private void OnEnable()
|
|
{
|
|
Instance = this;
|
|
if (skyMaterial != null)
|
|
RenderSettings.skybox = skyMaterial;
|
|
RenderSettings.ambientMode = AmbientMode.Skybox;
|
|
RenderSettings.defaultReflectionMode = DefaultReflectionMode.Skybox;
|
|
Apply();
|
|
}
|
|
|
|
/// <summary>
|
|
/// Clears the shared instance so a disabled manager never keeps answering as current.
|
|
/// </summary>
|
|
private void OnDisable()
|
|
{
|
|
if (Instance == this)
|
|
Instance = null;
|
|
}
|
|
|
|
/// <summary>
|
|
/// Re-clamps tuning and refreshes the sky whenever a value is edited in the inspector.
|
|
/// </summary>
|
|
private void OnValidate()
|
|
{
|
|
dayLengthSeconds = Mathf.Max(1f, dayLengthSeconds);
|
|
Apply();
|
|
}
|
|
|
|
/// <summary>
|
|
/// Advances time (Play only) and re-applies the palette every frame.
|
|
/// </summary>
|
|
private void Update()
|
|
{
|
|
if (Application.isPlaying && autoAdvance)
|
|
timeOfDay = Mathf.Repeat(timeOfDay + Time.deltaTime / dayLengthSeconds, 1f);
|
|
|
|
Apply();
|
|
}
|
|
|
|
#endregion
|
|
|
|
#region Apply
|
|
|
|
/// <summary>
|
|
/// Resolves the current time of day into sun orientation and the full sky palette,
|
|
/// writes it onto the sky material, and mirrors the sun color/intensity onto the
|
|
/// directional light so scene lighting matches the sky. Single source of every
|
|
/// time-dependent value. No-op without a material to drive.
|
|
/// </summary>
|
|
private void Apply()
|
|
{
|
|
Vector3 sunDir = OrientSun();
|
|
float nightFactor = Mathf.SmoothStep(0f, 1f,
|
|
Mathf.InverseLerp(starFadeInElevation, starFullElevation, sunDir.y));
|
|
Color sunColor = sunColorOverDay.Evaluate(timeOfDay);
|
|
DriveSunLight(sunColor);
|
|
DriveMoonLight(sunDir, nightFactor);
|
|
PromoteMainLight(nightFactor);
|
|
|
|
Color horizon = horizonColorOverDay.Evaluate(timeOfDay);
|
|
CurrentHorizonColor = horizon;
|
|
CurrentSunColor = sunColor;
|
|
CurrentNightFactor = nightFactor;
|
|
if (driveFogColor)
|
|
RenderSettings.fogColor = ToRendered(horizon);
|
|
|
|
if (skyMaterial == null)
|
|
{
|
|
RepaintEditor();
|
|
return;
|
|
}
|
|
|
|
skyMaterial.SetColor(ZenithColorId, ToRendered(zenithColorOverDay.Evaluate(timeOfDay)));
|
|
skyMaterial.SetColor(HorizonColorId, ToRendered(horizon));
|
|
skyMaterial.SetColor(GroundColorId, ToRendered(groundColorOverDay.Evaluate(timeOfDay)));
|
|
|
|
skyMaterial.SetVector(SunDirectionId, sunDir);
|
|
skyMaterial.SetColor(SunColorId, ToRendered(sunColorOverDay.Evaluate(timeOfDay)));
|
|
skyMaterial.SetVector(MoonDirectionId, -sunDir);
|
|
skyMaterial.SetColor(MoonColorId, ToRendered(moonColor));
|
|
|
|
skyMaterial.SetColor(CloudColorId, ToRendered(cloudColorOverDay.Evaluate(timeOfDay)));
|
|
skyMaterial.SetColor(CloudShadowColorId, ToRendered(cloudShadowColorOverDay.Evaluate(timeOfDay)));
|
|
|
|
skyMaterial.SetFloat(StarOpacityId, nightFactor);
|
|
|
|
RefreshAmbient();
|
|
RepaintEditor();
|
|
}
|
|
|
|
/// <summary>
|
|
/// Recomputes ambient light and the skybox reflection from the current (procedural)
|
|
/// sky so scene lighting tracks the day — Unity does NOT do this automatically for a
|
|
/// material that changes every frame. Throttled to a few times a second because
|
|
/// rebaking the environment probe every frame would stutter the editor.
|
|
/// </summary>
|
|
private void RefreshAmbient()
|
|
{
|
|
float now = Time.realtimeSinceStartup;
|
|
if (now - lastAmbientRefresh < 0.15f) return;
|
|
lastAmbientRefresh = now;
|
|
DynamicGI.UpdateEnvironment();
|
|
}
|
|
|
|
/// <summary>
|
|
/// Rotates the sun light so noon sits overhead and sunrise/sunset sit on the horizon,
|
|
/// and returns the world-space direction pointing toward the sun (what the shader
|
|
/// wants). Works from the current time even when no light is assigned.
|
|
/// </summary>
|
|
private Vector3 OrientSun()
|
|
{
|
|
float pitch = timeOfDay * 360f - 90f;
|
|
Quaternion rotation = Quaternion.Euler(pitch, sunYaw, 0f);
|
|
if (sunLight != null)
|
|
sunLight.transform.rotation = rotation;
|
|
return -(rotation * Vector3.forward);
|
|
}
|
|
|
|
/// <summary>
|
|
/// Tints the directional light and fades its intensity to zero at night so the world
|
|
/// darkens together with the sky. No-op when no light is wired up.
|
|
/// </summary>
|
|
private void DriveSunLight(Color sunColor)
|
|
{
|
|
if (sunLight == null) return;
|
|
sunLight.color = sunColor;
|
|
sunLight.intensity = sunIntensityOverDay.Evaluate(timeOfDay) * maxSunIntensity;
|
|
}
|
|
|
|
/// <summary>
|
|
/// Aims the moon light opposite the sun (its light travels toward the sun direction, i.e.
|
|
/// comes from the moon), tints it cool, and fades its intensity in with nightFactor so it
|
|
/// only lights the scene once the sun is down. No-op when no moon light is wired up.
|
|
/// </summary>
|
|
private void DriveMoonLight(Vector3 sunDir, float nightFactor)
|
|
{
|
|
if (moonLight == null) return;
|
|
moonLight.transform.rotation = Quaternion.LookRotation(sunDir);
|
|
moonLight.color = moonLightColor;
|
|
moonLight.intensity = nightFactor * maxMoonIntensity;
|
|
}
|
|
|
|
/// <summary>
|
|
/// Points URP's main (shadow-casting) light at whichever body is currently up — sun by
|
|
/// day, moon by night. Without this, a pinned Sun Source keeps the dark night sun as the
|
|
/// main light and shadows vanish once it sets; swapping to the moon keeps shadows going.
|
|
/// </summary>
|
|
private void PromoteMainLight(float nightFactor)
|
|
{
|
|
Light mainLight = nightFactor > 0.5f && moonLight != null ? moonLight : sunLight;
|
|
if (mainLight != null)
|
|
RenderSettings.sun = mainLight;
|
|
}
|
|
|
|
/// <summary>
|
|
/// Converts an authored sRGB color to the value the shader must receive. Colors
|
|
/// pushed via Material.SetColor are NOT auto-gamma-corrected (unlike the inspector
|
|
/// color field), so in a linear project they must be pre-converted or the whole sky
|
|
/// washes out to white.
|
|
/// </summary>
|
|
private static Color ToRendered(Color c)
|
|
{
|
|
return QualitySettings.activeColorSpace == ColorSpace.Linear ? c.linear : c;
|
|
}
|
|
|
|
/// <summary>
|
|
/// Forces the Scene view to redraw while scrubbing values outside Play, matching
|
|
/// SeasonManager, so the sky updates live without entering Play mode.
|
|
/// </summary>
|
|
private void RepaintEditor()
|
|
{
|
|
#if UNITY_EDITOR
|
|
if (!Application.isPlaying)
|
|
UnityEditor.SceneView.RepaintAll();
|
|
#endif
|
|
}
|
|
|
|
#endregion
|
|
|
|
#region Public API
|
|
|
|
/// <summary>
|
|
/// Current normalized time of day (0 = midnight, 0.5 = noon).
|
|
/// </summary>
|
|
public float TimeOfDay => timeOfDay;
|
|
|
|
/// <summary>
|
|
/// The horizon color resolved for the current time (authored/gamma space). External
|
|
/// atmosphere systems (e.g. the Better Fog driver) read this to tint fog with the sky.
|
|
/// </summary>
|
|
public Color CurrentHorizonColor { get; private set; }
|
|
|
|
/// <summary>
|
|
/// The sun color resolved for the current time (authored/gamma space) — for fog sun
|
|
/// scattering or any effect that should match the sun tint.
|
|
/// </summary>
|
|
public Color CurrentSunColor { get; private set; }
|
|
|
|
/// <summary>
|
|
/// 0 by day, 1 in the dead of night (same curve that fades in the stars and the moon).
|
|
/// </summary>
|
|
public float CurrentNightFactor { get; private set; }
|
|
|
|
/// <summary>
|
|
/// Sets the time of day directly (wrapped into [0,1)) and refreshes the sky. This is
|
|
/// the seam a networked world-clock drives from — call it on every client with the
|
|
/// server time instead of relying on the local auto-advance.
|
|
/// </summary>
|
|
public void SetTimeOfDay(float normalized)
|
|
{
|
|
timeOfDay = Mathf.Repeat(normalized, 1f);
|
|
Apply();
|
|
}
|
|
|
|
#endregion
|
|
|
|
#region Defaults
|
|
|
|
/// <summary>
|
|
/// Seeds every gradient/curve with a bright stylized daytime palette (blue noon,
|
|
/// warm dawn/dusk, deep-blue night) so a freshly added manager looks right without
|
|
/// hand-authoring. Invoked by the editor when the component is added or reset.
|
|
/// </summary>
|
|
private void Reset()
|
|
{
|
|
timeOfDay = 0.5f;
|
|
sunYaw = 20f;
|
|
dayLengthSeconds = 300f;
|
|
SeedStylizedPreset();
|
|
}
|
|
|
|
/// <summary>
|
|
/// Re-applies the tuned palette WITHOUT clearing the wired references (Sun Light,
|
|
/// Sky Material) or the current time — unlike Reset, which nulls everything. Exposed
|
|
/// on the context menu so the palette can be re-seeded during iteration.
|
|
/// </summary>
|
|
[ContextMenu("Apply Stylized Sky Preset")]
|
|
private void SeedStylizedPreset()
|
|
{
|
|
zenithColorOverDay = BuildGradient(
|
|
(0.00f, new Color(0.03f, 0.05f, 0.12f)),
|
|
(0.23f, new Color(0.22f, 0.30f, 0.52f)),
|
|
(0.32f, new Color(0.20f, 0.42f, 0.80f)),
|
|
(0.50f, new Color(0.20f, 0.45f, 0.85f)),
|
|
(0.68f, new Color(0.20f, 0.40f, 0.78f)),
|
|
(0.78f, new Color(0.24f, 0.28f, 0.50f)),
|
|
(1.00f, new Color(0.03f, 0.05f, 0.12f)));
|
|
|
|
horizonColorOverDay = BuildGradient(
|
|
(0.00f, new Color(0.05f, 0.07f, 0.14f)),
|
|
(0.23f, new Color(0.95f, 0.58f, 0.40f)),
|
|
(0.32f, new Color(0.75f, 0.86f, 0.95f)),
|
|
(0.50f, new Color(0.72f, 0.85f, 0.95f)),
|
|
(0.68f, new Color(0.78f, 0.84f, 0.92f)),
|
|
(0.78f, new Color(0.97f, 0.52f, 0.35f)),
|
|
(1.00f, new Color(0.05f, 0.07f, 0.14f)));
|
|
|
|
groundColorOverDay = BuildGradient(
|
|
(0.00f, new Color(0.03f, 0.04f, 0.08f)),
|
|
(0.25f, new Color(0.35f, 0.33f, 0.32f)),
|
|
(0.50f, new Color(0.40f, 0.44f, 0.48f)),
|
|
(0.75f, new Color(0.36f, 0.31f, 0.30f)),
|
|
(1.00f, new Color(0.03f, 0.04f, 0.08f)));
|
|
|
|
sunColorOverDay = BuildGradient(
|
|
(0.00f, new Color(0.35f, 0.40f, 0.55f)),
|
|
(0.23f, new Color(1.00f, 0.55f, 0.30f)),
|
|
(0.50f, new Color(1.00f, 0.96f, 0.88f)),
|
|
(0.77f, new Color(1.00f, 0.52f, 0.28f)),
|
|
(1.00f, new Color(0.35f, 0.40f, 0.55f)));
|
|
|
|
cloudColorOverDay = BuildGradient(
|
|
(0.00f, new Color(0.14f, 0.17f, 0.26f)),
|
|
(0.25f, new Color(1.00f, 0.74f, 0.58f)),
|
|
(0.50f, new Color(1.00f, 1.00f, 1.00f)),
|
|
(0.75f, new Color(1.00f, 0.72f, 0.55f)),
|
|
(1.00f, new Color(0.14f, 0.17f, 0.26f)));
|
|
|
|
cloudShadowColorOverDay = BuildGradient(
|
|
(0.00f, new Color(0.06f, 0.08f, 0.16f)),
|
|
(0.25f, new Color(0.60f, 0.45f, 0.52f)),
|
|
(0.50f, new Color(0.58f, 0.65f, 0.82f)),
|
|
(0.75f, new Color(0.58f, 0.43f, 0.50f)),
|
|
(1.00f, new Color(0.06f, 0.08f, 0.16f)));
|
|
|
|
sunIntensityOverDay = new AnimationCurve(
|
|
new Keyframe(0.00f, 0f), new Keyframe(0.23f, 0.05f), new Keyframe(0.30f, 0.8f),
|
|
new Keyframe(0.50f, 1f), new Keyframe(0.70f, 0.8f), new Keyframe(0.78f, 0.05f),
|
|
new Keyframe(1.00f, 0f));
|
|
}
|
|
|
|
/// <summary>
|
|
/// Builds a Gradient from (time, color) tuples with full alpha, keeping color
|
|
/// authoring in one compact place. Alpha is unused by the sky (every color opaque).
|
|
/// </summary>
|
|
private static Gradient BuildGradient(params (float time, Color color)[] stops)
|
|
{
|
|
var colorKeys = new GradientColorKey[stops.Length];
|
|
for (int i = 0; i < stops.Length; i++)
|
|
colorKeys[i] = new GradientColorKey(stops[i].color, stops[i].time);
|
|
|
|
var gradient = new Gradient();
|
|
gradient.SetKeys(colorKeys, new[]
|
|
{
|
|
new GradientAlphaKey(1f, 0f),
|
|
new GradientAlphaKey(1f, 1f)
|
|
});
|
|
return gradient;
|
|
}
|
|
|
|
#endregion
|
|
}
|
|
}
|