Merge remote-tracking branch 'origin/feat/inport-props' into feat/craft-discovery

# 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
This commit is contained in:
2026-07-25 19:42:26 +02:00
954 changed files with 999772 additions and 26193 deletions
+193 -112
View File
@@ -1,15 +1,21 @@
using UnityEngine;
using UnityEngine.Rendering;
namespace Ashwild.Environment
{
/// <summary>
/// Drives the day/night cycle for the "GAME/StylizedSky" skybox. Rotates the sun light and
/// pushes every time-dependent value (gradient colors, sun/moon direction, cloud params,
/// star opacity) into shader globals — mirroring how SeasonManager broadcasts "_Season".
/// The skybox shader itself is dumb: it only renders whatever globals this manager writes.
/// Runs in edit mode (ExecuteAlways) so the sky updates live while scrubbing the time slider.
/// Purely visual and local; when co-op time-sync is added, only the driving time value needs
/// to come from the server (see the "not yet networked" note in CLAUDE.md, like SeasonManager).
/// 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]
@@ -27,14 +33,16 @@ namespace Ashwild.Environment
[Header("References")]
[Tooltip("Directional light rotated to act as the sun. Optional but recommended.")]
[SerializeField] private Light sunLight;
[Tooltip("Skybox material using GAME/StylizedSky. Assigned to RenderSettings on enable when set.")]
[SerializeField] private Material skyboxMaterial;
[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 24h cycle. Only advances in Play.")]
[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;
@@ -45,68 +53,70 @@ namespace Ashwild.Environment
[Tooltip("Peak directional-light intensity reached around noon.")]
[SerializeField] private float maxSunIntensity = 1.3f;
[Header("Sky Gradient")]
[Header("Sky Gradient Over Day")]
[SerializeField] private Gradient zenithColorOverDay = new Gradient();
[SerializeField] private Gradient horizonColorOverDay = new Gradient();
[SerializeField] private Gradient groundColorOverDay = new Gradient();
[Header("Moon")]
[SerializeField] private Color moonColor = new Color(0.85f, 0.88f, 0.96f);
[Header("Clouds")]
[Header("Clouds Over Day")]
[SerializeField] private Gradient cloudColorOverDay = new Gradient();
[SerializeField] private Gradient cloudShadowColorOverDay = new Gradient();
[Tooltip("Coverage threshold over the day. Higher = fewer clouds.")]
[SerializeField] private AnimationCurve cloudCoverageOverDay = AnimationCurve.Constant(0f, 1f, 0.48f);
[Range(0f, 1f)] [SerializeField] private float cloudOpacity = 1f;
[SerializeField] private Vector2 windDirection = new Vector2(1f, 0.35f);
[SerializeField] private float windSpeed = 0.01f;
[Header("Stars")]
[Tooltip("Sun elevation at which stars start fading in (upper) and are fully visible (lower).")]
[SerializeField] private float starFadeInElevation = 0.05f;
[SerializeField] private float starFullElevation = -0.12f;
[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;
#endregion
#region State
private Vector2 cloudOffset;
[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("_SkyZenithColor");
private static readonly int HorizonColorId = Shader.PropertyToID("_SkyHorizonColor");
private static readonly int GroundColorId = Shader.PropertyToID("_SkyGroundColor");
private static readonly int SunDirectionId = Shader.PropertyToID("_SkySunDirection");
private static readonly int SunColorId = Shader.PropertyToID("_SkySunColor");
private static readonly int MoonDirectionId = Shader.PropertyToID("_SkyMoonDirection");
private static readonly int MoonColorId = Shader.PropertyToID("_SkyMoonColor");
private static readonly int CloudColorId = Shader.PropertyToID("_SkyCloudColor");
private static readonly int CloudShadowColorId = Shader.PropertyToID("_SkyCloudShadowColor");
private static readonly int CloudParamsId = Shader.PropertyToID("_SkyCloudParams");
private static readonly int StarOpacityId = Shader.PropertyToID("_SkyStarOpacity");
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, assigns the skybox and pushes an initial state so the sky is
/// correct the moment the component becomes active (including in the editor).
/// 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 (skyboxMaterial != null)
RenderSettings.skybox = skyboxMaterial;
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 the current one.
/// Clears the shared instance so a disabled manager never keeps answering as current.
/// </summary>
private void OnDisable()
{
@@ -124,14 +134,13 @@ namespace Ashwild.Environment
}
/// <summary>
/// Advances time (Play only) and scrolls the clouds, then re-applies every frame.
/// 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);
cloudOffset += windDirection.normalized * (windSpeed * Time.deltaTime);
Apply();
}
@@ -140,42 +149,70 @@ namespace Ashwild.Environment
#region Apply
/// <summary>
/// Resolves the current time of day into sun orientation and shader globals, and mirrors
/// the resolved sun color/intensity onto the directional light so scene lighting matches
/// the sky. This is the single place every time-dependent value is broadcast.
/// 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();
Vector3 moonDir = -sunDir;
float nightFactor = Mathf.SmoothStep(0f, 1f,
Mathf.InverseLerp(starFadeInElevation, starFullElevation, sunDir.y));
Color sunColor = sunColorOverDay.Evaluate(timeOfDay);
DriveSunLight(sunColor);
DriveMoonLight(sunDir, nightFactor);
PromoteMainLight(nightFactor);
Shader.SetGlobalColor(ZenithColorId, ToRendered(zenithColorOverDay.Evaluate(timeOfDay)));
Shader.SetGlobalColor(HorizonColorId, ToRendered(horizonColorOverDay.Evaluate(timeOfDay)));
Shader.SetGlobalColor(GroundColorId, ToRendered(groundColorOverDay.Evaluate(timeOfDay)));
Color horizon = horizonColorOverDay.Evaluate(timeOfDay);
CurrentHorizonColor = horizon;
CurrentSunColor = sunColor;
CurrentNightFactor = nightFactor;
if (driveFogColor)
RenderSettings.fogColor = ToRendered(horizon);
Shader.SetGlobalVector(SunDirectionId, sunDir);
Shader.SetGlobalColor(SunColorId, ToRendered(sunColor));
Shader.SetGlobalVector(MoonDirectionId, moonDir);
Shader.SetGlobalColor(MoonColorId, ToRendered(moonColor));
if (skyMaterial == null)
{
RepaintEditor();
return;
}
Shader.SetGlobalColor(CloudColorId, ToRendered(cloudColorOverDay.Evaluate(timeOfDay)));
Shader.SetGlobalColor(CloudShadowColorId, ToRendered(cloudShadowColorOverDay.Evaluate(timeOfDay)));
Shader.SetGlobalVector(CloudParamsId, new Vector4(
cloudCoverageOverDay.Evaluate(timeOfDay), cloudOpacity, cloudOffset.x, cloudOffset.y));
skyMaterial.SetColor(ZenithColorId, ToRendered(zenithColorOverDay.Evaluate(timeOfDay)));
skyMaterial.SetColor(HorizonColorId, ToRendered(horizon));
skyMaterial.SetColor(GroundColorId, ToRendered(groundColorOverDay.Evaluate(timeOfDay)));
Shader.SetGlobalFloat(StarOpacityId,
Mathf.SmoothStep(0f, 1f, Mathf.InverseLerp(starFadeInElevation, starFullElevation, sunDir.y)));
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.
/// 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()
{
@@ -198,9 +235,35 @@ namespace Ashwild.Environment
}
/// <summary>
/// Converts an authored sRGB color to the value the shader must receive. Colors pushed via
/// Shader.SetGlobalColor are NOT auto-gamma-corrected (unlike material properties), so in a
/// linear project they must be pre-converted or the whole sky washes out to white.
/// 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)
{
@@ -208,7 +271,8 @@ namespace Ashwild.Environment
}
/// <summary>
/// Forces the Scene view to redraw while scrubbing values outside Play, matching SeasonManager.
/// 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()
{
@@ -228,7 +292,26 @@ namespace Ashwild.Environment
public float TimeOfDay => timeOfDay;
/// <summary>
/// Sets the time of day directly (wrapped into [0,1)) and refreshes the sky immediately.
/// 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)
{
@@ -241,9 +324,9 @@ namespace Ashwild.Environment
#region Defaults
/// <summary>
/// Seeds all gradients/curves with a bright, soft daytime palette matched to the Raygeas
/// stylized skybox reference, so a freshly added manager looks right without hand-authoring.
/// Invoked by the editor when the component is added or reset.
/// 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()
{
@@ -254,70 +337,68 @@ namespace Ashwild.Environment
}
/// <summary>
/// Re-applies the tuned color palette WITHOUT clearing the wired references (Sun Light,
/// Skybox Material) or the current time — unlike Reset, which nulls everything. Exposed on
/// the component's context menu so the palette can be re-seeded during iteration.
/// 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.02f, 0.03f, 0.07f)),
(0.23f, new Color(0.18f, 0.28f, 0.40f)),
(0.30f, new Color(0.14f, 0.40f, 0.56f)),
(0.50f, new Color(0.13f, 0.42f, 0.58f)),
(0.70f, new Color(0.14f, 0.38f, 0.53f)),
(0.78f, new Color(0.22f, 0.26f, 0.40f)),
(1.00f, new Color(0.02f, 0.03f, 0.07f)));
(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.04f, 0.05f, 0.10f)),
(0.23f, new Color(0.86f, 0.50f, 0.36f)),
(0.32f, new Color(0.32f, 0.56f, 0.68f)),
(0.50f, new Color(0.26f, 0.52f, 0.66f)),
(0.68f, new Color(0.30f, 0.50f, 0.60f)),
(0.78f, new Color(0.86f, 0.48f, 0.32f)),
(1.00f, new Color(0.04f, 0.05f, 0.10f)));
(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.26f, 0.30f, 0.34f)),
(0.50f, new Color(0.22f, 0.44f, 0.56f)),
(0.75f, new Color(0.28f, 0.28f, 0.30f)),
(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.40f, 0.40f, 0.50f)),
(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.95f, 0.85f)),
(0.77f, new Color(1.00f, 0.50f, 0.28f)),
(1.00f, new Color(0.40f, 0.40f, 0.50f)));
(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.10f, 0.11f, 0.18f)),
(0.25f, new Color(0.98f, 0.78f, 0.66f)),
(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(0.98f, 0.76f, 0.64f)),
(1.00f, new Color(0.10f, 0.11f, 0.18f)));
(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.05f, 0.06f, 0.10f)),
(0.25f, new Color(0.55f, 0.45f, 0.50f)),
(0.50f, new Color(0.70f, 0.76f, 0.84f)),
(0.75f, new Color(0.56f, 0.44f, 0.48f)),
(1.00f, new Color(0.05f, 0.06f, 0.10f)));
(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));
cloudCoverageOverDay = AnimationCurve.Constant(0f, 1f, 0.55f);
}
/// <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 is opaque).
/// 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)
{