Files
RobustToolbox/SS14.Client/Graphics/Lighting/LightManager.cs
Pieter-Jan Briers dbc88e253b Allow client to run headlessly. (#727)
AKA Without Godot.

Still links against GodotSharp, but this does mean that you can run headless or not from the same binary.
2019-01-19 18:23:41 +01:00

289 lines
9.4 KiB
C#

using System;
using System.Collections.Generic;
using SS14.Client.Interfaces;
using SS14.Client.Interfaces.Graphics.Lighting;
using SS14.Shared;
using SS14.Shared.IoC;
using SS14.Shared.Interfaces.Configuration;
using SS14.Client.Interfaces.ResourceManagement;
using SS14.Client.ResourceManagement;
using SS14.Shared.Log;
using SS14.Shared.Maths;
using SS14.Shared.Utility;
// Ok let me just quickly explain how deferred lighting works.
// So for some reason, Godot's Light2D has performance issues on some computers. Why? No clue. Anyways.
// So to fix this, we do "deferred lighting"!
// We have a config option to enable it because my shitty implementation doesn't look as good as normal Godot lighting.
// When enabled, each light is put into a separate Viewport. Said viewport contains the CanvasModulate and a white background.
// This means the viewport will have light rendering happen in it, onto a white canvas.
// The Viewport's output texture is then basically a lightmap we can blend over the rest of the game,
// which we do using a Sprite on its own canvas layer set to multiply blend in it CanvasMaterial.
// This performs better because we forgo the O(nlights + 1) drawing complexity of canvas items.
namespace SS14.Client.Graphics.Lighting
{
public sealed partial class LightManager : ILightManager, IDisposable, IPostInjectInit
{
[Dependency] readonly ISceneTreeHolder sceneTreeHolder;
[Dependency] readonly IConfigurationManager configManager;
[Dependency] readonly IResourceCache resourceCache;
private bool enabled = true;
public bool Enabled
{
get => enabled;
set
{
if (value == Enabled)
{
return;
}
enabled = value;
UpdateEnabled();
}
}
private List<Light> lights = new List<Light>();
private List<Occluder> occluders = new List<Occluder>();
private LightingSystem System = LightingSystem.Normal;
private Godot.CanvasModulate canvasModulate;
private Godot.Viewport rootViewport;
private Godot.Viewport deferredViewport;
private Godot.CanvasLayer deferredMaskLayer;
private Godot.Sprite deferredMaskBackground;
private Godot.Sprite deferredMaskSprite;
private GodotGlue.GodotSignalSubscriber0 deferredSizeChangedSubscriber;
private bool disposed = false;
public void PostInject()
{
configManager.RegisterCVar("display.lighting_system", LightingSystem.Normal,
Shared.Configuration.CVar.ARCHIVE);
}
public void Initialize()
{
System = configManager.GetCVar<LightingSystem>("display.lighting_system");
if (!GameController.OnGodot)
{
return;
}
canvasModulate = new Godot.CanvasModulate()
{
// Black
Color = new Godot.Color(0.1f, 0.1f, 0.1f),
Name = "LightingCanvasModulate"
};
rootViewport = sceneTreeHolder.SceneTree.Root;
if (System == LightingSystem.Deferred)
{
deferredViewport = new Godot.Viewport
{
Name = "LightingViewport",
RenderTargetUpdateMode = Godot.Viewport.UpdateMode.Always,
RenderTargetVFlip = true,
};
deferredViewport.AddChild(canvasModulate);
rootViewport.AddChild(deferredViewport);
var whiteTex =
resourceCache.GetResource<TextureResource>(new ResourcePath(@"/Textures/Effects/Light/white.png"));
deferredMaskBackground = new Godot.Sprite()
{
Name = "DeferredMaskBackground",
Texture = whiteTex.Texture.GodotTexture,
Centered = false,
};
deferredViewport.AddChild(deferredMaskBackground);
deferredSizeChangedSubscriber = new GodotGlue.GodotSignalSubscriber0();
deferredSizeChangedSubscriber.Connect(rootViewport, "size_changed");
deferredSizeChangedSubscriber.Signal += OnWindowSizeChanged;
deferredMaskLayer = new Godot.CanvasLayer()
{
Name = "LightingMaskLayer",
Layer = CanvasLayers.LAYER_DEFERRED_LIGHTING,
};
rootViewport.AddChild(deferredMaskLayer);
CreateDeferMaskSprite();
OnWindowSizeChanged();
}
else if (System == LightingSystem.Normal)
{
sceneTreeHolder.WorldRoot.AddChild(canvasModulate);
}
}
private void OnWindowSizeChanged()
{
if (System == LightingSystem.Deferred)
{
var size = sceneTreeHolder.SceneTree.Root.Size;
deferredViewport.Size = size;
deferredMaskBackground.Scale = size;
// Needs to be re-created because otherwise it seems to not acknowledge the updated texture size?
// Might be a better way to do this.
CreateDeferMaskSprite();
}
}
private void CreateDeferMaskSprite()
{
if (deferredMaskSprite != null)
{
deferredMaskSprite.QueueFree();
deferredMaskSprite.Dispose();
}
var mat = new Godot.CanvasItemMaterial()
{
BlendMode = Godot.CanvasItemMaterial.BlendModeEnum.Mul
};
deferredMaskSprite = new Godot.Sprite
{
Name = "LightingDeferMask",
Texture = deferredViewport.GetTexture(),
Centered = false,
Material = mat,
};
deferredMaskLayer.AddChild(deferredMaskSprite);
}
public void Dispose()
{
if (disposed)
{
return;
}
var localLights = new List<ILight>(lights);
foreach (var light in localLights)
{
light.Dispose();
}
if (!GameController.OnGodot)
{
return;
}
if (System == LightingSystem.Deferred)
{
deferredSizeChangedSubscriber.Disconnect(rootViewport, "size_changed");
deferredSizeChangedSubscriber.Dispose();
deferredSizeChangedSubscriber = null;
}
rootViewport = null;
if (System == LightingSystem.Deferred)
{
deferredViewport.QueueFree();
deferredViewport.Dispose();
deferredViewport = null;
deferredMaskLayer.QueueFree();
deferredMaskLayer.Dispose();
deferredMaskLayer = null;
// These are implicitly freed by the other free calls.
deferredMaskBackground.Dispose();
deferredMaskBackground = null;
deferredMaskSprite.Dispose();
deferredMaskSprite = null;
}
canvasModulate.QueueFree();
canvasModulate.Dispose();
canvasModulate = null;
}
public ILight MakeLight()
{
var light = new Light(this);
lights.Add(light);
light.UpdateEnabled();
return light;
}
private void RemoveLight(Light light)
{
// TODO: This removal is O(n) because it's a regualar list,
// and might become a performance issue.
// Use some smarter datastructure maybe?
lights.Remove(light);
}
public IOccluder MakeOccluder()
{
var occluder = new Occluder(this);
occluders.Add(occluder);
return occluder;
}
private void RemoveOccluder(Occluder occluder)
{
// TODO: This removal is O(n) because it's a regualar list,
// and might become a performance issue.
// Use some smarter datastructure maybe?
occluders.Remove(occluder);
}
private void UpdateEnabled()
{
if (!GameController.OnGodot)
{
return;
}
if (System == LightingSystem.Deferred)
{
deferredMaskSprite.Visible = Enabled;
}
foreach (var light in lights)
{
light.UpdateEnabled();
}
}
public void FrameUpdate(RenderFrameEventArgs args)
{
if (GameController.OnGodot && System == LightingSystem.Deferred)
{
var transform = rootViewport.CanvasTransform;
deferredViewport.CanvasTransform = transform;
deferredMaskBackground.Transform = transform.Inverse();
deferredMaskBackground.Scale = rootViewport.Size;
}
foreach (var light in lights)
{
light.FrameProcess(args);
}
foreach (var occluder in occluders)
{
occluder.FrameProcess(args);
}
}
public enum LightingSystem
{
Normal = 0,
Deferred = 1,
Disabled = 2,
}
}
}