FootballBroadcastLighting.cs

Partial extension of FootballWorldPanel that builds an evening broadcast lighting rig in the runtime scene: one directional key light, one ambient fill, four pooled floodlight spots, shadow settings on the stadium batch, and lamp presentation. Also returns an anonymous diagnostic report of the lighting state.

using Sandbox;
using Socer;
using System;
using System.Linq;

// Runtime evening lighting, shared by authored/procedural broadcasts and replay.
public sealed partial class FootballWorldPanel
{
    const int BroadcastLightingVersion=7;
    int broadcastLightingVersion;
    Scene broadcastLightingScene;
    DirectionalLight broadcastSun;
    AmbientLight broadcastSky;
    SpotLight[] broadcastPools=new SpotLight[4];

    bool BroadcastLightingAlive()
    {
        if(broadcastLightingScene!=RenderScene || broadcastSun?.GameObject?.IsValid!=true ||
            broadcastSky?.GameObject?.IsValid!=true || eveningLamps?.GameObject?.IsValid!=true ||
            broadcastPools==null || broadcastPools.Length!=4)return false;
        for(int i=0;i<broadcastPools.Length;i++)if(broadcastPools[i]?.GameObject?.IsValid!=true)return false;
        return true;
    }

    void EnsureBroadcastLighting()
    {
        if(RenderScene==null || sceneCamera==null)return;
        if(broadcastLightingVersion==BroadcastLightingVersion && BroadcastLightingAlive())return;
        // Only the ScenePanel's runtime copy is changed. The editor-authored
        // environment and its scene/prefab remain the user's source geometry.
        foreach(var old in RenderScene.GetAllComponents<SpotLight>().ToArray())
            if(old.GameObject.Name=="Stadium floodlight")old.GameObject.Destroy();
        broadcastSun=RenderScene.GetAllComponents<DirectionalLight>().FirstOrDefault(l=>l.GameObject.Name=="Socer evening key") ??
            new GameObject(RenderScene,true,"Socer evening key").Components.Create<DirectionalLight>();
        broadcastSky=RenderScene.GetAllComponents<AmbientLight>().FirstOrDefault(l=>l.GameObject.Name=="Socer evening ambient") ??
            new GameObject(RenderScene,true,"Socer evening ambient").Components.Create<AmbientLight>();
        foreach(var light in RenderScene.GetAllComponents<DirectionalLight>())light.Enabled=light==broadcastSun;
        foreach(var light in RenderScene.GetAllComponents<AmbientLight>())light.Enabled=light==broadcastSky;
        foreach(var light in RenderScene.GetAllComponents<PointLight>())light.Enabled=false;
        foreach(var light in RenderScene.GetAllComponents<SpotLight>())light.Enabled=false;
        broadcastSun.GameObject.Enabled=broadcastSky.GameObject.Enabled=true;
        // A lower key reaches shirts and faces, while neutral cool fill keeps
        // their camera-facing side readable without exposing the whole turf.
        broadcastSky.Color=new Color(.16f,.19f,.24f);
        broadcastSun.WorldRotation=Rotation.From(new Angles(48,32,0));
        broadcastSun.LightColor=new Color(.85f,.90f,.96f);
        broadcastSun.Shadows=true;
        broadcastSun.ShadowCascadeCount=3;
        broadcastSun.ShadowCascadeSplitRatio=.86f;
        broadcastSun.ShadowHardness=.55f;
        broadcastSun.ContactShadows=true;
        sceneCamera.BackgroundColor=new Color(.018f,.035f,.060f);

        if(broadcastPools==null || broadcastPools.Length!=4)broadcastPools=new SpotLight[4];
        float[] poolX={19,45,73,99};
        for(int i=0;i<4;i++)
        {
            string name="Socer evening pool "+i;
            var light=RenderScene.GetAllComponents<SpotLight>().FirstOrDefault(l=>l.GameObject.Name==name) ??
                new GameObject(RenderScene,true,name).Components.Create<SpotLight>();
            var origin=WorldPoint(new Point(poolX[i],-13.83f),7.71f);
            var aim=WorldPoint(new Point(poolX[i],5),0);
            light.GameObject.Enabled=true;light.Enabled=true;
            light.WorldPosition=origin;light.WorldRotation=Rotation.LookAt(aim-origin);
            light.LightColor=new Color(.20f,.22f,.25f);
            light.Radius=55*Units;light.ConeInner=15;light.ConeOuter=45;light.Attenuation=.00008f;
            light.Shadows=false;broadcastPools[i]=light;
        }
        foreach(var renderer in RenderScene.GetAllComponents<ModelRenderer>())
            if(renderer.GameObject.Name=="Stadium batch")
                renderer.RenderType=ModelRenderer.ShadowRenderType.On;
        EnsureEveningLampPresentation();
        broadcastLightingScene=RenderScene;
        broadcastLightingVersion=BroadcastLightingVersion;
    }

