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 @@ +