From 60341cb245ad3dfb605cd5d30ffd184676ef590a Mon Sep 17 00:00:00 2001
From: metalgearsloth <31366439+metalgearsloth@users.noreply.github.com>
Date: Tue, 24 Jun 2025 17:56:14 +1000
Subject: [PATCH] Add wall-based ambient occlusion (#38276)
* Add wall ambient occlusion
* wawawewa
* Work
* cvars
* Comment to make slart happy
---
.../Light/AmbientOcclusionOverlay.cs | 137 ++++++++++++++++++
.../Light/EntitySystems/PlanetLightSystem.cs | 34 +++++
Content.Client/Light/RoofOverlay.cs | 8 +-
.../Options/UI/Tabs/GraphicsTab.xaml | 1 +
.../Options/UI/Tabs/GraphicsTab.xaml.cs | 1 +
Content.Shared/CCVar/CCVars.Lighting.cs | 21 +++
.../en-US/escape-menu/ui/options-menu.ftl | 1 +
Resources/Prototypes/Shaders/Stencils.yml | 11 ++
8 files changed, 210 insertions(+), 4 deletions(-)
create mode 100644 Content.Client/Light/AmbientOcclusionOverlay.cs
create mode 100644 Content.Shared/CCVar/CCVars.Lighting.cs
diff --git a/Content.Client/Light/AmbientOcclusionOverlay.cs b/Content.Client/Light/AmbientOcclusionOverlay.cs
new file mode 100644
index 0000000000..e24ee73bf4
--- /dev/null
+++ b/Content.Client/Light/AmbientOcclusionOverlay.cs
@@ -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;
+
+///
+/// Applies ambient-occlusion to the viewport.
+///
+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();
+ var lookups = _entManager.System();
+ var query = _entManager.System();
+ var xformSystem = _entManager.System();
+ 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("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("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("StencilMask").Instance());
+ worldHandle.DrawTextureRect(_aoStencilTarget!.Texture, worldBounds);
+
+ // Draw the Blurred AO texture finally.
+ worldHandle.UseShader(_proto.Index("StencilEqualDraw").Instance());
+ worldHandle.DrawTextureRect(_aoTarget!.Texture, worldBounds, color);
+
+ args.WorldHandle.SetTransform(Matrix3x2.Identity);
+ args.WorldHandle.UseShader(null);
+ }
+}
diff --git a/Content.Client/Light/EntitySystems/PlanetLightSystem.cs b/Content.Client/Light/EntitySystems/PlanetLightSystem.cs
index cbe2f47f78..deabadaddf 100644
--- a/Content.Client/Light/EntitySystems/PlanetLightSystem.cs
+++ b/Content.Client/Light/EntitySystems/PlanetLightSystem.cs
@@ -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!;
+ ///
+ /// Enables / disables the ambient occlusion overlay.
+ ///
+ public bool AmbientOcclusion
+ {
+ get => _ambientOcclusion;
+ set
+ {
+ if (_ambientOcclusion == value)
+ return;
+
+ _ambientOcclusion = value;
+
+ if (value)
+ {
+ _overlayMan.AddOverlay(new AmbientOcclusionOverlay());
+ }
+ else
+ {
+ _overlayMan.RemoveOverlay();
+ }
+ }
+ }
+
+ private bool _ambientOcclusion;
+
public override void Initialize()
{
base.Initialize();
SubscribeLocalEvent(OnClearColor);
+ _cfgManager.OnValueChanged(CCVars.AmbientOcclusion, val =>
+ {
+ AmbientOcclusion = val;
+ }, true);
+
_overlayMan.AddOverlay(new BeforeLightTargetOverlay());
_overlayMan.AddOverlay(new RoofOverlay(EntityManager));
_overlayMan.AddOverlay(new TileEmissionOverlay(EntityManager));
diff --git a/Content.Client/Light/RoofOverlay.cs b/Content.Client/Light/RoofOverlay.cs
index 6e43466c24..9be4bfe4c4 100644
--- a/Content.Client/Light/RoofOverlay.cs
+++ b/Content.Client/Light/RoofOverlay.cs
@@ -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);
diff --git a/Content.Client/Options/UI/Tabs/GraphicsTab.xaml b/Content.Client/Options/UI/Tabs/GraphicsTab.xaml
index f1b9743cad..29279d1733 100644
--- a/Content.Client/Options/UI/Tabs/GraphicsTab.xaml
+++ b/Content.Client/Options/UI/Tabs/GraphicsTab.xaml
@@ -14,6 +14,7 @@
+
diff --git a/Content.Client/Options/UI/Tabs/GraphicsTab.xaml.cs b/Content.Client/Options/UI/Tabs/GraphicsTab.xaml.cs
index f53a2edd95..efd788c58a 100644
--- a/Content.Client/Options/UI/Tabs/GraphicsTab.xaml.cs
+++ b/Content.Client/Options/UI/Tabs/GraphicsTab.xaml.cs
@@ -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
index 0000000000..606aadcf05
--- /dev/null
+++ b/Content.Shared/CCVar/CCVars.Lighting.cs
@@ -0,0 +1,21 @@
+using Robust.Shared.Configuration;
+
+namespace Content.Shared.CCVar;
+
+public sealed partial class CCVars
+{
+ public static readonly CVarDef AmbientOcclusion =
+ CVarDef.Create("light.ambient_occlusion", true, CVar.CLIENTONLY | CVar.ARCHIVE);
+
+ ///
+ /// Distance in world-pixels of ambient occlusion.
+ ///
+ public static readonly CVarDef AmbientOcclusionColor =
+ CVarDef.Create("light.ambient_occlusion_color", "#04080FAA", CVar.CLIENTONLY);
+
+ ///
+ /// Distance in world-pixels of ambient occlusion.
+ ///
+ public static readonly CVarDef AmbientOcclusionDistance =
+ CVarDef.Create("light.ambient_occlusion_distance", 4f, CVar.CLIENTONLY);
+}
diff --git a/Resources/Locale/en-US/escape-menu/ui/options-menu.ftl b/Resources/Locale/en-US/escape-menu/ui/options-menu.ftl
index 027b65667b..64f9428a13 100644
--- a/Resources/Locale/en-US/escape-menu/ui/options-menu.ftl
+++ b/Resources/Locale/en-US/escape-menu/ui/options-menu.ftl
@@ -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:
diff --git a/Resources/Prototypes/Shaders/Stencils.yml b/Resources/Prototypes/Shaders/Stencils.yml
index e1a8bdf6bc..81122a30c4 100644
--- a/Resources/Prototypes/Shaders/Stencils.yml
+++ b/Resources/Prototypes/Shaders/Stencils.yml
@@ -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
@@ -23,3 +25,12 @@
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
--
2.51.2