CatStation/Content.Client/Light/SunShadowOverlay.cs
TemporalOroboros 84b2f07157
The IMapManager is dead (#44002)
* Axes the easy IMapManager references

* Axes the references to unported (obsolete) IMapManager methods

* Converts salvageruler command to a LocalizedEntityCommand so it can use SharedMapSystem

* Removes IMapManager from GameTicker
This required axing the map nuke during restart, but that should be fine?
Maps are entities so EntityManager.FlushEntities should delete them all and that's the preceding line

* submodule update(?)

---------

Co-authored-by: ArtisticRoomba <145879011+ArtisticRoomba@users.noreply.github.com>
2026-07-01 20:44:54 -07:00

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 partial class SunShadowOverlay : Overlay
{
private static readonly ProtoId<ShaderPrototype> MixShader = "Mix";
public override OverlaySpace Space => OverlaySpace.BeforeLighting;
[Dependency] private IClyde _clyde = default!;
[Dependency] private IEntityManager _entManager = default!;
[Dependency] private IPrototypeManager _protoManager = default!;
private readonly EntityLookupSystem _lookup;
private readonly SharedMapSystem _mapSys;
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>();
_mapSys = _entManager.System<SharedMapSystem>();
_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();
_mapSys.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();
}
}
}