From 4c1f8f93352839fd0272816aa27ef70fe7976b4f Mon Sep 17 00:00:00 2001
From: Stalen <33173619+stalengd@users.noreply.github.com>
Date: Wed, 14 Aug 2024 01:06:20 +0300
Subject: [PATCH] Texture fade overlay (#1580)
---
Content.Client/SS220/Overlays/OverlayStack.cs | 119 ++++++++++++++++++
.../SS220/TextureFade/TextureFadeOverlay.cs | 75 +++++++++++
.../TextureFadeOverlayComponent.cs | 36 ++++++
.../TextureFade/TextureFadeOverlaySystem.cs | 82 ++++++++++++
.../TextureFadeOverlayComponent.cs | 9 ++
.../SharedTextureFadeOverlayComponent.cs | 10 ++
.../Prototypes/SS220/Shaders/texture_fade.yml | 5 +
.../Textures/SS220/Shaders/texture_fade.swsl | 12 ++
8 files changed, 348 insertions(+)
create mode 100644 Content.Client/SS220/Overlays/OverlayStack.cs
create mode 100644 Content.Client/SS220/TextureFade/TextureFadeOverlay.cs
create mode 100644 Content.Client/SS220/TextureFade/TextureFadeOverlayComponent.cs
create mode 100644 Content.Client/SS220/TextureFade/TextureFadeOverlaySystem.cs
create mode 100644 Content.Server/SS220/TextureFade/TextureFadeOverlayComponent.cs
create mode 100644 Content.Shared/SS220/TextureFade/SharedTextureFadeOverlayComponent.cs
create mode 100644 Resources/Prototypes/SS220/Shaders/texture_fade.yml
create mode 100644 Resources/Textures/SS220/Shaders/texture_fade.swsl
diff --git a/Content.Client/SS220/Overlays/OverlayStack.cs b/Content.Client/SS220/Overlays/OverlayStack.cs
new file mode 100644
index 00000000000000..80d2433542836c
--- /dev/null
+++ b/Content.Client/SS220/Overlays/OverlayStack.cs
@@ -0,0 +1,119 @@
+// © SS220, An EULA/CLA with a hosting restriction, full text: https://raw.githubusercontent.com/SerbiaStrong-220/space-station-14/master/CLA.txt
+using Robust.Client.Graphics;
+using Robust.Shared.Enums;
+using Robust.Shared.Timing;
+
+namespace Content.Client.SS220.Overlays
+{
+ ///
+ /// Special overlays container created to bypass limits of not supporting multiple overlays of the same type.
+ ///
+ public sealed class OverlayStack : Overlay
+ {
+ public override OverlaySpace Space => _space;
+ private readonly SortedDictionary _overlays = new(OverlayComparer.Instance);
+
+ private OverlaySpace _space;
+
+ public bool AddOverlay(StackableOverlay overlay)
+ {
+ if (_overlays.ContainsKey(overlay))
+ return false;
+ _overlays.Add(overlay, new());
+ _space |= overlay.Space;
+ return true;
+ }
+
+ public bool RemoveOverlay(StackableOverlay overlay)
+ {
+ if (!_overlays.ContainsKey(overlay))
+ return false;
+ _overlays.Remove(overlay);
+ return true;
+ }
+
+ protected override void FrameUpdate(FrameEventArgs args)
+ {
+ foreach (var (overlay, _) in _overlays)
+ {
+ overlay.FrameUpdatePublic(args);
+ }
+ }
+
+ protected override bool BeforeDraw(in OverlayDrawArgs args)
+ {
+ var result = false;
+ foreach (var (overlay, data) in _overlays)
+ {
+ if (!ShouldDraw(overlay, args))
+ continue;
+ var shouldDraw = overlay.BeforeDrawPublic(in args);
+ data.ShouldDrawThisFrame = shouldDraw;
+ result |= shouldDraw;
+ }
+ return result;
+ }
+
+ protected override void Draw(in OverlayDrawArgs args)
+ {
+ foreach (var (overlay, data) in _overlays)
+ {
+ if (!ShouldDraw(overlay, args) || !data.ShouldDrawThisFrame)
+ continue;
+ overlay.DrawPublic(in args);
+ }
+ }
+
+ public static OverlayStack Get(IOverlayManager overlayManager)
+ {
+ if (overlayManager.TryGetOverlay(out var overlay))
+ return overlay;
+ overlay = new OverlayStack();
+ overlayManager.AddOverlay(overlay);
+ return overlay;
+ }
+
+ private static bool ShouldDraw(StackableOverlay overlay, OverlayDrawArgs drawArgs)
+ {
+ return (overlay.Space & drawArgs.Space) != 0;
+ }
+
+ private sealed class OverlayData
+ {
+ public bool ShouldDrawThisFrame;
+ }
+
+ private sealed class OverlayComparer : IComparer
+ {
+ public static readonly OverlayComparer Instance = new();
+
+ public int Compare(Overlay? x, Overlay? y)
+ {
+ var zX = x?.ZIndex ?? 0;
+ var zY = y?.ZIndex ?? 0;
+ return zX.CompareTo(zY);
+ }
+ }
+ }
+
+ ///
+ /// An overlay supported by
+ ///
+ public abstract class StackableOverlay : Overlay
+ {
+ public void FrameUpdatePublic(FrameEventArgs args)
+ {
+ FrameUpdate(args);
+ }
+
+ public bool BeforeDrawPublic(in OverlayDrawArgs args)
+ {
+ return BeforeDraw(in args);
+ }
+
+ public void DrawPublic(in OverlayDrawArgs args)
+ {
+ Draw(in args);
+ }
+ }
+}
diff --git a/Content.Client/SS220/TextureFade/TextureFadeOverlay.cs b/Content.Client/SS220/TextureFade/TextureFadeOverlay.cs
new file mode 100644
index 00000000000000..e0be72beb9762c
--- /dev/null
+++ b/Content.Client/SS220/TextureFade/TextureFadeOverlay.cs
@@ -0,0 +1,75 @@
+// © SS220, An EULA/CLA with a hosting restriction, full text: https://raw.githubusercontent.com/SerbiaStrong-220/space-station-14/master/CLA.txt
+using System.Numerics;
+using Content.Client.SS220.Overlays;
+using Robust.Client.GameObjects;
+using Robust.Client.Graphics;
+using Robust.Shared.Enums;
+using Robust.Shared.Prototypes;
+using Robust.Shared.Timing;
+using Robust.Shared.Utility;
+
+namespace Content.Client.SS220.TextureFade;
+
+///
+/// Performs alpha clip on the privided texture with variable threshold (threshold filter). See for more automatic use.
+///
+public sealed class TextureFadeOverlay : StackableOverlay
+{
+ [Dependency] private readonly IPrototypeManager _prototypeManager = default!;
+ [Dependency] private readonly IEntityManager _entityManager = default!;
+ private readonly SpriteSystem _spriteSystem = default!;
+
+ public override OverlaySpace Space => OverlaySpace.WorldSpace;
+ public SpriteSpecifier? Sprite;
+ public Color Modulate = Color.White;
+ public float FadeProgress = 0f;
+ public TimeSpan Time;
+ public bool Loop = true;
+
+ private readonly ShaderInstance _shader;
+
+ public TextureFadeOverlay()
+ {
+ IoCManager.InjectDependencies(this);
+ _spriteSystem = _entityManager.EntitySysManager.GetEntitySystem();
+ _shader = _prototypeManager.Index("TextureFade").InstanceUnique();
+ }
+
+ protected override void FrameUpdate(FrameEventArgs args)
+ {
+ Time += TimeSpan.FromSeconds(args.DeltaSeconds);
+ }
+
+ protected override void Draw(in OverlayDrawArgs args)
+ {
+ if (Sprite == null)
+ return;
+
+ var texture = _spriteSystem.GetFrame(Sprite, Time, Loop);
+
+ var worldHandle = args.WorldHandle;
+ _shader.SetParameter("FadeProgress", FadeProgress);
+
+ var viewQuad = args.WorldBounds;
+ var viewSize = viewQuad.Box.Size;
+ var viewRatio = viewSize.X / viewSize.Y;
+ var regionSize = texture.Size;
+ var regionRatio = (float)regionSize.X / regionSize.Y;
+ var scaleBy = Vector2.One;
+ if (viewRatio > regionRatio)
+ {
+ scaleBy.Y = viewRatio / regionRatio;
+ }
+ else
+ {
+ scaleBy.X = regionRatio / viewRatio;
+ }
+ viewQuad.Box = viewQuad.Box.Scale(scaleBy);
+
+ worldHandle.Modulate = Modulate;
+ worldHandle.UseShader(_shader);
+ worldHandle.DrawTextureRectRegion(texture, viewQuad);
+ worldHandle.UseShader(null);
+ worldHandle.Modulate = Color.White;
+ }
+}
diff --git a/Content.Client/SS220/TextureFade/TextureFadeOverlayComponent.cs b/Content.Client/SS220/TextureFade/TextureFadeOverlayComponent.cs
new file mode 100644
index 00000000000000..52c1642e0286bd
--- /dev/null
+++ b/Content.Client/SS220/TextureFade/TextureFadeOverlayComponent.cs
@@ -0,0 +1,36 @@
+// © SS220, An EULA/CLA with a hosting restriction, full text: https://raw.githubusercontent.com/SerbiaStrong-220/space-station-14/master/CLA.txt
+using Content.Shared.SS220.TextureFade;
+using Robust.Shared.Utility;
+
+namespace Content.Client.SS220.TextureFade;
+
+///
+/// Component for automatic texture fade processing, you can still use directly.
+/// You can use all this data fiels directly in code to enable/disable, set progress, etc.
+///
+[RegisterComponent]
+public sealed partial class TextureFadeOverlayComponent : SharedTextureFadeOverlayComponent
+{
+ [DataField, ViewVariables(VVAccess.ReadWrite)]
+ public bool IsEnabled = false;
+ [DataField, ViewVariables(VVAccess.ReadWrite)]
+ public float FadeProgress = 1;
+ [DataField, ViewVariables(VVAccess.ReadWrite)]
+ public SpriteSpecifier Sprite = SpriteSpecifier.Invalid;
+ [DataField, ViewVariables(VVAccess.ReadWrite)]
+ public Color Modulate = Color.White;
+ [DataField, ViewVariables(VVAccess.ReadWrite)]
+ public int ZIndex = 0;
+ [DataField, ViewVariables(VVAccess.ReadWrite)]
+ public float ProgressSpeed = 0;
+ [DataField, ViewVariables(VVAccess.ReadWrite)]
+ public float MinProgress = 0;
+ [DataField, ViewVariables(VVAccess.ReadWrite)]
+ public float MaxProgress = 1;
+ [DataField, ViewVariables(VVAccess.ReadWrite)]
+ public float PulseMagnitude = 0;
+ [DataField, ViewVariables(VVAccess.ReadWrite)]
+ public float PulseRate = 0;
+
+ public TextureFadeOverlay? Overlay;
+}
diff --git a/Content.Client/SS220/TextureFade/TextureFadeOverlaySystem.cs b/Content.Client/SS220/TextureFade/TextureFadeOverlaySystem.cs
new file mode 100644
index 00000000000000..166a9c5a94ca59
--- /dev/null
+++ b/Content.Client/SS220/TextureFade/TextureFadeOverlaySystem.cs
@@ -0,0 +1,82 @@
+// © SS220, An EULA/CLA with a hosting restriction, full text: https://raw.githubusercontent.com/SerbiaStrong-220/space-station-14/master/CLA.txt
+using Content.Client.SS220.Overlays;
+using Robust.Client.Graphics;
+
+namespace Content.Client.SS220.TextureFade;
+
+public sealed class TextureFadeOverlaySystem : EntitySystem
+{
+ [Dependency] private readonly IOverlayManager _overlayManager = default!;
+
+ public override void Initialize()
+ {
+ SubscribeLocalEvent(OnRemove);
+ }
+
+ public override void FrameUpdate(float frameTime)
+ {
+ var componentsQuery = EntityQueryEnumerator();
+ while (componentsQuery.MoveNext(out var comp))
+ {
+ HandleOverlayActivityUpdate(comp);
+ HandleOverlayProgressUpdate(comp, frameTime);
+ }
+ }
+
+ private void OnRemove(Entity entity, ref ComponentRemove args)
+ {
+ DestroyOverlay(entity.Comp);
+ }
+
+ private void HandleOverlayActivityUpdate(TextureFadeOverlayComponent component)
+ {
+ if (component.IsEnabled && component.Overlay is null)
+ {
+ component.Overlay = CreateOverlay(component);
+ return;
+ }
+ if (!component.IsEnabled && component.Overlay is { })
+ {
+ DestroyOverlay(component);
+ return;
+ }
+ }
+
+ private void HandleOverlayProgressUpdate(TextureFadeOverlayComponent component, float frameTime)
+ {
+ if (component.Overlay == null)
+ return;
+ if (component.ProgressSpeed != 0f)
+ {
+ component.FadeProgress += component.ProgressSpeed * frameTime;
+ component.FadeProgress = Math.Clamp(component.FadeProgress, component.MinProgress, component.MaxProgress);
+ }
+ var fadeProgressMod = component.FadeProgress;
+ fadeProgressMod += (float)Math.Sin(Math.PI * component.Overlay.Time.TotalSeconds * component.PulseRate) * component.PulseMagnitude;
+ fadeProgressMod = Math.Clamp(fadeProgressMod, 0f, 1f);
+ component.Overlay.FadeProgress = fadeProgressMod;
+ component.Overlay.Modulate = component.Modulate;
+ component.Overlay.ZIndex = component.ZIndex;
+ }
+
+ private TextureFadeOverlay CreateOverlay(TextureFadeOverlayComponent component)
+ {
+ var overlay = new TextureFadeOverlay()
+ {
+ Sprite = component.Sprite,
+ Modulate = component.Modulate,
+ ZIndex = component.ZIndex,
+ };
+ OverlayStack.Get(_overlayManager).AddOverlay(overlay);
+ return overlay;
+ }
+
+ private void DestroyOverlay(TextureFadeOverlayComponent component)
+ {
+ if (component.Overlay is null)
+ return;
+ OverlayStack.Get(_overlayManager).RemoveOverlay(component.Overlay);
+ component.Overlay.Dispose();
+ component.Overlay = null;
+ }
+}
diff --git a/Content.Server/SS220/TextureFade/TextureFadeOverlayComponent.cs b/Content.Server/SS220/TextureFade/TextureFadeOverlayComponent.cs
new file mode 100644
index 00000000000000..8b0b48d9ab93dc
--- /dev/null
+++ b/Content.Server/SS220/TextureFade/TextureFadeOverlayComponent.cs
@@ -0,0 +1,9 @@
+// © SS220, An EULA/CLA with a hosting restriction, full text: https://raw.githubusercontent.com/SerbiaStrong-220/space-station-14/master/CLA.txt
+using Content.Shared.SS220.TextureFade;
+
+namespace Content.Server.SS220.TextureFade;
+
+[RegisterComponent]
+public sealed partial class TextureFadeOverlayComponent : SharedTextureFadeOverlayComponent
+{
+}
diff --git a/Content.Shared/SS220/TextureFade/SharedTextureFadeOverlayComponent.cs b/Content.Shared/SS220/TextureFade/SharedTextureFadeOverlayComponent.cs
new file mode 100644
index 00000000000000..595f847e86e73a
--- /dev/null
+++ b/Content.Shared/SS220/TextureFade/SharedTextureFadeOverlayComponent.cs
@@ -0,0 +1,10 @@
+// © SS220, An EULA/CLA with a hosting restriction, full text: https://raw.githubusercontent.com/SerbiaStrong-220/space-station-14/master/CLA.txt
+namespace Content.Shared.SS220.TextureFade;
+
+///
+/// Component for automatic texture fade processing.
+/// Makes sense only on client.
+///
+public abstract partial class SharedTextureFadeOverlayComponent : Component
+{
+}
diff --git a/Resources/Prototypes/SS220/Shaders/texture_fade.yml b/Resources/Prototypes/SS220/Shaders/texture_fade.yml
new file mode 100644
index 00000000000000..8b2bc8ebd7fcf7
--- /dev/null
+++ b/Resources/Prototypes/SS220/Shaders/texture_fade.yml
@@ -0,0 +1,5 @@
+# © SS220, An EULA/CLA with a hosting restriction, full text: https://raw.githubusercontent.com/SerbiaStrong-220/space-station-14/master/CLA.txt
+- type: shader
+ id: TextureFade
+ kind: source
+ path: "/Textures/SS220/Shaders/texture_fade.swsl"
\ No newline at end of file
diff --git a/Resources/Textures/SS220/Shaders/texture_fade.swsl b/Resources/Textures/SS220/Shaders/texture_fade.swsl
new file mode 100644
index 00000000000000..af5ad221f9f7e1
--- /dev/null
+++ b/Resources/Textures/SS220/Shaders/texture_fade.swsl
@@ -0,0 +1,12 @@
+// © SS220, An EULA/CLA with a hosting restriction, full text: https://raw.githubusercontent.com/SerbiaStrong-220/space-station-14/master/CLA.txt
+uniform highp float FadeProgress;
+
+void fragment() {
+
+ highp vec4 color = texture2D(TEXTURE, UV.xy);
+ if (color.a <= FadeProgress) {
+ discard;
+ }
+ color.a = 1;
+ COLOR = color;
+}
\ No newline at end of file