Static helper that shades the world by the current TimeOfDay. Each frame it sets color adjustments and grading temperature on the camera, dims and tints directional lights and skyboxes at night, toggles the map's street lamps, and builds extra street lights along GPS road points. Nothing is networked; each client computes the same time locally.
namespace HorizonRP;
/// <summary>
/// Le jour et la nuit, chez chaque joueur, d'après <see cref="GameClock"/>. La ville est une carte
/// Source à l'éclairage précalculé : pas de soleil qui tourne. L'ambiance passe donc par l'image :
/// un étalonnage sur la caméra (luminosité, température de couleur : un peu froide la nuit,
/// chaude à l'aube et au crépuscule), la teinte du ciel, et les lampadaires de la carte, allumés
/// seulement la nuit. Rien n'est en réseau : chaque machine calcule la même heure.
/// </summary>
public static class DayNight
{
// La nuit : ce qu'il reste de lumière, de couleurs, et sa température (kelvins).
// La carte est déjà sombre d'elle-même : la nuit n'ôte qu'un quart de la lumière (à 0,5, on
// n'y voyait plus rien). Le jour reste tel quel : à 1,1, l'image sortait délavée.
// Les couleurs ne bougent plus (30/10/2026) : désaturées à 0,7, contrastées à 1,08 et passées à
// 10 500 K, les textures des props sortaient délavées, le rouge de l'écran du distributeur
// virait au rose (Julien). La nuit assombrit, et se refroidit à peine.
public static float NightBrightness { get; set; } = 0.72f;
public static float DayBrightness { get; set; } = 1f;
public static float NightSaturation { get; set; } = 1f;
public static float NightContrast { get; set; } = 1f;
public static float NightTemperature { get; set; } = 7800f;
public static float DayTemperature { get; set; } = 6500f;
public static float WarmTemperature { get; set; } = 3800f;
// La lune : de nuit, le soleil de la scène (light_environment) et l'ambiant du ciel baissent,
// bleutés (30/10/2026). Jusque-là le soleil éclairait toute la nuit (les arbres avaient leur ombre)
// et la nuit n'était qu'une image assombrie : les lampadaires de Truenorth, éclairée en direct, ne
// se voyaient pas. Union, à l'éclairage précalculé, n'en change presque pas.
public static float MoonLight { get; set; } = 0.12f;
public static float MoonSky { get; set; } = 0.22f;
static Color MoonTint => new( 0.75f, 0.85f, 1.2f );
static readonly Dictionary<DirectionalLight, (Color Light, Color Sky)> suns = new();
static ColorAdjustments adjust;
static ColorGrading grading;
static GameObject onCamera;
static readonly Dictionary<SkyBox2D, Color> skies = new();
static List<PointLight> lamps;
static GameObject streetLights;
// L'éclairage public : les lumières de la carte sont trop rares (une tous les vingt mètres, de
// neuf mètres de portée) et la rue restait noire entre elles (Julien). Une lumière de lampadaire
// orangée tous les <see cref="StreetSpacing"/> pouces le long des routes du GPS, à
// <see cref="StreetHeight"/> au-dessus de la chaussée, chez chaque joueur (pas en réseau).
public static float StreetSpacing { get; set; } = 450f;
public static float StreetHeight { get; set; } = 280f;
public static float StreetRadius { get; set; } = 820f;
public static float StreetIntensity { get; set; } = 10f;
static bool? lampsOn;
static RealTimeSince sinceLamps = 10;
/// <summary>Chaque image, chez le joueur (HUD).</summary>
public static void Update( Scene scene )
{
if ( Application.IsDedicatedServer || scene?.Camera is not { } camera )
return;
// Les réglages d'image vivent sur la caméra du jeu ; recréés si elle change.
if ( onCamera != camera.GameObject || !adjust.IsValid() || !grading.IsValid() )
{
onCamera = camera.GameObject;
adjust = onCamera.Components.GetOrCreate<ColorAdjustments>();
grading = onCamera.Components.GetOrCreate<ColorGrading>();
grading.GradingMethod = ColorGrading.GradingType.TemperatureControl;
}
var light = GameClock.Daylight;
// L'aube et le crépuscule : la chaleur culmine à mi-chemin entre le jour et la nuit.
var warm = light is > 0 and < 1 ? 1 - MathF.Abs( light * 2 - 1 ) : 0;
adjust.Brightness = MathX.Lerp( NightBrightness, DayBrightness, light );
// Inconscient : l'écran passe en noir et blanc (Julien, 31/10/2026), en fondu (Pain.Grey).
adjust.Saturation = MathX.Lerp( NightSaturation, 1f, light ) * (1f - Pain.Grey);
adjust.Contrast = MathX.Lerp( NightContrast, 1f, light );
var temperature = MathX.Lerp( NightTemperature, DayTemperature, light );
grading.ColorTempK = MathX.Lerp( temperature, WarmTemperature, warm * 0.8f );
foreach ( var sun in scene.GetAllComponents<DirectionalLight>() )
{
if ( !suns.ContainsKey( sun ) )
suns[sun] = (sun.LightColor, sun.SkyColor);
var (dayLight, daySky) = suns[sun];
sun.LightColor = Color.Lerp( dayLight * MoonLight * MoonTint, dayLight, light ).WithAlpha( dayLight.a );
sun.SkyColor = Color.Lerp( daySky * MoonSky, daySky, light ).WithAlpha( daySky.a );
}
foreach ( var sky in scene.GetAllComponents<SkyBox2D>() )
{
if ( !skies.ContainsKey( sky ) )
skies[sky] = sky.Tint;
var day = skies[sky];
var tint = Color.Lerp( new Color( 0.05f, 0.06f, 0.12f ), day, light );
sky.Tint = Color.Lerp( tint, new Color( 1f, 0.62f, 0.42f ), warm * 0.55f );
}
// Les lampadaires de la carte : allumés la nuit (relevés une fois, basculés au changement).
if ( sinceLamps > 1f )
{
sinceLamps = 0;
lamps ??= scene.GetAllComponents<PointLight>().Where( l => l.GameObject.Parent?.Name?.StartsWith( "Carte" ) == true ).ToList();
var on = light < 0.55f;
// Les routes du GPS sont celles d'Union : sur une autre carte, ces lumières flotteraient.
if ( on && !streetLights.IsValid() && scene.GetAllComponents<MapInstance>().Any() )
BuildStreetLights( scene );
if ( lampsOn != on )
{
lampsOn = on;
foreach ( var l in lamps )
if ( l.IsValid() )
l.Enabled = on;
if ( streetLights.IsValid() )
streetLights.Enabled = on;
}
}
}
/// <summary>Les lumières de l'éclairage public, une fois, au premier soir.</summary>
static void BuildStreetLights( Scene scene )
{
streetLights = new GameObject( true, "Éclairage public" );
streetLights.Flags |= GameObjectFlags.NotNetworked | GameObjectFlags.NotSaved;
foreach ( var p in GpsMap.RoadPoints( StreetSpacing ) )
{
// La chaussée sous le point, cherchée sous les ponts du métro aérien (5656) : la ville est
// vers 5408, le quartier sud-ouest un peu plus haut.
var from = p.WithZ( 5600 );
var hit = scene.Trace.Ray( from, from + Vector3.Down * 600 ).WithoutTags( "player", "vehicule", "trigger" ).Run();
if ( !hit.Hit )
continue;
var go = new GameObject( streetLights, true, "Lampadaire" );
go.WorldPosition = hit.EndPosition + Vector3.Up * StreetHeight;
var l = go.Components.Create<PointLight>();
l.LightColor = new Color( 1f, 0.72f, 0.42f ) * StreetIntensity;
l.Radius = StreetRadius;
l.Shadows = false;
}
streetLights.Enabled = lampsOn ?? true;
}
/// <summary>Essais : l'éclairage public est refait avec les réglages du moment.</summary>
public static void RebuildStreetLights()
{
if ( streetLights.IsValid() )
streetLights.Destroy();
streetLights = null;
lampsOn = null;
}
}