Add wall-based ambient occlusion (#38276)

* Add wall ambient occlusion

* wawawewa

* Work

* cvars

* Comment to make slart happy
This commit is contained in:
metalgearsloth
2025-06-24 17:56:14 +10:00
committed by GitHub
parent 6a2afa5625
commit 60341cb245
8 changed files with 210 additions and 4 deletions

View File

@@ -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);
}
}

View File

@@ -1,17 +1,51 @@
using Content.Shared.CCVar;
using Robust.Client.Graphics; using Robust.Client.Graphics;
using Robust.Shared.Configuration;
namespace Content.Client.Light.EntitySystems; namespace Content.Client.Light.EntitySystems;
public sealed class PlanetLightSystem : EntitySystem public sealed class PlanetLightSystem : EntitySystem
{ {
[Dependency] private readonly IConfigurationManager _cfgManager = default!;
[Dependency] private readonly IOverlayManager _overlayMan = 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() public override void Initialize()
{ {
base.Initialize(); base.Initialize();
SubscribeLocalEvent<GetClearColorEvent>(OnClearColor); SubscribeLocalEvent<GetClearColorEvent>(OnClearColor);
_cfgManager.OnValueChanged(CCVars.AmbientOcclusion, val =>
{
AmbientOcclusion = val;
}, true);
_overlayMan.AddOverlay(new BeforeLightTargetOverlay()); _overlayMan.AddOverlay(new BeforeLightTargetOverlay());
_overlayMan.AddOverlay(new RoofOverlay(EntityManager)); _overlayMan.AddOverlay(new RoofOverlay(EntityManager));
_overlayMan.AddOverlay(new TileEmissionOverlay(EntityManager)); _overlayMan.AddOverlay(new TileEmissionOverlay(EntityManager));

View File

@@ -62,6 +62,8 @@ public sealed class RoofOverlay : Overlay
worldHandle.RenderInRenderTarget(target, worldHandle.RenderInRenderTarget(target,
() => () =>
{ {
var invMatrix = target.GetWorldToLocalMatrix(eye, scale);
for (var i = 0; i < _grids.Count; i++) for (var i = 0; i < _grids.Count; i++)
{ {
var grid = _grids[i]; var grid = _grids[i];
@@ -69,8 +71,6 @@ public sealed class RoofOverlay : Overlay
if (!_entManager.TryGetComponent(grid.Owner, out ImplicitRoofComponent? roof)) if (!_entManager.TryGetComponent(grid.Owner, out ImplicitRoofComponent? roof))
continue; continue;
var invMatrix = target.GetWorldToLocalMatrix(eye, scale);
var gridMatrix = _xformSystem.GetWorldMatrix(grid.Owner); var gridMatrix = _xformSystem.GetWorldMatrix(grid.Owner);
var matty = Matrix3x2.Multiply(gridMatrix, invMatrix); var matty = Matrix3x2.Multiply(gridMatrix, invMatrix);
@@ -94,13 +94,13 @@ public sealed class RoofOverlay : Overlay
worldHandle.RenderInRenderTarget(target, worldHandle.RenderInRenderTarget(target,
() => () =>
{ {
var invMatrix = target.GetWorldToLocalMatrix(eye, scale);
foreach (var grid in _grids) foreach (var grid in _grids)
{ {
if (!_entManager.TryGetComponent(grid.Owner, out RoofComponent? roof)) if (!_entManager.TryGetComponent(grid.Owner, out RoofComponent? roof))
continue; continue;
var invMatrix = target.GetWorldToLocalMatrix(eye, scale);
var gridMatrix = _xformSystem.GetWorldMatrix(grid.Owner); var gridMatrix = _xformSystem.GetWorldMatrix(grid.Owner);
var matty = Matrix3x2.Multiply(gridMatrix, invMatrix); var matty = Matrix3x2.Multiply(gridMatrix, invMatrix);

View File

@@ -14,6 +14,7 @@
<ui:OptionDropDown Name="DropDownLightingQuality" Title="{Loc 'ui-options-lighting-label'}" /> <ui:OptionDropDown Name="DropDownLightingQuality" Title="{Loc 'ui-options-lighting-label'}" />
<CheckBox Name="ViewportLowResCheckBox" Text="{Loc 'ui-options-vp-low-res'}" /> <CheckBox Name="ViewportLowResCheckBox" Text="{Loc 'ui-options-vp-low-res'}" />
<CheckBox Name="ParallaxLowQualityCheckBox" Text="{Loc 'ui-options-parallax-low-quality'}" /> <CheckBox Name="ParallaxLowQualityCheckBox" Text="{Loc 'ui-options-parallax-low-quality'}" />
<CheckBox Name="AmbientOcclusionCheckBox" Text="{Loc 'ui-options-ambient-occlusion'}" />
<!-- Interface --> <!-- Interface -->
<Label Text="{Loc 'ui-options-interface-label'}" StyleClasses="LabelKeyText"/> <Label Text="{Loc 'ui-options-interface-label'}" StyleClasses="LabelKeyText"/>

View File

@@ -20,6 +20,7 @@ public sealed partial class GraphicsTab : Control
RobustXamlLoader.Load(this); RobustXamlLoader.Load(this);
Control.AddOptionCheckBox(CVars.DisplayVSync, VSyncCheckBox); Control.AddOptionCheckBox(CVars.DisplayVSync, VSyncCheckBox);
Control.AddOptionCheckBox(CCVars.AmbientOcclusion, AmbientOcclusionCheckBox);
Control.AddOption(new OptionFullscreen(Control, _cfg, FullscreenCheckBox)); Control.AddOption(new OptionFullscreen(Control, _cfg, FullscreenCheckBox));
Control.AddOption(new OptionLightingQuality(Control, _cfg, DropDownLightingQuality)); Control.AddOption(new OptionLightingQuality(Control, _cfg, DropDownLightingQuality));

View File

@@ -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);
}

View 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. will cause the viewport to be cut off on the horizontal axis.
ui-options-vp-low-res = Low-resolution viewport ui-options-vp-low-res = Low-resolution viewport
ui-options-parallax-low-quality = Low-quality Parallax (background) 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-fps-counter = Show FPS counter
ui-options-vp-width = Viewport width: ui-options-vp-width = Viewport width:
ui-options-hud-layout = HUD layout: ui-options-hud-layout = HUD layout:

View File

@@ -7,6 +7,7 @@
op: Replace op: Replace
func: Always func: Always
# Draws to the stencil buffer if the alpha is not set to 0.
- type: shader - type: shader
id: StencilMask id: StencilMask
kind: source kind: source
@@ -16,6 +17,7 @@
op: Replace op: Replace
func: Always func: Always
# Draws if the texture in the stencil buffer is not equal to white.
- type: shader - type: shader
id: StencilDraw id: StencilDraw
kind: canvas kind: canvas
@@ -23,3 +25,12 @@
ref: 1 ref: 1
op: Keep op: Keep
func: NotEqual 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