diff --git a/Content.Client/Light/AmbientOcclusionOverlay.cs b/Content.Client/Light/AmbientOcclusionOverlay.cs index a08b9074d8..b56ee5cc60 100644 --- a/Content.Client/Light/AmbientOcclusionOverlay.cs +++ b/Content.Client/Light/AmbientOcclusionOverlay.cs @@ -1,12 +1,14 @@ using System.Numerics; using System.Runtime.InteropServices; using Content.Client.Graphics; +using Content.Client.Light.EntitySystems; using Content.Shared.CCVar; -using Content.Shared.Maps; +using Content.Shared.Light.Components; +using Robust.Shared.ComponentTrees; using Robust.Client.Graphics; using Robust.Shared.Configuration; using Robust.Shared.Enums; -using Robust.Shared.Map.Components; +using Robust.Shared.Physics; using Robust.Shared.Prototypes; using Robust.Shared.Utility; @@ -20,32 +22,42 @@ public sealed partial class AmbientOcclusionOverlay : Overlay private static readonly ProtoId UnshadedShader = "unshaded"; private static readonly ProtoId StencilMaskShader = "StencilMask"; private static readonly ProtoId StencilEqualDrawShader = "StencilEqualDraw"; + private const float BlurMultiplier = 7f; [Dependency] private IClyde _clyde = default!; [Dependency] private IConfigurationManager _cfgManager = default!; [Dependency] private IEntityManager _entManager = default!; [Dependency] private IPrototypeManager _proto = default!; - private List> _cachedGrids = new(); - private readonly List> _cachedOccluders = new(); private readonly List _aoVertices = new(4096); private readonly List _aoIndices = new(6144); public override OverlaySpace Space => OverlaySpace.WorldSpaceBelowEntities; private readonly OverlayResourceCache _resources = new (); + private readonly OccluderSystem _occluders; + private readonly GridStencilSystem _gridStencil; + private readonly SharedTransformSystem _xformSystem; + + private Color _color; public AmbientOcclusionOverlay() { IoCManager.InjectDependencies(this); ZIndex = AfterLightTargetOverlay.ContentZIndex + 1; + + _occluders = _entManager.System(); + _gridStencil = _entManager.System(); + _xformSystem = _entManager.System(); + + _cfgManager.OnValueChanged(CCVars.AmbientOcclusionColor, OnColorChanged, true); } protected override void Draw(in OverlayDrawArgs args) { /* * tl;dr - * - we draw a black square on each "ambient occlusion" entity. + * - we draw each occluder's polygon to an AO source texture. * - we blur this. * - We apply it to the viewport. * @@ -58,36 +70,29 @@ public sealed partial class AmbientOcclusionOverlay : Overlay var mapId = args.MapId; var worldBounds = args.WorldBounds; var worldHandle = args.WorldHandle; - var color = Color.FromHex(_cfgManager.GetCVar(CCVars.AmbientOcclusionColor)); - //var color = Color.Red; + var distance = _cfgManager.GetCVar(CCVars.AmbientOcclusionDistance); + var resolutionScale = Math.Clamp(_cfgManager.GetCVar(CCVars.AmbientOcclusionResolutionScale), 0.1f, 1f); var target = viewport.RenderTarget; - var lightScale = target.Size / (Vector2) viewport.Size; + var aoSize = new Vector2i( + Math.Max(1, (int) MathF.Ceiling(target.Size.X * resolutionScale)), + Math.Max(1, (int) MathF.Ceiling(target.Size.Y * resolutionScale))); + var lightScale = aoSize / (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 expandedBounds = worldBounds.Enlarged(GetBlurMargin(viewport, distance)); + var polygonExpansion = distance / EyeManager.PixelsPerMeter; var res = _resources.GetForViewport(args.Viewport, static _ => new CachedResources()); - if (res.AOTarget?.Texture.Size != target.Size) + if (res.AOTarget?.Texture.Size != aoSize) { res.AOTarget?.Dispose(); - res.AOTarget = _clyde.CreateRenderTarget(target.Size, new RenderTargetFormatParameters(RenderTargetColorFormat.Rgba8Srgb), name: "ambient-occlusion-target"); + res.AOTarget = _clyde.CreateRenderTarget(aoSize, new RenderTargetFormatParameters(RenderTargetColorFormat.Rgba8Srgb), name: "ambient-occlusion-target"); } - if (res.AOBlurBuffer?.Texture.Size != target.Size) + if (res.AOBlurBuffer?.Texture.Size != aoSize) { 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"); + res.AOBlurBuffer = _clyde.CreateRenderTarget(aoSize, new RenderTargetFormatParameters(RenderTargetColorFormat.Rgba8Srgb), name: "ambient-occlusion-blur-target"); } // Draw the texture data to the texture. @@ -97,56 +102,29 @@ public sealed partial class AmbientOcclusionOverlay : Overlay worldHandle.UseShader(_proto.Index(UnshadedShader).Instance()); worldHandle.SetTransform(Matrix3x2.Identity); var worldToTargetMatrix = res.AOTarget.GetWorldToLocalMatrix(viewport.Eye!, scale); + var state = new AmbientOcclusionQueryState(this, worldHandle, worldToTargetMatrix, polygonExpansion); - _cachedOccluders.Clear(); - query.QueryAabb(_cachedOccluders, mapId, worldBounds); - - foreach (var entry in _cachedOccluders) + _occluders.QueryAabb(ref state, static (ref AmbientOcclusionQueryState state, in ComponentTreeEntry entry) => { - DebugTools.Assert(entry.Comp1.Enabled); - var matrix = xformSystem.GetWorldMatrix(entry.Comp2); - var localToTargetMatrix = Matrix3x2.Multiply(matrix, worldToTargetMatrix); - AppendAmbientOcclusionPolygon(worldHandle, entry.Comp1.Polygon, localToTargetMatrix); - } + state.Overlay.AppendAmbientOcclusionPolygon(entry, ref state); + return true; + }, mapId, expandedBounds); FlushAmbientOcclusionPolygons(worldHandle); }, 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()); - - _cachedGrids.Clear(); - maps.FindGridsIntersecting(mapId, worldBounds, ref _cachedGrids); - foreach (var grid in _cachedGrids) - { - 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.Comp.TileSize); - worldHandle.DrawRect(bounds, Color.White); - } - } - - }, Color.Transparent); + _clyde.BlurRenderTarget(viewport, res.AOTarget, res.AOBlurBuffer, viewport.Eye!, BlurMultiplier); // Draw the stencil texture to depth buffer. + var stencil = _gridStencil.GetNonSpaceStencil(args); worldHandle.UseShader(_proto.Index(StencilMaskShader).Instance()); - worldHandle.DrawTextureRect(res.AOStencilTarget!.Texture, worldBounds); + worldHandle.DrawTextureRect(stencil.Texture, worldBounds); // Draw the Blurred AO texture finally. + var color = _entManager.TryGetComponent(args.MapUid, out MapAmbientColorComponent? mapAmbient) + ? mapAmbient.Color + : _color; + worldHandle.UseShader(_proto.Index(StencilEqualDrawShader).Instance()); worldHandle.DrawTextureRect(res.AOTarget!.Texture, worldBounds, color); @@ -154,17 +132,72 @@ public sealed partial class AmbientOcclusionOverlay : Overlay args.WorldHandle.UseShader(null); } + private void OnColorChanged(string value) + { + _color = Color.FromHex(value); + } + + private static float GetBlurMargin(IClydeViewport viewport, float distance) + { + if (viewport.Eye == null) + return distance / EyeManager.PixelsPerMeter; + + var cameraSize = viewport.Eye.Zoom.Y * viewport.Size.Y * (1 / viewport.RenderScale.Y) / EyeManager.PixelsPerMeter; + + // Matches Clyde's BlurRenderTarget radius calculation closely enough to include off-screen AO contributors. + return distance / EyeManager.PixelsPerMeter + BlurMultiplier / cameraSize; + } + protected override void DisposeBehavior() { + _cfgManager.UnsubValueChanged(CCVars.AmbientOcclusionColor, OnColorChanged); _resources.Dispose(); base.DisposeBehavior(); } + private void AppendAmbientOcclusionPolygon( + in ComponentTreeEntry entry, + ref AmbientOcclusionQueryState state) + { + DebugTools.Assert(entry.Component.Enabled); + + var localToTargetMatrix = GetLocalToTargetMatrix(entry, ref state); + + AppendAmbientOcclusionPolygon( + state.WorldHandle, + entry.Component.Polygon, + localToTargetMatrix, + state.Expansion); + } + + private Matrix3x2 GetLocalToTargetMatrix( + in ComponentTreeEntry entry, + ref AmbientOcclusionQueryState state) + { + // OccluderSystem's tree invariant is that occluders are parented directly to their map/grid tree. + // In that case LocalMatrix is already local-to-tree, so avoid resolving a recursive world matrix per occluder. + if (entry.Transform.ParentUid == entry.Component.TreeUid) + { + if (state.TreeUid != entry.Transform.ParentUid) + { + state.TreeUid = entry.Transform.ParentUid; + state.TreeToTargetMatrix = Matrix3x2.Multiply( + _xformSystem.GetWorldMatrix(entry.Transform.ParentUid), + state.WorldToTargetMatrix); + } + + return Matrix3x2.Multiply(entry.Transform.LocalMatrix, state.TreeToTargetMatrix); + } + + return Matrix3x2.Multiply(_xformSystem.GetWorldMatrix(entry.Transform), state.WorldToTargetMatrix); + } + private void AppendAmbientOcclusionPolygon( DrawingHandleWorld worldHandle, ReadOnlySpan polygon, - Matrix3x2 localToTargetMatrix) + Matrix3x2 localToTargetMatrix, + float expansion) { if (polygon.Length < 3) return; @@ -174,10 +207,27 @@ public sealed partial class AmbientOcclusionOverlay : Overlay FlushAmbientOcclusionPolygons(worldHandle); var indexBase = (ushort) _aoVertices.Count; + var center = Vector2.Zero; for (var i = 0; i < polygon.Length; i++) { - _aoVertices.Add(Vector2.Transform(polygon[i], localToTargetMatrix)); + center += polygon[i]; + } + + center /= polygon.Length; + + for (var i = 0; i < polygon.Length; i++) + { + var vertex = polygon[i]; + + if (expansion > 0f) + { + var offset = vertex - center; + if (offset.LengthSquared() > 0f) + vertex += Vector2.Normalize(offset) * expansion; + } + + _aoVertices.Add(Vector2.Transform(vertex, localToTargetMatrix)); } for (var i = 1; i < polygon.Length - 1; i++) @@ -203,19 +253,40 @@ public sealed partial class AmbientOcclusionOverlay : Overlay _aoIndices.Clear(); } + private struct AmbientOcclusionQueryState + { + public AmbientOcclusionOverlay Overlay; + public DrawingHandleWorld WorldHandle; + public Matrix3x2 WorldToTargetMatrix; + public float Expansion; + public EntityUid? TreeUid; + public Matrix3x2 TreeToTargetMatrix; + + public AmbientOcclusionQueryState( + AmbientOcclusionOverlay overlay, + DrawingHandleWorld worldHandle, + Matrix3x2 worldToTargetMatrix, + float expansion) + { + Overlay = overlay; + WorldHandle = worldHandle; + WorldToTargetMatrix = worldToTargetMatrix; + Expansion = expansion; + TreeUid = null; + TreeToTargetMatrix = Matrix3x2.Identity; + } + } + 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(); } } } + diff --git a/Content.Client/Light/EntitySystems/GridStencilSystem.cs b/Content.Client/Light/EntitySystems/GridStencilSystem.cs new file mode 100644 index 0000000000..cf5108d2d4 --- /dev/null +++ b/Content.Client/Light/EntitySystems/GridStencilSystem.cs @@ -0,0 +1,217 @@ +using System.Numerics; +using System.Runtime.InteropServices; +using Content.Client.Graphics; +using Content.Shared.Maps; +using Robust.Client.Graphics; +using Robust.Shared.Enums; +using Robust.Shared.Map; +using Robust.Shared.Map.Components; +using Robust.Shared.Timing; + +namespace Content.Client.Light.EntitySystems; + +/// +/// Builds viewport-sized masks for grid contents that need to be reused by world overlays. +/// +public sealed partial class GridStencilSystem : EntitySystem +{ + public delegate bool TileStencilPredicate(Entity grid, TileRef tile); + + [Dependency] private IClyde _clyde = default!; + [Dependency] private IGameTiming _timing = default!; + [Dependency] private EntityLookupSystem _lookup = default!; + [Dependency] private SharedMapSystem _map = default!; + [Dependency] private SharedTransformSystem _xform = default!; + [Dependency] private TurfSystem _turf = default!; + + private readonly OverlayResourceCache _resources = new(); + private List> _grids = new(); + private readonly List _rects = new(); + + /// + /// Returns a viewport-sized texture where non-space grid tiles are white and everything else is transparent. + /// The texture is rebuilt at most once per viewport per frame. + /// + public IRenderTexture GetNonSpaceStencil(in OverlayDrawArgs args) + { + return GetTileBoundsStencil(args, + "non-space", + "non-space-grid-stencil", + tile => !_turf.IsSpace(tile)); + } + + private IRenderTexture GetTileBoundsStencil( + in OverlayDrawArgs args, + string key, + string name, + Predicate predicate) + { + var viewport = args.Viewport; + var target = viewport.RenderTarget; + var res = _resources.GetForViewport(viewport, static _ => new CachedResources()); + var targetRes = res.GetOrCreate(key); + + if (targetRes.Target?.Texture.Size != target.Size) + { + targetRes.Target?.Dispose(); + targetRes.Target = _clyde.CreateRenderTarget(target.Size, + new RenderTargetFormatParameters(RenderTargetColorFormat.Rgba8Srgb), + name: name); + targetRes.LastFrame = 0; + } + + var worldHandle = args.WorldHandle; + var invMatrix = viewport.GetWorldToLocalMatrix(); + var mapId = args.MapId; + var worldBounds = args.WorldBounds; + + if (targetRes.LastFrame == _timing.CurFrame && + targetRes.LastMapId == mapId && + targetRes.LastWorldBounds.Equals(worldBounds)) + { + return targetRes.Target!; + } + + targetRes.LastFrame = _timing.CurFrame; + targetRes.LastMapId = mapId; + targetRes.LastWorldBounds = worldBounds; + + worldHandle.RenderInRenderTarget(targetRes.Target, + () => + { + _grids.Clear(); + _map.FindGridsIntersecting(mapId, worldBounds, ref _grids); + + foreach (var grid in _grids) + { + var worldToTextureMatrix = Matrix3x2.Multiply(_xform.GetWorldMatrix(grid.Owner), invMatrix); + var tiles = _map.GetTilesEnumerator(grid.Owner, grid, worldBounds); + worldHandle.SetTransform(worldToTextureMatrix); + _rects.Clear(); + + while (tiles.MoveNext(out var tileRef)) + { + if (!predicate(tileRef.Tile)) + continue; + + var bounds = _lookup.GetLocalBounds(tileRef, grid.Comp.TileSize); + _rects.Add(new WorldRect(bounds, Color.White)); + } + + worldHandle.DrawRectsUnmodulated(CollectionsMarshal.AsSpan(_rects)); + } + }, + Color.Transparent); + + return targetRes.Target!; + } + + /// + /// Returns a viewport-sized texture where tiles accepted by are white and everything else is transparent. + /// + public IRenderTexture GetTileStencil( + in OverlayDrawArgs args, + string key, + string name, + TileStencilPredicate predicate) + { + var viewport = args.Viewport; + var target = viewport.RenderTarget; + var res = _resources.GetForViewport(viewport, static _ => new CachedResources()); + var targetRes = res.GetOrCreate(key); + + if (targetRes.Target?.Texture.Size != target.Size) + { + targetRes.Target?.Dispose(); + targetRes.Target = _clyde.CreateRenderTarget(target.Size, + new RenderTargetFormatParameters(RenderTargetColorFormat.Rgba8Srgb), + name: name); + targetRes.LastFrame = 0; + } + + var worldHandle = args.WorldHandle; + var invMatrix = viewport.GetWorldToLocalMatrix(); + var mapId = args.MapId; + var worldBounds = args.WorldBounds; + + if (targetRes.LastFrame == _timing.CurFrame && + targetRes.LastMapId == mapId && + targetRes.LastWorldBounds.Equals(worldBounds)) + { + return targetRes.Target!; + } + + targetRes.LastFrame = _timing.CurFrame; + targetRes.LastMapId = mapId; + targetRes.LastWorldBounds = worldBounds; + + worldHandle.RenderInRenderTarget(targetRes.Target, + () => + { + _grids.Clear(); + _map.FindGridsIntersecting(mapId, worldBounds, ref _grids); + + foreach (var grid in _grids) + { + var transform = _xform.GetWorldMatrix(grid.Owner); + var worldToTextureMatrix = Matrix3x2.Multiply(transform, invMatrix); + var tiles = _map.GetTilesEnumerator(grid.Owner, grid, worldBounds); + worldHandle.SetTransform(worldToTextureMatrix); + _rects.Clear(); + + while (tiles.MoveNext(out var tileRef)) + { + if (!predicate(grid, tileRef)) + continue; + + var bounds = _lookup.GetLocalBounds(tileRef, grid.Comp.TileSize); + _rects.Add(new WorldRect(bounds, Color.White)); + } + + worldHandle.DrawRectsUnmodulated(CollectionsMarshal.AsSpan(_rects)); + } + }, + Color.Transparent); + + return targetRes.Target!; + } + + public override void Shutdown() + { + base.Shutdown(); + _resources.Dispose(); + } + + private sealed class CachedResources : IDisposable + { + private readonly Dictionary _targets = new(); + + public CachedTarget GetOrCreate(string key) + { + if (_targets.TryGetValue(key, out var target)) + return target; + + target = new CachedTarget(); + _targets.Add(key, target); + return target; + } + + public void Dispose() + { + foreach (var target in _targets.Values) + { + target.Target?.Dispose(); + } + + _targets.Clear(); + } + } + + private sealed class CachedTarget + { + public IRenderTexture? Target; + public uint LastFrame; + public MapId LastMapId; + public Box2Rotated LastWorldBounds; + } +} diff --git a/Content.Client/Light/EntitySystems/PlanetLightSystem.cs b/Content.Client/Light/EntitySystems/PlanetLightSystem.cs index 81dd9b32d3..7a71dfefbe 100644 --- a/Content.Client/Light/EntitySystems/PlanetLightSystem.cs +++ b/Content.Client/Light/EntitySystems/PlanetLightSystem.cs @@ -68,5 +68,6 @@ public sealed partial class PlanetLightSystem : EntitySystem _overlayMan.RemoveOverlay(); _overlayMan.RemoveOverlay(); _overlayMan.RemoveOverlay(); + _overlayMan.RemoveOverlay(); } } diff --git a/Content.Client/Overlays/StencilOverlay.Weather.cs b/Content.Client/Overlays/StencilOverlay.Weather.cs index 2d7a94828f..f54b7891ba 100644 --- a/Content.Client/Overlays/StencilOverlay.Weather.cs +++ b/Content.Client/Overlays/StencilOverlay.Weather.cs @@ -3,22 +3,16 @@ using Content.Shared.Light.Components; using Content.Shared.StatusEffectNew.Components; using Content.Shared.Weather; using Robust.Client.Graphics; -using Robust.Shared.Map.Components; namespace Content.Client.Overlays; public sealed partial class StencilOverlay { - private List> _grids = new(); - private void DrawWeather( in OverlayDrawArgs args, - CachedResources res, - HashSet> weathers, - Matrix3x2 invMatrix) + HashSet> weathers) { var worldHandle = args.WorldHandle; - var mapId = args.MapId; var worldAABB = args.WorldAABB; var worldBounds = args.WorldBounds; var position = args.Viewport.Eye?.Position.Position ?? Vector2.Zero; @@ -26,43 +20,21 @@ public sealed partial class StencilOverlay // Cut out the irrelevant bits via stencil // This is why we don't just use parallax; we might want specific tiles to get drawn over // particularly for planet maps or stations. - worldHandle.RenderInRenderTarget(res.Blep!, - () => + var stencil = _gridStencil.GetTileStencil(args, + "weather-blocked", + "weather-blocked-grid-stencil", + (grid, tile) => { - var xformQuery = _entManager.GetEntityQuery(); - _grids.Clear(); - - // idk if this is safe to cache in a field and clear sloth help - _map.FindGridsIntersecting(mapId, worldAABB, ref _grids); - - foreach (var grid in _grids) - { - var matrix = _transform.GetWorldMatrix(grid, xformQuery); - var matty = Matrix3x2.Multiply(matrix, invMatrix); - worldHandle.SetTransform(matty); - _entManager.TryGetComponent(grid.Owner, out RoofComponent? roofComp); - - foreach (var tile in _map.GetTilesIntersecting(grid.Owner, grid, worldAABB)) - { - // Ignored tiles for stencil - if (_weather.CanWeatherAffect((grid.Owner, grid, roofComp), tile)) - continue; - - var gridTile = new Box2(tile.GridIndices * grid.Comp.TileSize, - (tile.GridIndices + Vector2i.One) * grid.Comp.TileSize); - - worldHandle.DrawRect(gridTile, Color.White); - } - } - }, - Color.Transparent); + _entManager.TryGetComponent(grid.Owner, out RoofComponent? roofComp); + // Ignored tiles for stencil. + return !_weather.CanWeatherAffect((grid.Owner, grid.Comp, roofComp), tile); + }); worldHandle.SetTransform(Matrix3x2.Identity); worldHandle.UseShader(_protoManager.Index(StencilMask).Instance()); - worldHandle.DrawTextureRect(res.Blep!.Texture, worldBounds); + worldHandle.DrawTextureRect(stencil.Texture, worldBounds); var curTime = _timing.RealTime; - foreach (var (uid, weather, status) in weathers) { var alpha = _weather.GetWeatherPercent((uid, status)); diff --git a/Content.Client/Overlays/StencilOverlay.cs b/Content.Client/Overlays/StencilOverlay.cs index 86c9c68a83..efb22f5612 100644 --- a/Content.Client/Overlays/StencilOverlay.cs +++ b/Content.Client/Overlays/StencilOverlay.cs @@ -1,5 +1,6 @@ using System.Numerics; using Content.Client.Graphics; +using Content.Client.Light.EntitySystems; using Content.Client.Parallax; using Content.Client.Weather; using Content.Shared.Salvage; @@ -34,6 +35,7 @@ public sealed partial class StencilOverlay : Overlay private readonly SpriteSystem _sprite; private readonly WeatherSystem _weather; private readonly StatusEffectsSystem _statusEffects; + private GridStencilSystem _gridStencil = default!; private HashSet>? _weatherSet = new(); public override OverlaySpace Space => OverlaySpace.WorldSpaceBelowFOV; @@ -52,6 +54,7 @@ public sealed partial class StencilOverlay : Overlay _weather = weather; _statusEffects = statusEffects; IoCManager.InjectDependencies(this); + _gridStencil = _entManager.System(); _shader = _protoManager.Index(CircleShader).InstanceUnique(); } @@ -69,7 +72,7 @@ public sealed partial class StencilOverlay : Overlay } if (_statusEffects.TryEffectsWithComp(mapUid, out _weatherSet)) - DrawWeather(args, res, _weatherSet, invMatrix); + DrawWeather(args, _weatherSet); if (_entManager.TryGetComponent(mapUid, out var restrictedRangeComponent)) DrawRestrictedRange(args, res, restrictedRangeComponent, invMatrix); diff --git a/Content.Shared/CCVar/CCVars.Lighting.cs b/Content.Shared/CCVar/CCVars.Lighting.cs index 3a3fac5b11..1dd65cbcc0 100644 --- a/Content.Shared/CCVar/CCVars.Lighting.cs +++ b/Content.Shared/CCVar/CCVars.Lighting.cs @@ -16,11 +16,19 @@ public sealed partial class CCVars public static readonly CVarDef AmbientOcclusion = CVarDef.Create("light.ambient_occlusion", true, CVar.CLIENTONLY | CVar.ARCHIVE); + public static readonly Color DefaultAmbientOcclusionColor = Color.FromHex("#04080FAA"); + /// - /// Distance in world-pixels of ambient occlusion. + /// Color of ambient occlusion. /// public static readonly CVarDef AmbientOcclusionColor = - CVarDef.Create("light.ambient_occlusion_color", "#04080FAA", CVar.CLIENTONLY); + CVarDef.Create("light.ambient_occlusion_color", DefaultAmbientOcclusionColor.ToHex(), CVar.CLIENTONLY); + + /// + /// Render resolution scale for ambient occlusion. + /// + public static readonly CVarDef AmbientOcclusionResolutionScale = + CVarDef.Create("light.ambient_occlusion_resolution_scale", 0.5f, CVar.CLIENTONLY | CVar.ARCHIVE); /// /// Distance in world-pixels of ambient occlusion. diff --git a/Content.Shared/Entry/EntryPoint.cs b/Content.Shared/Entry/EntryPoint.cs index 04d737086c..5c9ab990e2 100644 --- a/Content.Shared/Entry/EntryPoint.cs +++ b/Content.Shared/Entry/EntryPoint.cs @@ -2,11 +2,9 @@ using System.Diagnostics.CodeAnalysis; using System.IO; using System.Linq; using Content.Shared.Humanoid.Markings; -using Content.Shared.Maps; using Robust.Shared; using Robust.Shared.Configuration; using Robust.Shared.ContentPack; -using Robust.Shared.Map; using Robust.Shared.Prototypes; using Robust.Shared.Serialization.Markdown; using Robust.Shared.Serialization.Markdown.Sequence; @@ -18,7 +16,6 @@ namespace Content.Shared.Entry public sealed partial class EntryPoint : GameShared { [Dependency] private IPrototypeManager _prototypeManager = default!; - [Dependency] private ITileDefinitionManager _tileDefinitionManager = default!; [Dependency] private IResourceManager _resMan = default!; #if DEBUG [Dependency] private IConfigurationManager _configurationManager = default!; @@ -31,11 +28,6 @@ namespace Content.Shared.Entry Dependencies.InjectDependencies(this); } - public override void Shutdown() - { - _prototypeManager.PrototypesReloaded -= PrototypeReload; - } - public override void Init() { IgnorePrototypes(); @@ -45,7 +37,6 @@ namespace Content.Shared.Entry { base.PostInit(); - InitTileDefinitions(); Dependencies.Resolve().Initialize(); #if DEBUG @@ -55,54 +46,6 @@ namespace Content.Shared.Entry #endif } - private void InitTileDefinitions() - { - _prototypeManager.PrototypesReloaded += PrototypeReload; - - // Register space first because I'm a hard coding hack. - var spaceDef = _prototypeManager.Index(ContentTileDefinition.SpaceID); - - _tileDefinitionManager.Register(spaceDef); - - var prototypeList = new List(); - foreach (var tileDef in _prototypeManager.EnumeratePrototypes()) - { - if (tileDef.ID == ContentTileDefinition.SpaceID) - { - continue; - } - - prototypeList.Add(tileDef); - } - - // Sort ordinal to ensure it's consistent client and server. - // So that tile IDs match up. - prototypeList.Sort((a, b) => string.Compare(a.ID, b.ID, StringComparison.Ordinal)); - - foreach (var tileDef in prototypeList) - { - _tileDefinitionManager.Register(tileDef); - } - - _tileDefinitionManager.Initialize(); - } - - private void PrototypeReload(PrototypesReloadedEventArgs obj) - { - /* I am leaving this here commented out to re-iterate - - our game is shitcode - - tiledefmanager no likey proto reloads and you must re-assign the tile ids. - if (!obj.WasModified()) - return; - */ - - // Need to re-allocate tiledefs due to how prototype reloads work - foreach (var def in _prototypeManager.EnumeratePrototypes()) - { - def.AssignTileId(_tileDefinitionManager[def.ID].TileId); - } - } - private void IgnorePrototypes() { if (!TryReadFile(out var sequences)) diff --git a/Content.Shared/Light/Components/MapAmbientColorComponent.cs b/Content.Shared/Light/Components/MapAmbientColorComponent.cs new file mode 100644 index 0000000000..457cf6783d --- /dev/null +++ b/Content.Shared/Light/Components/MapAmbientColorComponent.cs @@ -0,0 +1,17 @@ +using Content.Shared.CCVar; +using Robust.Shared.GameStates; + +namespace Content.Shared.Light.Components; + +/// +/// Controls map-specific ambient occlusion color. +/// +[RegisterComponent, NetworkedComponent, AutoGenerateComponentState] +public sealed partial class MapAmbientColorComponent : Component +{ + /// + /// Color used by ambient occlusion on this map. + /// + [DataField, AutoNetworkedField] + public Color Color = CCVars.DefaultAmbientOcclusionColor; +} diff --git a/Content.Shared/Maps/TurfSystem.TileDefinitions.cs b/Content.Shared/Maps/TurfSystem.TileDefinitions.cs new file mode 100644 index 0000000000..ce7d522d0d --- /dev/null +++ b/Content.Shared/Maps/TurfSystem.TileDefinitions.cs @@ -0,0 +1,56 @@ +using Robust.Shared.Collections; +using Robust.Shared.Prototypes; + +namespace Content.Shared.Maps; + +/// +/// Tile definition registration and reload handling for . +/// +/// +/// Tile IDs are assigned by registration order and are used directly by map chunks. Keep this initialization in a +/// TurfSystem partial so the tile atmosphere cache is always built after registration. +/// +public sealed partial class TurfSystem +{ + [Dependency] private IPrototypeManager _prototypeManager = default!; + + private void RegisterTileDefinitions() + { + if (_tileDefinitions.Count > 0) + return; + + // Register space first because ID 0 is assumed by a few map/tile paths. + var spaceDef = _prototypeManager.Index(ContentTileDefinition.SpaceID); + _tileDefinitions.Register(spaceDef); + + var prototypeList = new ValueList(); + foreach (var tileDef in _prototypeManager.EnumeratePrototypes()) + { + if (tileDef.ID == ContentTileDefinition.SpaceID) + continue; + + prototypeList.Add(tileDef); + } + + // Sort ordinal to ensure tile IDs are deterministic and match between client and server. + prototypeList.Sort((a, b) => string.Compare(a.ID, b.ID, StringComparison.Ordinal)); + + foreach (var tileDef in prototypeList) + { + _tileDefinitions.Register(tileDef); + } + + _tileDefinitions.Initialize(); + } + + private void PreserveTileIds() + { + // TileDefinitionManager does not re-register definitions on prototype reload. Preserve the IDs assigned during + // initial registration so reloaded tile prototypes continue to match map chunk tile IDs. + foreach (var def in _prototypeManager.EnumeratePrototypes()) + { + if (_tileDefinitions.TryGetDefinition(def.ID, out var registered)) + def.AssignTileId(registered.TileId); + } + } +} diff --git a/Content.Shared/Maps/TurfSystem.cs b/Content.Shared/Maps/TurfSystem.cs index c6b0755cf4..34d9b75a42 100644 --- a/Content.Shared/Maps/TurfSystem.cs +++ b/Content.Shared/Maps/TurfSystem.cs @@ -1,11 +1,15 @@ +using System.Buffers; using System.Diagnostics.CodeAnalysis; +using System.Diagnostics.Contracts; using System.Linq; using System.Numerics; using Content.Shared.Physics; using Robust.Shared.Map; using Robust.Shared.Map.Components; using Robust.Shared.Physics; +using Robust.Shared.Prototypes; using Robust.Shared.Toolshed.Commands.Values; +using Robust.Shared.Utility; namespace Content.Shared.Maps; @@ -21,6 +25,58 @@ public sealed partial class TurfSystem : EntitySystem [Dependency] private EntityQuery _fixtureQuery = default!; + private bool[] _tileHasMapAtmosphere = ArrayPool.Shared.Rent(0); + + public override void Initialize() + { + base.Initialize(); + + RegisterTileDefinitions(); + RebuildTileAtmosphereCache(); + } + + [SubscribeLocalEvent] + private void OnPrototypesReloaded(PrototypesReloadedEventArgs args) + { + if (!args.WasModified()) + return; + + PreserveTileIds(); + RebuildTileAtmosphereCache(); + } + + private void RebuildTileAtmosphereCache() + { + var maxTileId = 0; + + foreach (var tileDef in _tileDefinitions) + { + maxTileId = Math.Max(maxTileId, tileDef.TileId); + } + + ArrayPool.Shared.Return(_tileHasMapAtmosphere); + var cache = ArrayPool.Shared.Rent(maxTileId + 1); + Array.Clear(cache); + + foreach (var tileDef in _tileDefinitions) + { + if (tileDef is not ContentTileDefinition contentTile) + continue; + + cache[contentTile.TileId] = contentTile.MapAtmosphere; + } + + _tileHasMapAtmosphere = cache; + } + + public override void Shutdown() + { + base.Shutdown(); + + ArrayPool.Shared.Return(_tileHasMapAtmosphere); + _tileHasMapAtmosphere = []; + } + /// /// Attempts to get the turf at or under some given coordinates or null if no such turf exists. /// @@ -127,9 +183,18 @@ public sealed partial class TurfSystem : EntitySystem /// /// The tile in question. /// True if the tile is considered to be space, false otherwise. + [Pure] public bool IsSpace(Tile tile) { - return GetContentTileDefinition(tile).MapAtmosphere; + var typeId = tile.TypeId; + if (typeId < _tileHasMapAtmosphere.Length) + return _tileHasMapAtmosphere[typeId]; + + var tileDef = GetContentTileDefinition(tile); + DebugTools.Assert(false, $"Found non-cached tilemap atmosphere for ID {tile.TypeId}: {tileDef.ID}"); + + // Tile IDs are normally stable after startup but keep this in case shit breaks. + return tileDef.MapAtmosphere; } /// @@ -137,6 +202,7 @@ public sealed partial class TurfSystem : EntitySystem /// /// The tile in question. /// True if the tile is considered to be space, false otherwise. + [Pure] public bool IsSpace(TileRef tile) { return IsSpace(tile.Tile); @@ -155,6 +221,7 @@ public sealed partial class TurfSystem : EntitySystem /// /// Returns the content tile definition for a tile. /// + [Pure] public ContentTileDefinition GetContentTileDefinition(Tile tile) { return (ContentTileDefinition)_tileDefinitions[tile.TypeId]; @@ -163,6 +230,7 @@ public sealed partial class TurfSystem : EntitySystem /// /// Returns the content tile definition for a tile ref. /// + [Pure] public ContentTileDefinition GetContentTileDefinition(TileRef tile) { return GetContentTileDefinition(tile.Tile);