Ambient occlusion overlay optimisation + art change + cleanup (#44262)

This commit is contained in:
metalgearsloth 2026-08-08 16:29:35 +10:00 • committed by GitHub
parent bd0fac6c6c
commit efafb0ba37
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
10 changed files with 524 additions and 168 deletions

View file

@ -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<ShaderPrototype> UnshadedShader = "unshaded";
private static readonly ProtoId<ShaderPrototype> StencilMaskShader = "StencilMask";
private static readonly ProtoId<ShaderPrototype> 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<Entity<MapGridComponent>> _cachedGrids = new();
private readonly List<Entity<OccluderComponent, TransformComponent>> _cachedOccluders = new();
private readonly List<Vector2> _aoVertices = new(4096);
private readonly List<ushort> _aoIndices = new(6144);
public override OverlaySpace Space => OverlaySpace.WorldSpaceBelowEntities;
private readonly OverlayResourceCache<CachedResources> _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<OccluderSystem>();
_gridStencil = _entManager.System<GridStencilSystem>();
_xformSystem = _entManager.System<SharedTransformSystem>();
_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<SharedMapSystem>();
var lookups = _entManager.System<EntityLookupSystem>();
var query = _entManager.System<OccluderSystem>();
var xformSystem = _entManager.System<SharedTransformSystem>();
var turfSystem = _entManager.System<TurfSystem>();
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<OccluderComponent> 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<OccluderComponent> 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<OccluderComponent> 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<Vector2> 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();
}
}
}

View file

@ -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;
/// <summary>
/// Builds viewport-sized masks for grid contents that need to be reused by world overlays.
/// </summary>
public sealed partial class GridStencilSystem : EntitySystem
{
public delegate bool TileStencilPredicate(Entity<MapGridComponent> 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<CachedResources> _resources = new();
private List<Entity<MapGridComponent>> _grids = new();
private readonly List<WorldRect> _rects = new();
/// <summary>
/// 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.
/// </summary>
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<Tile> 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!;
}
/// <summary>
/// Returns a viewport-sized texture where tiles accepted by <paramref name="predicate"/> are white and everything else is transparent.
/// </summary>
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<string, CachedTarget> _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;
}
}

View file

@ -68,5 +68,6 @@ public sealed partial class PlanetLightSystem : EntitySystem
_overlayMan.RemoveOverlay<LightBlurOverlay>();
_overlayMan.RemoveOverlay<SunShadowOverlay>();
_overlayMan.RemoveOverlay<AfterLightTargetOverlay>();
_overlayMan.RemoveOverlay<AmbientOcclusionOverlay>();
}
}

View file

@ -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<Entity<MapGridComponent>> _grids = new();
private void DrawWeather(
in OverlayDrawArgs args,
CachedResources res,
HashSet<Entity<WeatherStatusEffectComponent, StatusEffectComponent>> weathers,
Matrix3x2 invMatrix)
HashSet<Entity<WeatherStatusEffectComponent, StatusEffectComponent>> 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<TransformComponent>();
_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));

View file

@ -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<Entity<WeatherStatusEffectComponent, StatusEffectComponent>>? _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<GridStencilSystem>();
_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<RestrictedRangeComponent>(mapUid, out var restrictedRangeComponent))
DrawRestrictedRange(args, res, restrictedRangeComponent, invMatrix);

View file

@ -16,11 +16,19 @@ public sealed partial class CCVars
public static readonly CVarDef<bool> AmbientOcclusion =
CVarDef.Create("light.ambient_occlusion", true, CVar.CLIENTONLY | CVar.ARCHIVE);
public static readonly Color DefaultAmbientOcclusionColor = Color.FromHex("#04080FAA");
/// <summary>
/// Distance in world-pixels of ambient occlusion.
/// Color of ambient occlusion.
/// </summary>
public static readonly CVarDef<string> AmbientOcclusionColor =
CVarDef.Create("light.ambient_occlusion_color", "#04080FAA", CVar.CLIENTONLY);
CVarDef.Create("light.ambient_occlusion_color", DefaultAmbientOcclusionColor.ToHex(), CVar.CLIENTONLY);
/// <summary>
/// Render resolution scale for ambient occlusion.
/// </summary>
public static readonly CVarDef<float> AmbientOcclusionResolutionScale =
CVarDef.Create("light.ambient_occlusion_resolution_scale", 0.5f, CVar.CLIENTONLY | CVar.ARCHIVE);
/// <summary>
/// Distance in world-pixels of ambient occlusion.

