]> git.smokeofanarchy.ru Git - space-station-14.git/commitdiff
Add wall-based ambient occlusion (#38276)
authormetalgearsloth <31366439+metalgearsloth@users.noreply.github.com>
Tue, 24 Jun 2025 07:56:14 +0000 (17:56 +1000)
committerGitHub <noreply@github.com>
Tue, 24 Jun 2025 07:56:14 +0000 (17:56 +1000)
* Add wall ambient occlusion

* wawawewa

* Work

* cvars

* Comment to make slart happy

Content.Client/Light/AmbientOcclusionOverlay.cs [new file with mode: 0644]
Content.Client/Light/EntitySystems/PlanetLightSystem.cs
Content.Client/Light/RoofOverlay.cs
Content.Client/Options/UI/Tabs/GraphicsTab.xaml
Content.Client/Options/UI/Tabs/GraphicsTab.xaml.cs
Content.Shared/CCVar/CCVars.Lighting.cs [new file with mode: 0644]
Resources/Locale/en-US/escape-menu/ui/options-menu.ftl
Resources/Prototypes/Shaders/Stencils.yml

diff --git a/Content.Client/Light/AmbientOcclusionOverlay.cs b/Content.Client/Light/AmbientOcclusionOverlay.cs
new file mode 100644 (file)
index 0000000..e24ee73
--- /dev/null
@@ -0,0 +1,137 @@
+using System.Numerics;
+using Content.Shared.CCVar;
+using Content.Shared.Maps;
+using Robust.Client.Graphics;
+using Robust.Shared.Configuration;
+using Robust.Shared.Enums;
+using Robust.Shared.Map;
+using Robust.Shared.Prototypes;
+using Robust.Shared.Utility;
+
+namespace Content.Client.Light;
+
+/// <summary>
+/// Applies ambient-occlusion to the viewport.
+/// </summary>
+public sealed class AmbientOcclusionOverlay : Overlay
+{
+    [Dependency] private readonly IClyde _clyde = default!;
+    [Dependency] private readonly IConfigurationManager _cfgManager = default!;
+    [Dependency] private readonly IEntityManager _entManager = default!;
+    [Dependency] private readonly IMapManager _mapManager = default!;
+    [Dependency] private readonly IPrototypeManager _proto = default!;
+    [Dependency] private readonly ITileDefinitionManager _tileDefManager = default!;
+
+    public override OverlaySpace Space => OverlaySpace.WorldSpaceBelowEntities;
+
+    private IRenderTexture? _aoTarget;
+
+    // Couldn't figure out a way to avoid this so if you can then please do.
+    private IRenderTexture? _aoStencilTarget;
+
+    public AmbientOcclusionOverlay()
+    {
+        IoCManager.InjectDependencies(this);
+        ZIndex = AfterLightTargetOverlay.ContentZIndex + 1;
+    }
+
+    protected override void Draw(in OverlayDrawArgs args)
+    {
+        /*
+         * tl;dr
+         * - we draw a black square on each "ambient occlusion" entity.
+         * - we blur this.
+         * - We apply it to the viewport.
+         *
+         * We do this while ignoring lighting because it will wash out the actual effect.
+         * In 3D ambient occlusion is more complicated due top having to calculate normals but in 2D
+         * we don't have a concept of depth / corners necessarily.
+         */
+
+        var viewport = args.Viewport;
+        var mapId = args.MapId;
+        var worldBounds = args.WorldBounds;
+        var worldHandle = args.WorldHandle;
+        var color = Color.FromHex(_cfgManager.GetCVar(CCVars.AmbientOcclusionColor));
+        var distance = _cfgManager.GetCVar(CCVars.AmbientOcclusionDistance);
+        //var color = Color.Red;
+        var target = viewport.RenderTarget;
+        var lightScale = target.Size / (Vector2) viewport.Size;
+        var scale = viewport.RenderScale / (Vector2.One / lightScale);
+        var maps = _entManager.System<SharedMapSystem>();
+        var lookups = _entManager.System<EntityLookupSystem>();
+        var query = _entManager.System<OccluderSystem>();
+        var xformSystem = _entManager.System<SharedTransformSystem>();
+        var invMatrix = args.Viewport.GetWorldToLocalMatrix();
+
+        if (_aoTarget?.Texture.Size != target.Size)
+        {
+            _aoTarget?.Dispose();
+            _aoTarget = _clyde.CreateRenderTarget(target.Size, new RenderTargetFormatParameters(RenderTargetColorFormat.Rgba8Srgb), name: "ambient-occlusion-target");
+        }
+
+        if (_aoStencilTarget?.Texture.Size != target.Size)
+        {
+            _aoStencilTarget?.Dispose();
+            _aoStencilTarget = _clyde.CreateRenderTarget(target.Size, new RenderTargetFormatParameters(RenderTargetColorFormat.Rgba8Srgb), name: "ambient-occlusion-stencil-target");
+        }
+
+        // Draw the texture data to the texture.
+        args.WorldHandle.RenderInRenderTarget(_aoTarget,
+            () =>
+            {
+                worldHandle.UseShader(_proto.Index<ShaderPrototype>("unshaded").Instance());
+                var invMatrix = _aoTarget.GetWorldToLocalMatrix(viewport.Eye!, scale);
+
+                foreach (var entry in query.QueryAabb(mapId, worldBounds))
+                {
+                    DebugTools.Assert(entry.Component.Enabled);
+                    var matrix = xformSystem.GetWorldMatrix(entry.Transform);
+                    var localMatrix = Matrix3x2.Multiply(matrix, invMatrix);
+
+                    worldHandle.SetTransform(localMatrix);
+                    // 4 pixels
+                    worldHandle.DrawRect(Box2.UnitCentered.Enlarged(distance / EyeManager.PixelsPerMeter), Color.White);
+                }
+            }, Color.Transparent);
+
+        _clyde.BlurRenderTarget(viewport, _aoTarget, _aoTarget, viewport.Eye!, 14f);
+
+        // Need to do stencilling after blur as it will nuke it.
+        // Draw stencil for the grid so we don't draw in space.
+        args.WorldHandle.RenderInRenderTarget(_aoStencilTarget,
+            () =>
+            {
+                // Don't want lighting affecting it.
+                worldHandle.UseShader(_proto.Index<ShaderPrototype>("unshaded").Instance());
+
+                foreach (var grid in _mapManager.FindGridsIntersecting(mapId, worldBounds))
+                {
+                    var transform = xformSystem.GetWorldMatrix(grid.Owner);
+                    var worldToTextureMatrix = Matrix3x2.Multiply(transform, invMatrix);
+                    var tiles = maps.GetTilesEnumerator(grid.Owner, grid, worldBounds);
+                    worldHandle.SetTransform(worldToTextureMatrix);
+                    while (tiles.MoveNext(out var tileRef))
+                    {
+                        if (tileRef.IsSpace(_tileDefManager))
+                            continue;
+
+                        var bounds = lookups.GetLocalBounds(tileRef, grid.TileSize);
+                        worldHandle.DrawRect(bounds, Color.White);
+                    }
+                }
+
+            }, Color.Transparent);
+
+        // Draw the stencil texture to depth buffer.
+        worldHandle.UseShader(_proto.Index<ShaderPrototype>("StencilMask").Instance());
+        worldHandle.DrawTextureRect(_aoStencilTarget!.Texture, worldBounds);
+
+        // Draw the Blurred AO texture finally.
+        worldHandle.UseShader(_proto.Index<ShaderPrototype>("StencilEqualDraw").Instance());
+        worldHandle.DrawTextureRect(_aoTarget!.Texture, worldBounds, color);
+
+        args.WorldHandle.SetTransform(Matrix3x2.Identity);
+        args.WorldHandle.UseShader(null);
+    }
+}
index cbe2f47f78e5be5539d57102faa7ce2617029a72..deabadaddff61d21260d95896ac6ffb4fa096493 100644 (file)
@@ -1,17 +1,51 @@
+using Content.Shared.CCVar;
 using Robust.Client.Graphics;
+using Robust.Shared.Configuration;
 
 namespace Content.Client.Light.EntitySystems;
 
 public sealed class PlanetLightSystem : EntitySystem
 {
+    [Dependency] private readonly IConfigurationManager _cfgManager = default!;
     [Dependency] private readonly IOverlayManager _overlayMan = default!;
 
+    /// <summary>
+    /// Enables / disables the ambient occlusion overlay.
+    /// </summary>
+    public bool AmbientOcclusion
+    {
+        get => _ambientOcclusion;
+        set
+        {
+            if (_ambientOcclusion == value)
+                return;
+
+            _ambientOcclusion = value;
+
+            if (value)
+            {
+                _overlayMan.AddOverlay(new AmbientOcclusionOverlay());
+            }
+            else
+            {
+                _overlayMan.RemoveOverlay<AmbientOcclusionOverlay>();
+            }
+        }
+    }
+
+    private bool _ambientOcclusion;
+
     public override void Initialize()
     {
         base.Initialize();
 
         SubscribeLocalEvent<GetClearColorEvent>(OnClearColor);
 
+        _cfgManager.OnValueChanged(CCVars.AmbientOcclusion, val =>
+        {
+            AmbientOcclusion = val;
+        }, true);
+
         _overlayMan.AddOverlay(new BeforeLightTargetOverlay());
         _overlayMan.AddOverlay(new RoofOverlay(EntityManager));
         _overlayMan.AddOverlay(new TileEmissionOverlay(EntityManager));
index 6e43466c24fed8eebab766f9506cc7e367cb74fe..9be4bfe4c4d961d58c796b89d3186f28354b7f1f 100644 (file)
@@ -62,6 +62,8 @@ public sealed class RoofOverlay : Overlay
         worldHandle.RenderInRenderTarget(target,
             () =>
             {
+                var invMatrix = target.GetWorldToLocalMatrix(eye, scale);
+
                 for (var i = 0; i < _grids.Count; i++)
                 {
                     var grid = _grids[i];
@@ -69,8 +71,6 @@ public sealed class RoofOverlay : Overlay
                     if (!_entManager.TryGetComponent(grid.Owner, out ImplicitRoofComponent? roof))
                         continue;
 
-                    var invMatrix = target.GetWorldToLocalMatrix(eye, scale);
-
                     var gridMatrix = _xformSystem.GetWorldMatrix(grid.Owner);
                     var matty = Matrix3x2.Multiply(gridMatrix, invMatrix);
 
@@ -94,13 +94,13 @@ public sealed class RoofOverlay : Overlay
         worldHandle.RenderInRenderTarget(target,
             () =>
             {
+                var invMatrix = target.GetWorldToLocalMatrix(eye, scale);
+
                 foreach (var grid in _grids)
                 {
                     if (!_entManager.TryGetComponent(grid.Owner, out RoofComponent? roof))
                         continue;
 
-                    var invMatrix = target.GetWorldToLocalMatrix(eye, scale);
-
                     var gridMatrix = _xformSystem.GetWorldMatrix(grid.Owner);
                     var matty = Matrix3x2.Multiply(gridMatrix, invMatrix);
 
index f1b9743cad385b542b6cfd7dd5cd2de522c2299f..29279d17333bf868491f47995c475d0d34c94db0 100644 (file)
@@ -14,6 +14,7 @@
                 <ui:OptionDropDown Name="DropDownLightingQuality" Title="{Loc 'ui-options-lighting-label'}" />
                 <CheckBox Name="ViewportLowResCheckBox" Text="{Loc 'ui-options-vp-low-res'}" />
                 <CheckBox Name="ParallaxLowQualityCheckBox" Text="{Loc 'ui-options-parallax-low-quality'}" />
+                <CheckBox Name="AmbientOcclusionCheckBox" Text="{Loc 'ui-options-ambient-occlusion'}" />
 
                 <!-- Interface -->
                 <Label Text="{Loc 'ui-options-interface-label'}" StyleClasses="LabelKeyText"/>
index f53a2edd95793bcee7365f18ec7216abe91e9e8d..efd788c58ade026e840985a2d8f9235ef1934ba3 100644 (file)
@@ -20,6 +20,7 @@ public sealed partial class GraphicsTab : Control
         RobustXamlLoader.Load(this);
 
         Control.AddOptionCheckBox(CVars.DisplayVSync, VSyncCheckBox);
+        Control.AddOptionCheckBox(CCVars.AmbientOcclusion, AmbientOcclusionCheckBox);
         Control.AddOption(new OptionFullscreen(Control, _cfg, FullscreenCheckBox));
         Control.AddOption(new OptionLightingQuality(Control, _cfg, DropDownLightingQuality));
 
diff --git a/Content.Shared/CCVar/CCVars.Lighting.cs b/Content.Shared/CCVar/CCVars.Lighting.cs
new file mode 100644 (file)
index 0000000..606aadc
--- /dev/null
@@ -0,0 +1,21 @@
+using Robust.Shared.Configuration;
+
+namespace Content.Shared.CCVar;
+
+public sealed partial class CCVars
+{
+    public static readonly CVarDef<bool> AmbientOcclusion =
+        CVarDef.Create("light.ambient_occlusion", true, CVar.CLIENTONLY | CVar.ARCHIVE);
+
+    /// <summary>
+    /// Distance in world-pixels of ambient occlusion.
+    /// </summary>
+    public static readonly CVarDef<string> AmbientOcclusionColor =
+        CVarDef.Create("light.ambient_occlusion_color", "#04080FAA", CVar.CLIENTONLY);
+
+    /// <summary>
+    /// Distance in world-pixels of ambient occlusion.
+    /// </summary>
+    public static readonly CVarDef<float> AmbientOcclusionDistance =
+        CVarDef.Create("light.ambient_occlusion_distance", 4f, CVar.CLIENTONLY);
+}
index 027b65667b2ca861c3849eb600362959d3c597a2..64f9428a139f0df9bcabb0cf75a0ad324d4764c7 100644 (file)
@@ -98,6 +98,7 @@ ui-options-vp-vertical-fit-tooltip = When enabled, the main viewport will ignore
                                      will cause the viewport to be cut off on the horizontal axis.
 ui-options-vp-low-res = Low-resolution viewport
 ui-options-parallax-low-quality = Low-quality Parallax (background)
+ui-options-ambient-occlusion = Show Ambient Occlusion
 ui-options-fps-counter = Show FPS counter
 ui-options-vp-width = Viewport width:
 ui-options-hud-layout = HUD layout:
index e1a8bdf6bc42a6b9708d1e6977b0f4a38a1cd67f..81122a30c43913217c74ea60690ea1c1626a5b85 100644 (file)
@@ -7,6 +7,7 @@
     op: Replace
     func: Always
 
+# Draws to the stencil buffer if the alpha is not set to 0.
 - type: shader
   id: StencilMask
   kind: source
@@ -16,6 +17,7 @@
     op: Replace
     func: Always
 
+# Draws if the texture in the stencil buffer is not equal to white.
 - type: shader
   id: StencilDraw
   kind: canvas
     ref: 1
     op: Keep
     func: NotEqual
+
+# Draws if the texture in the stencil buffer is equal to white.
+- type: shader
+  id: StencilEqualDraw
+  kind: canvas
+  stencil:
+    ref: 1
+    op: Keep
+    func: Equal