Game/LightingPreview.cs

The class provides a local system for visual comparison of scene lighting configurations. It temporarily overrides tonemapping, bloom, and directional light properties to test various lighting presets against an original scene state.

namespace CitySim;

/// <summary>Local tabletop lighting with session-only comparison controls.</summary>
public sealed class LightingPreview
{
 readonly Tonemapping tone;
 readonly Bloom bloom;
 readonly DirectionalLight sun;
 readonly AmbientLight fill;
 readonly GameObject rig;
 readonly (DirectionalLight Light,bool Enabled)[] sceneSuns;
 readonly Tonemapping.TonemappingMode originalMode;
 readonly float originalExposure, originalBloom;
 readonly bool originalToneEnabled, originalBloomEnabled, originalAutoExposure;
 readonly Color originalSky, originalSun;
 readonly Rotation originalSunRotation;
 static readonly int[] SunLevels = { 0, 50, 75, 100, 150, 200, 300 };
 static readonly int[] SunDirections = { 0, 45, 90, 135, 180, 225, 270, 315 };
 int sunLevel=3, sunDirection;
 public string SunBrightness => $"{SunLevels[sunLevel]}%";
 public string SunDirection => sunDirection==0 ? "Default" : $"+{SunDirections[sunDirection]}°";
 public int Index { get; private set; }
 static readonly string[] Names = { "Boardgame", "Original", "Brighter", "Bright sky", "ACES", "Linear", "No tone map / bloom" };
 public string Name => Names[Index];
 public LightingPreview(Scene scene)
 {
  tone=scene.GetAllComponents<Tonemapping>().FirstOrDefault();
  bloom=scene.GetAllComponents<Bloom>().FirstOrDefault();
  sceneSuns=scene.GetAllComponents<DirectionalLight>().Select(s=>(s,s.Enabled)).ToArray();
  var source=sceneSuns.Select(s=>s.Light).FirstOrDefault();
  originalSky=source.IsValid() ? source.SkyColor : new Color(0.25f,0.32f,0.35f);
  originalSun=source.IsValid() ? source.LightColor : Color.White;
  originalSunRotation=source.IsValid() ? source.GameObject.WorldRotation : Rotation.From(55,30,0);
  if (tone.IsValid()) { originalMode=tone.Mode; originalExposure=tone.ExposureCompensation; originalToneEnabled=tone.Enabled; originalAutoExposure=tone.AutoExposureEnabled; }
  if (bloom.IsValid()) { originalBloom=bloom.Strength; originalBloomEnabled=bloom.Enabled; }
  // Own the light transform locally: network scene transforms cannot overwrite the preview.
  rig=new GameObject(true,"Tabletop lighting") { Flags=GameObjectFlags.NotNetworked };
  sun=rig.Components.Create<DirectionalLight>();
  sun.Shadows=true; sun.ShadowHardness=0; sun.ContactShadows=false;
  fill=rig.Components.Create<AmbientLight>();
  foreach(var item in sceneSuns) item.Light.Enabled=false;
  Apply();
 }
 public void Step(int delta) { Index=(Index+delta+Names.Length)%Names.Length; Apply(); }
 public void StepSunBrightness(int delta) { sunLevel=(sunLevel+delta+SunLevels.Length)%SunLevels.Length; Apply(); }
 public void StepSunDirection(int delta) { sunDirection=(sunDirection+delta+SunDirections.Length)%SunDirections.Length; Apply(); }
 public void Reset() { Index=0; sunLevel=3; sunDirection=0; Apply(); }
 public void Apply()
 {
  var tabletop=Index==0;
  if (tone.IsValid())
  {
   tone.Enabled=Index!=6;
   tone.AutoExposureEnabled=false;
   tone.Mode=tabletop || Index==4 ? Tonemapping.TonemappingMode.ACES : Index==5 ? Tonemapping.TonemappingMode.Linear : originalMode;
   tone.ExposureCompensation=tabletop ? 0.4f : originalExposure+(Index is 2 or 3 ? 0.7f : 0f);
  }
  if (sun.IsValid())
  {
   // Warm key and neutral fill keep the cardboard inks legible in shadow.
   sun.SkyColor=tabletop ? new Color(0.45f,0.46f,0.44f) : Index==3 ? new Color(0.5f,0.55f,0.58f) : originalSky;
   fill.Enabled=tabletop; fill.Color=new Color(0.35f,0.34f,0.31f);
   var colour=tabletop ? new Color(1.4f,1.32f,1.18f) : originalSun;
   var intensity=SunLevels[sunLevel]/100f;
   sun.LightColor=new Color(colour.r*intensity,colour.g*intensity,colour.b*intensity,1);
   sun.GameObject.WorldRotation=Rotation.FromYaw(SunDirections[sunDirection])*(tabletop ? Rotation.From(55,30,0) : originalSunRotation);
  }
  if (bloom.IsValid()) { bloom.Enabled=Index!=6 && !tabletop && originalBloomEnabled; bloom.Strength=originalBloom; }
 }
 public void Dispose()
 {
  if (rig.IsValid()) rig.Destroy();
  foreach(var item in sceneSuns) if (item.Light.IsValid()) item.Light.Enabled=item.Enabled;
  if (tone.IsValid()) { tone.Enabled=originalToneEnabled; tone.Mode=originalMode; tone.AutoExposureEnabled=originalAutoExposure; tone.ExposureCompensation=originalExposure; }
  if (bloom.IsValid()) { bloom.Enabled=originalBloomEnabled; bloom.Strength=originalBloom; }
 }
}