mirror of
https://github.com/space-syndicate/space-station-14.git
synced 2026-10-09 10:21:18 +03:00
Ambient occlusion overlay optimisation + art change + cleanup (#44262)
This commit is contained in:
parent
bd0fac6c6c
commit
efafb0ba37
10 changed files with 524 additions and 168 deletions
|
|
@ -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();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
|||
217
Content.Client/Light/EntitySystems/GridStencilSystem.cs
Normal file
217
Content.Client/Light/EntitySystems/GridStencilSystem.cs
Normal 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;
|
||||
}
|
||||
}
|
||||
|
|
@ -68,5 +68,6 @@ public sealed partial class PlanetLightSystem : EntitySystem
|
|||
_overlayMan.RemoveOverlay<LightBlurOverlay>();
|
||||
_overlayMan.RemoveOverlay<SunShadowOverlay>();
|
||||
_overlayMan.RemoveOverlay<AfterLightTargetOverlay>();
|
||||
_overlayMan.RemoveOverlay<AmbientOcclusionOverlay>();
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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));
|
||||
|
|
|
|||
|
|
@ -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);
|
||||
|
|
|
|||
|
|
@ -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.
|
||||
|
|
|
|||
|
|
@ -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))
|
||||
|
|
|
|||
17
Content.Shared/Light/Components/MapAmbientColorComponent.cs
Normal file
17
Content.Shared/Light/Components/MapAmbientColorComponent.cs
Normal 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;
|
||||
}
|
||||
56
Content.Shared/Maps/TurfSystem.TileDefinitions.cs
Normal file
56
Content.Shared/Maps/TurfSystem.TileDefinitions.cs
Normal 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);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -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);
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue