using System.Numerics; using Content.Client.Graphics; 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 { private static readonly ProtoId UnshadedShader = "unshaded"; private static readonly ProtoId StencilMaskShader = "StencilMask"; private static readonly ProtoId StencilEqualDrawShader = "StencilEqualDraw"; [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!; public override OverlaySpace Space => OverlaySpace.WorldSpaceBelowEntities; private readonly OverlayResourceCache _resources = new (); 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 turfSystem = _entManager.System(); var invMatrix = args.Viewport.GetWorldToLocalMatrix(); var res = _resources.GetForViewport(args.Viewport, static _ => new CachedResources()); if (res.AOTarget?.Texture.Size != target.Size) { res.AOTarget?.Dispose(); res.AOTarget = _clyde.CreateRenderTarget(target.Size, new RenderTargetFormatParameters(RenderTargetColorFormat.Rgba8Srgb), name: "ambient-occlusion-target"); } if (res.AOBlurBuffer?.Texture.Size != target.Size) { res.AOBlurBuffer?.Dispose(); res.AOBlurBuffer = _clyde.CreateRenderTarget(target.Size, new RenderTargetFormatParameters(RenderTargetColorFormat.Rgba8Srgb), name: "ambient-occlusion-blur-target"); } if (res.AOStencilTarget?.Texture.Size != target.Size) { res.AOStencilTarget?.Dispose(); res.AOStencilTarget = _clyde.CreateRenderTarget(target.Size, new RenderTargetFormatParameters(RenderTargetColorFormat.Rgba8Srgb), name: "ambient-occlusion-stencil-target"); } // Draw the texture data to the texture. args.WorldHandle.RenderInRenderTarget(res.AOTarget, () => { worldHandle.UseShader(_proto.Index(UnshadedShader).Instance()); var invMatrix = res.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, res.AOTarget, res.AOBlurBuffer, 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(res.AOStencilTarget, () => { // Don't want lighting affecting it. worldHandle.UseShader(_proto.Index(UnshadedShader).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 (turfSystem.IsSpace(tileRef)) 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(StencilMaskShader).Instance()); worldHandle.DrawTextureRect(res.AOStencilTarget!.Texture, worldBounds); // Draw the Blurred AO texture finally. worldHandle.UseShader(_proto.Index(StencilEqualDrawShader).Instance()); worldHandle.DrawTextureRect(res.AOTarget!.Texture, worldBounds, color); args.WorldHandle.SetTransform(Matrix3x2.Identity); args.WorldHandle.UseShader(null); } protected override void DisposeBehavior() { _resources.Dispose(); base.DisposeBehavior(); } private sealed class CachedResources : IDisposable { public IRenderTexture? AOTarget; public IRenderTexture? AOBlurBuffer; // Couldn't figure out a way to avoid this so if you can then please do. public IRenderTexture? AOStencilTarget; public void Dispose() { AOTarget?.Dispose(); AOBlurBuffer?.Dispose(); AOStencilTarget?.Dispose(); } } }