Many newer overlays use IRenderTextures that are sized to the rendered viewport. This was completely broken, because a single viewport can be rendered on multiple viewports in a single frame. The end result of this was that in the better case, constant render targets were allocated and freed, which is extremely inefficient. In the worse case, many of these overlays completely failed to Dispose() their render targets, leading to *extremely* swift VRAM OOMs. This fixes all the overlays to properly cache resources per viewport. This uses new engine functionality, so it requires engine master. This is still a pretty lousy way to do GPU resource management but, well, anything better needs a render graph, so...
184 lines
7.1 KiB
C#
184 lines
7.1 KiB
C#
using System.Numerics;
|
|
using Content.Client.Graphics;
|
|
using Content.Shared.Light.Components;
|
|
using Robust.Client.Graphics;
|
|
using Robust.Shared.Enums;
|
|
using Robust.Shared.Map;
|
|
using Robust.Shared.Map.Components;
|
|
using Robust.Shared.Physics;
|
|
using Robust.Shared.Prototypes;
|
|
|
|
namespace Content.Client.Light;
|
|
|
|
public sealed class SunShadowOverlay : Overlay
|
|
{
|
|
private static readonly ProtoId<ShaderPrototype> MixShader = "Mix";
|
|
|
|
public override OverlaySpace Space => OverlaySpace.BeforeLighting;
|
|
|
|
[Dependency] private readonly IClyde _clyde = default!;
|
|
[Dependency] private readonly IEntityManager _entManager = default!;
|
|
[Dependency] private readonly IMapManager _mapManager = default!;
|
|
[Dependency] private readonly IPrototypeManager _protoManager = default!;
|
|
private readonly EntityLookupSystem _lookup;
|
|
private readonly SharedTransformSystem _xformSys;
|
|
|
|
private readonly HashSet<Entity<SunShadowCastComponent>> _shadows = new();
|
|
|
|
private readonly OverlayResourceCache<CachedResources> _resources = new();
|
|
|
|
public SunShadowOverlay()
|
|
{
|
|
IoCManager.InjectDependencies(this);
|
|
_xformSys = _entManager.System<SharedTransformSystem>();
|
|
_lookup = _entManager.System<EntityLookupSystem>();
|
|
ZIndex = AfterLightTargetOverlay.ContentZIndex + 1;
|
|
}
|
|
|
|
private List<Entity<MapGridComponent>> _grids = new();
|
|
|
|
protected override void Draw(in OverlayDrawArgs args)
|
|
{
|
|
var viewport = args.Viewport;
|
|
var eye = viewport.Eye;
|
|
|
|
if (eye == null)
|
|
return;
|
|
|
|
_grids.Clear();
|
|
_mapManager.FindGridsIntersecting(args.MapId,
|
|
args.WorldBounds.Enlarged(SunShadowComponent.MaxLength),
|
|
ref _grids);
|
|
|
|
var worldHandle = args.WorldHandle;
|
|
var mapId = args.MapId;
|
|
var worldBounds = args.WorldBounds;
|
|
var targetSize = viewport.LightRenderTarget.Size;
|
|
|
|
var res = _resources.GetForViewport(args.Viewport, static _ => new CachedResources());
|
|
|
|
if (res.Target?.Size != targetSize)
|
|
{
|
|
res.Target = _clyde
|
|
.CreateRenderTarget(targetSize,
|
|
new RenderTargetFormatParameters(RenderTargetColorFormat.Rgba8Srgb),
|
|
name: "sun-shadow-target");
|
|
|
|
if (res.BlurTarget?.Size != targetSize)
|
|
{
|
|
res.BlurTarget = _clyde
|
|
.CreateRenderTarget(targetSize, new RenderTargetFormatParameters(RenderTargetColorFormat.Rgba8Srgb), name: "sun-shadow-blur");
|
|
}
|
|
}
|
|
|
|
var lightScale = viewport.LightRenderTarget.Size / (Vector2)viewport.Size;
|
|
var scale = viewport.RenderScale / (Vector2.One / lightScale);
|
|
|
|
foreach (var grid in _grids)
|
|
{
|
|
if (!_entManager.TryGetComponent(grid.Owner, out SunShadowComponent? sun))
|
|
{
|
|
continue;
|
|
}
|
|
|
|
var direction = sun.Direction;
|
|
var alpha = Math.Clamp(sun.Alpha, 0f, 1f);
|
|
|
|
// Nowhere to cast to so ignore it.
|
|
if (direction.Equals(Vector2.Zero) || alpha == 0f)
|
|
continue;
|
|
|
|
// Feature todo: dynamic shadows for mobs and trees. Also ideally remove the fake tree shadows.
|
|
// TODO: Jittering still not quite perfect
|
|
|
|
var expandedBounds = worldBounds.Enlarged(direction.Length() + 0.01f);
|
|
_shadows.Clear();
|
|
|
|
// Draw shadow polys to stencil
|
|
args.WorldHandle.RenderInRenderTarget(res.Target,
|
|
() =>
|
|
{
|
|
var invMatrix =
|
|
res.Target.GetWorldToLocalMatrix(eye, scale);
|
|
var indices = new Vector2[PhysicsConstants.MaxPolygonVertices * 2];
|
|
|
|
// Go through shadows in range.
|
|
|
|
// For each one we:
|
|
// - Get the original vertices.
|
|
// - Extrapolate these along the sun direction.
|
|
// - Combine the above into 1 single polygon to draw.
|
|
|
|
// Note that this is range-limited for accuracy; if you set it too high it will clip through walls or other undesirable entities.
|
|
// This is probably not noticeable most of the time but if you want something "accurate" you'll want to code a solution.
|
|
// Ideally the CPU would have its own shadow-map copy that we could just ray-cast each vert into though
|
|
// You might need to batch verts or the likes as this could get expensive.
|
|
_lookup.GetEntitiesIntersecting(mapId, expandedBounds, _shadows);
|
|
|
|
foreach (var ent in _shadows)
|
|
{
|
|
var xform = _entManager.GetComponent<TransformComponent>(ent.Owner);
|
|
var (worldPos, worldRot) = _xformSys.GetWorldPositionRotation(xform);
|
|
// Need no rotation on matrix as sun shadow direction doesn't care.
|
|
var worldMatrix = Matrix3x2.CreateTranslation(worldPos);
|
|
var renderMatrix = Matrix3x2.Multiply(worldMatrix, invMatrix);
|
|
var pointCount = ent.Comp.Points.Length;
|
|
|
|
Array.Copy(ent.Comp.Points, indices, pointCount);
|
|
|
|
for (var i = 0; i < pointCount; i++)
|
|
{
|
|
// Update point based on entity rotation.
|
|
indices[i] = worldRot.RotateVec(indices[i]);
|
|
|
|
// Add the offset point by the sun shadow direction.
|
|
indices[pointCount + i] = indices[i] + direction;
|
|
}
|
|
|
|
var points = PhysicsHull.ComputePoints(indices, pointCount * 2);
|
|
worldHandle.SetTransform(renderMatrix);
|
|
|
|
worldHandle.DrawPrimitives(DrawPrimitiveTopology.TriangleFan, points, Color.White);
|
|
}
|
|
},
|
|
Color.Transparent);
|
|
|
|
// Slightly blur it just to avoid aliasing issues on the later viewport-wide blur.
|
|
_clyde.BlurRenderTarget(viewport, res.Target, res.BlurTarget!, eye, 1f);
|
|
|
|
// Draw stencil (see roofoverlay).
|
|
args.WorldHandle.RenderInRenderTarget(viewport.LightRenderTarget,
|
|
() =>
|
|
{
|
|
var invMatrix =
|
|
viewport.LightRenderTarget.GetWorldToLocalMatrix(eye, scale);
|
|
worldHandle.SetTransform(invMatrix);
|
|
|
|
var maskShader = _protoManager.Index(MixShader).Instance();
|
|
worldHandle.UseShader(maskShader);
|
|
|
|
worldHandle.DrawTextureRect(res.Target.Texture, worldBounds, Color.Black.WithAlpha(alpha));
|
|
}, null);
|
|
}
|
|
}
|
|
|
|
protected override void DisposeBehavior()
|
|
{
|
|
_resources.Dispose();
|
|
|
|
base.DisposeBehavior();
|
|
}
|
|
|
|
private sealed class CachedResources : IDisposable
|
|
{
|
|
public IRenderTexture? BlurTarget;
|
|
public IRenderTexture? Target;
|
|
|
|
public void Dispose()
|
|
{
|
|
BlurTarget?.Dispose();
|
|
Target?.Dispose();
|
|
}
|
|
}
|
|
}
|