A component that controls sky colors for the scene. Defines SkyPalette data (top, horizon, cloud, two hill layers), interpolates between palettes, cycles day/night over time or picks random palettes, and writes colors to a ModelRenderer's SceneObject attributes for the shader to consume.
using System;
using Sandbox;
namespace Breakout;
/// <summary>
/// A set of sky colors: the sky gradient (top and horizon), the clouds, and the two hill layers. Lerp blends smoothly between two palettes.
/// </summary>
public sealed class SkyPalette
{
[Property] public Color Top { get; set; } = new( 0.19f, 0.56f, 0.96f );
[Property] public Color Horizon { get; set; } = new( 0.70f, 0.88f, 1.00f );
[Property] public Color Cloud { get; set; } = Color.White;
[Property] public Color HillBack { get; set; } = new( 0.34f, 0.90f, 0.44f );
[Property] public Color HillFront { get; set; } = new( 0.16f, 0.72f, 0.37f );
/// <summary>
/// Blends smoothly between two palettes, where t=0 gives a and t=1 gives b.
/// </summary>
public static SkyPalette Lerp( SkyPalette a, SkyPalette b, float t ) => new()
{
Top = Color.Lerp( a.Top, b.Top, t ),
Horizon = Color.Lerp( a.Horizon, b.Horizon, t ),
Cloud = Color.Lerp( a.Cloud, b.Cloud, t ),
HillBack = Color.Lerp( a.HillBack, b.HillBack, t ),
HillFront = Color.Lerp( a.HillFront, b.HillFront, t ),
};
}
/// <summary>
/// Colors the background sky by blending palette colors and pushing them into the sky shader.
/// Classic mode slowly cycles from day into night; the other modes just pick a random palette
/// each level.
/// </summary>
[Title( "Sky Controller" ), Category( "Breakout" ), Icon( "wb_twilight" )]
public sealed class SkyController : Component
{
[Property] public ModelRenderer Target { get; set; }
[Property, Group( "Classic Day/Night" )] public SkyPalette Day { get; set; } = new();
[Property, Group( "Classic Day/Night" )] public SkyPalette Night { get; set; } = NightDefault();
[Property, Group( "Classic Day/Night" )] public float SecondsToNight { get; set; } = 120f;
[Property, Group( "Transition" )] public float TransitionSpeed { get; set; } = 1.5f;
private SkyPalette shown;
private SkyPalette target;
private bool cycling;
private float cycleElapsed;
protected override void OnEnabled()
{
Target ??= Components.Get<ModelRenderer>( FindMode.EverythingInSelfAndDescendants );
Day ??= new SkyPalette();
Night ??= NightDefault();
shown = SkyPalette.Lerp( Day, Day, 0f );
target = shown;
Apply( shown );
}
protected override void OnUpdate()
{
if ( shown is null || target is null )
return;
if ( cycling )
{
// Slide from day (0) to night (1), starting and ending gently and moving fastest in
// the middle, so the change eases in and out instead of running at a constant rate.
cycleElapsed += Time.Delta;
float night = SecondsToNight <= 0f
? 1f
: 0.5f - 0.5f * MathF.Cos( (cycleElapsed / SecondsToNight) * MathF.PI );
target = SkyPalette.Lerp( Day, Night, night );
}
// Always drift the shown colors toward the target, so any change (cycle or new level)
// fades in smoothly rather than snapping.
shown = SkyPalette.Lerp( shown, target, MathF.Min( 1f, Time.Delta * TransitionSpeed ) );
Apply( shown );
}
/// <summary>
/// Starts the slow Classic-mode day-to-night cycle from full daytime.
/// </summary>
public void BeginClassicCycle()
{
cycling = true;
cycleElapsed = 0f;
target = Day;
}
/// <summary>
/// Picks a random sky palette and fades to it (used by non-Classic modes each level).
/// </summary>
public void Randomize()
{
cycling = false;
target = RandomPalette( Random.Shared );
}
/// <summary>
/// Fades the sky back to the daytime palette.
/// </summary>
public void ToDay()
{
cycling = false;
target = Day;
}
private void Apply( SkyPalette p )
{
if ( !Target.IsValid() )
return;
var so = Target.SceneObject;
if ( so is null )
return;
so.Attributes.Set( "SkyTop", p.Top );
so.Attributes.Set( "SkyHorizon", p.Horizon );
so.Attributes.Set( "CloudColor", p.Cloud );
so.Attributes.Set( "HillBack", p.HillBack );
so.Attributes.Set( "HillFront", p.HillFront );
}
private static SkyPalette RandomPalette( Random rng )
{
float skyHue = rng.NextSingle();
float hillHue = (skyHue + 0.30f + rng.NextSingle() * 0.35f) % 1f;
return new SkyPalette
{
Top = new ColorHsv( skyHue * 360f, 0.72f, 0.96f ).ToColor(),
Horizon = new ColorHsv( skyHue * 360f, 0.34f, 1.00f ).ToColor(),
Cloud = new ColorHsv( skyHue * 360f, 0.06f, 1.00f ).ToColor(),
HillBack = new ColorHsv( hillHue * 360f, 0.64f, 0.92f ).ToColor(),
HillFront = new ColorHsv( hillHue * 360f, 0.78f, 0.74f ).ToColor(),
};
}
private static SkyPalette NightDefault() => new()
{
Top = new Color( 0.03f, 0.05f, 0.16f ),
Horizon = new Color( 0.22f, 0.17f, 0.36f ),
Cloud = new Color( 0.34f, 0.33f, 0.47f ),
HillBack = new Color( 0.10f, 0.16f, 0.24f ),
HillFront = new Color( 0.05f, 0.09f, 0.15f ),
};
}