View file

@ -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<MarkingManager>().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>(ContentTileDefinition.SpaceID);
_tileDefinitionManager.Register(spaceDef);
var prototypeList = new List<ContentTileDefinition>();
foreach (var tileDef in _prototypeManager.EnumeratePrototypes<ContentTileDefinition>())
{
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<ContentTileDefinition>())
return;
*/
// Need to re-allocate tiledefs due to how prototype reloads work
foreach (var def in _prototypeManager.EnumeratePrototypes<ContentTileDefinition>())
{
def.AssignTileId(_tileDefinitionManager[def.ID].TileId);
}
}
private void IgnorePrototypes()
{
if (!TryReadFile(out var sequences))

View file

@ -0,0 +1,17 @@
using Content.Shared.CCVar;
using Robust.Shared.GameStates;
namespace Content.Shared.Light.Components;
/// <summary>
/// Controls map-specific ambient occlusion color.
/// </summary>
[RegisterComponent, NetworkedComponent, AutoGenerateComponentState]
public sealed partial class MapAmbientColorComponent : Component
{
/// <summary>
/// Color used by ambient occlusion on this map.
/// </summary>
[DataField, AutoNetworkedField]
public Color Color = CCVars.DefaultAmbientOcclusionColor;
}

View file

@ -0,0 +1,56 @@
using Robust.Shared.Collections;
using Robust.Shared.Prototypes;
namespace Content.Shared.Maps;
/// <summary>
/// Tile definition registration and reload handling for <see cref="TurfSystem"/>.
/// </summary>
/// <remarks>
/// 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.
/// </remarks>
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>(ContentTileDefinition.SpaceID);
_tileDefinitions.Register(spaceDef);
var prototypeList = new ValueList<ContentTileDefinition>();
foreach (var tileDef in _prototypeManager.EnumeratePrototypes<ContentTileDefinition>())
{
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<ContentTileDefinition>())
{
if (_tileDefinitions.TryGetDefinition(def.ID, out var registered))
def.AssignTileId(registered.TileId);
}
}
}

View file

@ -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<FixturesComponent> _fixtureQuery = default!;
private bool[] _tileHasMapAtmosphere = ArrayPool<bool>.Shared.Rent(0);
public override void Initialize()
{
base.Initialize();
RegisterTileDefinitions();
RebuildTileAtmosphereCache();
}
[SubscribeLocalEvent]
private void OnPrototypesReloaded(PrototypesReloadedEventArgs args)
{
if (!args.WasModified<ContentTileDefinition>())
return;
PreserveTileIds();
RebuildTileAtmosphereCache();
}
private void RebuildTileAtmosphereCache()
{
var maxTileId = 0;
foreach (var tileDef in _tileDefinitions)
{
maxTileId = Math.Max(maxTileId, tileDef.TileId);
}
ArrayPool<bool>.Shared.Return(_tileHasMapAtmosphere);
var cache = ArrayPool<bool>.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<bool>.Shared.Return(_tileHasMapAtmosphere);
_tileHasMapAtmosphere = [];
}
/// <summary>
/// Attempts to get the turf at or under some given coordinates or null if no such turf exists.
/// </summary>
@ -127,9 +183,18 @@ public sealed partial class TurfSystem : EntitySystem
/// </summary>
/// <param name="tile">The tile in question.</param>
/// <returns>True if the tile is considered to be space, false otherwise.</returns>
[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;
}
/// <summary>
@ -137,6 +202,7 @@ public sealed partial class TurfSystem : EntitySystem
/// </summary>
/// <param name="tile">The tile in question.</param>
/// <returns>True if the tile is considered to be space, false otherwise.</returns>
[Pure]
public bool IsSpace(TileRef tile)
{
return IsSpace(tile.Tile);
@ -155,6 +221,7 @@ public sealed partial class TurfSystem : EntitySystem
/// <summary>
/// Returns the content tile definition for a tile.
/// </summary>
[Pure]
public ContentTileDefinition GetContentTileDefinition(Tile tile)
{
return (ContentTileDefinition)_tileDefinitions[tile.TypeId];
@ -163,6 +230,7 @@ public sealed partial class TurfSystem : EntitySystem
/// <summary>
/// Returns the content tile definition for a tile ref.
/// </summary>
[Pure]
public ContentTileDefinition GetContentTileDefinition(TileRef tile)
{
return GetContentTileDefinition(tile.Tile);