    public object BroadcastLightingReport()
    {
        var renderers=RenderScene.GetAllComponents<ModelRenderer>().ToArray();
        return new
        {
            Version=broadcastLightingVersion,
            Mode="Evening floodlights",UsesAuthoredEnvironment=authoredEnvironment!=null,
            RuntimeSceneOwned=broadcastLightingScene==RenderScene,Alive=BroadcastLightingAlive(),
            DirectionalLights=RenderScene.GetAllComponents<DirectionalLight>().Count(l=>l.Enabled && l.GameObject.Enabled),
            LocalLights=RenderScene.GetAllComponents<SpotLight>().Count(l=>l.Enabled && l.GameObject.Enabled)+
                RenderScene.GetAllComponents<PointLight>().Count(l=>l.Enabled && l.GameObject.Enabled),
            ShadowedLocalLights=RenderScene.GetAllComponents<SpotLight>().Count(l=>l.Enabled && l.Shadows)+
                RenderScene.GetAllComponents<PointLight>().Count(l=>l.Enabled && l.Shadows),
            ShadowedDirectionalLights=RenderScene.GetAllComponents<DirectionalLight>().Count(l=>l.Enabled && l.Shadows),
            AmbientLights=RenderScene.GetAllComponents<AmbientLight>().Count(l=>l.Enabled && l.GameObject.Enabled),
            SunColor=broadcastSun?.LightColor.ToString(),SkyColor=broadcastSky?.Color.ToString(),
            SunShadows=broadcastSun?.Shadows,
            ShadowCascades=broadcastSun?.ShadowCascadeCount,ContactShadows=broadcastSun?.ContactShadows,
            ShadowHardness=broadcastSun?.ShadowHardness,
            PoolRadiusMetres=broadcastPools?.Where(l=>l?.GameObject?.IsValid==true && l.Enabled).Select(l=>l.Radius/Units).DefaultIfEmpty(0).Max()??0,
            NonShadowPools=broadcastPools?.Count(l=>l?.GameObject?.IsValid==true && l.Enabled && !l.Shadows)??0,
            Pools=broadcastPools?.Where(l=>l?.GameObject?.IsValid==true).Select(l=>new
            {
                l.GameObject.Name,l.Enabled,l.Shadows,Color=l.LightColor.ToString(),Position=l.WorldPosition.ToString(),
                RadiusMetres=l.Radius/Units,l.ConeInner,l.ConeOuter,l.Attenuation
            }).ToArray(),
            VisibleLampBanks=eveningLamps?.Enabled==true?eveningLampBanks:0,
            LampPresentationDraws=eveningLamps?.Enabled==true?1:0,LampPresentationVertices=eveningLampVertices,
            EnabledShadowRenderers=renderers.Count(r=>r.Enabled && r.RenderType==ModelRenderer.ShadowRenderType.On),
            StadiumShadowBatches=renderers.Count(r=>r.GameObject.Name=="Stadium batch" && r.RenderType==ModelRenderer.ShadowRenderType.On),
            OpaqueTurf=pitchSurface!=null,PaintedLightPatches=false,ExtraTurfOverlays=0
        };
    }
}