From 6263b14666360f280c534e8ffcf4296feba4c496 Mon Sep 17 00:00:00 2001
From: Pok245 <113675512+Pok27@users.noreply.github.com>
Date: Sat, 18 Apr 2026 04:02:42 +0300
Subject: [PATCH] DrawOverFov
---
.../Components/Renderable/SpriteComponent.cs | 16 +-
.../EntitySystems/SpriteSystem.Component.cs | 7 +-
Robust.Client/Graphics/Clyde/Clyde.HLR.cs | 223 ++++++++++--------
3 files changed, 148 insertions(+), 98 deletions(-)
diff --git a/Robust.Client/GameObjects/Components/Renderable/SpriteComponent.cs b/Robust.Client/GameObjects/Components/Renderable/SpriteComponent.cs
index d1dbd6587c6..42a387746f6 100644
--- a/Robust.Client/GameObjects/Components/Renderable/SpriteComponent.cs
+++ b/Robust.Client/GameObjects/Components/Renderable/SpriteComponent.cs
@@ -231,6 +231,14 @@ public ShaderInstance? PostShader
[DataField]
public bool RaiseShaderEvent;
+ ///
+ /// If true, this sprite is drawn after hard FOV has been applied to the viewport.
+ /// Useful for semi-transparent occluders like smoke, where FOV should appear behind the sprite instead of
+ /// on top of it.
+ ///
+ [DataField]
+ public bool DrawOverFov;
+
[ViewVariables] internal Dictionary LayerMap { get; set; } = new();
[ViewVariables] internal List Layers = new();
@@ -1120,7 +1128,8 @@ public sealed class Layer : ISpriteLayer, ISerializationHooks
///
[ViewVariables] internal bool UnShaded;
- [ViewVariables] public RSI? RSI
+ [ViewVariables]
+ public RSI? RSI
{
get => _rsi;
[Obsolete("Use SpriteSystem.LayerSetRsi() instead.")]
@@ -1137,7 +1146,8 @@ [ViewVariables] public RSI? RSI
}
internal RSI.StateId StateId;
- [ViewVariables] public RSI.StateId State
+ [ViewVariables]
+ public RSI.StateId State
{
get => StateId;
[Obsolete("Use SpriteSystem.LayerSetRsiState() instead.")]
@@ -1637,7 +1647,7 @@ internal void GetLayerDrawMatrix(RsiDirection dir, out Matrix3x2 layerDrawMatrix
if (dir == RsiDirection.South || noRot)
layerDrawMatrix = LocalMatrix;
else
- layerDrawMatrix = Matrix3x2.Multiply(_rsiDirectionMatrices[(int) dir], LocalMatrix);
+ layerDrawMatrix = Matrix3x2.Multiply(_rsiDirectionMatrices[(int)dir], LocalMatrix);
}
private static Matrix3x2[] _rsiDirectionMatrices = new Matrix3x2[]
diff --git a/Robust.Client/GameObjects/EntitySystems/SpriteSystem.Component.cs b/Robust.Client/GameObjects/EntitySystems/SpriteSystem.Component.cs
index f96f1bb9ede..ba1c8927db8 100644
--- a/Robust.Client/GameObjects/EntitySystems/SpriteSystem.Component.cs
+++ b/Robust.Client/GameObjects/EntitySystems/SpriteSystem.Component.cs
@@ -42,7 +42,7 @@ public void SetAutoAnimateSync(SpriteComponent sprite, SpriteComponent.Layer lay
return;
}
- layer.AnimationTimeLeft = (float) -(time % state.TotalDelay);
+ layer.AnimationTimeLeft = (float)-(time % state.TotalDelay);
layer.AnimationFrame = 0;
}
@@ -82,11 +82,12 @@ in target
target.Comp.IsInert = source.Comp.IsInert;
target.Comp.LayerMap = source.Comp.LayerMap.ShallowClone();
- target.Comp.PostShader = source.Comp.PostShader is {Mutable: true}
+ target.Comp.PostShader = source.Comp.PostShader is { Mutable: true }
? source.Comp.PostShader.Duplicate()
: source.Comp.PostShader;
target.Comp.RenderOrder = source.Comp.RenderOrder;
+ target.Comp.DrawOverFov = source.Comp.DrawOverFov;
target.Comp.GranularLayersRendering = source.Comp.GranularLayersRendering;
target.Comp.Loop = source.Comp.Loop;
@@ -107,5 +108,5 @@ public void QueueUpdateIsInert(Entity sprite)
}
[Obsolete("Use QueueUpdateIsInert")]
- public void QueueUpdateInert(EntityUid uid, SpriteComponent sprite) => QueueUpdateIsInert(new (uid, sprite));
+ public void QueueUpdateInert(EntityUid uid, SpriteComponent sprite) => QueueUpdateIsInert(new(uid, sprite));
}
diff --git a/Robust.Client/Graphics/Clyde/Clyde.HLR.cs b/Robust.Client/Graphics/Clyde/Clyde.HLR.cs
index 684f75c4229..b1c163c222e 100644
--- a/Robust.Client/Graphics/Clyde/Clyde.HLR.cs
+++ b/Robust.Client/Graphics/Clyde/Clyde.HLR.cs
@@ -33,6 +33,7 @@ internal partial class Clyde
public static float PostShadeScale = 1.25f;
private List _overlays = new();
+ private readonly List _drawOverFovSprites = new();
public void Render()
{
@@ -297,6 +298,12 @@ private void DrawEntities(Viewport viewport, Box2Rotated worldBounds, Box2 world
{
ref var entry = ref _drawingSpriteList[indexList[i]];
+ if (entry.Sprite.DrawOverFov)
+ {
+ _drawOverFovSprites.Add(indexList[i]);
+ continue;
+ }
+
for (; overlayIndex < worldOverlays.Count; overlayIndex++)
{
var overlay = worldOverlays[overlayIndex];
@@ -316,118 +323,148 @@ private void DrawEntities(Viewport viewport, Box2Rotated worldBounds, Box2 world
RenderSingleWorldOverlay(overlay, viewport, OverlaySpace.WorldSpaceEntities, worldAABB, worldBounds);
}
- Vector2i roundedPos = default;
- if (entry.Sprite.PostShader != null)
+ DrawEntity(viewport, eye, spriteSystem, ref entry, ref entityPostRenderTarget, screenSize);
+ }
+
+ // draw remainder of overlays
+ for (; overlayIndex < worldOverlays.Count; overlayIndex++)
+ {
+ if (!flushed)
{
- // get the size of the sprite on screen, scaled slightly to allow for shaders that increase the final sprite size.
- var screenSpriteSize = (Vector2i)(entry.SpriteScreenBB.Size * PostShadeScale).Rounded();
-
- // I'm not 100% sure why it works, but without it post-shader
- // can be lower or upper by 1px than original sprite depending on sprite rotation or scale
- // probably some rotation rounding error
- if (screenSpriteSize.X % 2 != 0)
- screenSpriteSize.X++;
- if (screenSpriteSize.Y % 2 != 0)
- screenSpriteSize.Y++;
-
- bool exit = false;
- if (entry.Sprite.GetScreenTexture && entry.Sprite.PostShader != null)
- {
- FlushRenderQueue();
- var tex = CopyScreenTexture(viewport.RenderTarget);
- if (tex == null)
- exit = true;
- else
- entry.Sprite.PostShader.SetParameter("SCREEN_TEXTURE", tex);
- }
+ FlushRenderQueue();
+ flushed = true;
+ }
- // check that sprite size is valid
- if (!exit && screenSpriteSize.X > 0 && screenSpriteSize.Y > 0)
+ RenderSingleWorldOverlay(worldOverlays[overlayIndex], viewport, OverlaySpace.WorldSpaceEntities, worldAABB, worldBounds);
+ }
+
+ ArrayPool.Shared.Return(indexList);
+ entityPostRenderTarget?.DisposeDeferred();
+ FlushRenderQueue();
+ }
+
+ private void DrawEntity(
+ Viewport viewport,
+ IEye eye,
+ SpriteSystem spriteSystem,
+ ref SpriteData entry,
+ ref RenderTexture? entityPostRenderTarget,
+ Vector2i screenSize)
+ {
+ Vector2i roundedPos = default;
+ if (entry.Sprite.PostShader != null)
+ {
+ // get the size of the sprite on screen, scaled slightly to allow for shaders that increase the final sprite size.
+ var screenSpriteSize = (Vector2i)(entry.SpriteScreenBB.Size * PostShadeScale).Rounded();
+
+ // I'm not 100% sure why it works, but without it post-shader
+ // can be lower or upper by 1px than original sprite depending on sprite rotation or scale
+ // probably some rotation rounding error
+ if (screenSpriteSize.X % 2 != 0)
+ screenSpriteSize.X++;
+ if (screenSpriteSize.Y % 2 != 0)
+ screenSpriteSize.Y++;
+
+ bool exit = false;
+ if (entry.Sprite.GetScreenTexture)
+ {
+ FlushRenderQueue();
+ var tex = CopyScreenTexture(viewport.RenderTarget);
+ if (tex == null)
+ exit = true;
+ else
+ entry.Sprite.PostShader.SetParameter("SCREEN_TEXTURE", tex);
+ }
+
+ // check that sprite size is valid
+ if (!exit && screenSpriteSize.X > 0 && screenSpriteSize.Y > 0)
+ {
+ // This is really bare-bones render target re-use logic. One problem is that if it ever draws a
+ // single large entity in a frame, the render target may be way to big for every subsequent
+ // entity. But the vast majority of sprites are currently all 32x32, so it doesn't matter all that
+ // much.
+ //
+ // Also, if there are many differenty sizes, and they all happen to be drawn in order of
+ // increasing size, then this will still generate a whole bunch of render targets. So maybe
+ // iterate once _drawingSpriteList, check sprite sizes, and decide what render targets to create
+ // based off of that?
+ //
+ // TODO PERFORMANCE better renderTarget re-use / caching.
+
+ if (entityPostRenderTarget == null
+ || entityPostRenderTarget.Size.X < screenSpriteSize.X
+ || entityPostRenderTarget.Size.Y < screenSpriteSize.Y)
{
- // This is really bare-bones render target re-use logic. One problem is that if it ever draws a
- // single large entity in a frame, the render target may be way to big for every subsequent
- // entity. But the vast majority of sprites are currently all 32x32, so it doesn't matter all that
- // much.
- //
- // Also, if there are many differenty sizes, and they all happen to be drawn in order of
- // increasing size, then this will still generate a whole bunch of render targets. So maybe
- // iterate once _drawingSpriteList, check sprite sizes, and decide what render targets to create
- // based off of that?
- //
- // TODO PERFORMANCE better renderTarget re-use / caching.
-
- if (entityPostRenderTarget == null
- || entityPostRenderTarget.Size.X < screenSpriteSize.X
- || entityPostRenderTarget.Size.Y < screenSpriteSize.Y)
- {
- entityPostRenderTarget = CreateRenderTarget(screenSpriteSize,
- new RenderTargetFormatParameters(RenderTargetColorFormat.Rgba8Srgb, true),
- name: nameof(entityPostRenderTarget));
- }
-
- _renderHandle.UseRenderTarget(entityPostRenderTarget);
- _renderHandle.Clear(default, 0, ClearBufferMask.ColorBufferBit | ClearBufferMask.StencilBufferBit);
-
- // Calculate viewport so that the entity thinks it's drawing to the same position,
- // which is necessary for light application,
- // but it's ACTUALLY drawing into the center of the render target.
- roundedPos = (Vector2i) entry.SpriteScreenBB.Center;
- var flippedPos = new Vector2i(roundedPos.X, screenSize.Y - roundedPos.Y);
- flippedPos -= entityPostRenderTarget.Size / 2;
- _renderHandle.Viewport(Box2i.FromDimensions(-flippedPos, screenSize));
-
- if (entry.Sprite.RaiseShaderEvent)
- _entityManager.EventBus.RaiseLocalEvent(entry.Uid,
- new BeforePostShaderRenderEvent(entry.Sprite, viewport), false);
+ entityPostRenderTarget = CreateRenderTarget(screenSpriteSize,
+ new RenderTargetFormatParameters(RenderTargetColorFormat.Rgba8Srgb, true),
+ name: nameof(entityPostRenderTarget));
}
+
+ _renderHandle.UseRenderTarget(entityPostRenderTarget);
+ _renderHandle.Clear(default, 0, ClearBufferMask.ColorBufferBit | ClearBufferMask.StencilBufferBit);
+
+ // Calculate viewport so that the entity thinks it's drawing to the same position,
+ // which is necessary for light application,
+ // but it's ACTUALLY drawing into the center of the render target.
+ roundedPos = (Vector2i)entry.SpriteScreenBB.Center;
+ var flippedPos = new Vector2i(roundedPos.X, screenSize.Y - roundedPos.Y);
+ flippedPos -= entityPostRenderTarget.Size / 2;
+ _renderHandle.Viewport(Box2i.FromDimensions(-flippedPos, screenSize));
+
+ if (entry.Sprite.RaiseShaderEvent)
+ _entityManager.EventBus.RaiseLocalEvent(entry.Uid,
+ new BeforePostShaderRenderEvent(entry.Sprite, viewport), false);
}
+ }
- spriteSystem.RenderSprite(new(entry.Uid, entry.Sprite), _renderHandle.DrawingHandleWorld, eye.Rotation, entry.WorldRot, entry.WorldPos);
+ spriteSystem.RenderSprite(new(entry.Uid, entry.Sprite), _renderHandle.DrawingHandleWorld, eye.Rotation, entry.WorldRot, entry.WorldPos);
- if (entry.Sprite.PostShader != null && entityPostRenderTarget != null)
- {
- var oldProj = _currentMatrixProj;
- var oldView = _currentMatrixView;
+ if (entry.Sprite.PostShader == null || entityPostRenderTarget == null)
+ return;
- _renderHandle.UseRenderTarget(viewport.RenderTarget);
- _renderHandle.Viewport(Box2i.FromDimensions(Vector2i.Zero, screenSize));
+ var oldProj = _currentMatrixProj;
+ var oldView = _currentMatrixView;
- _renderHandle.UseShader(entry.Sprite.PostShader);
- CalcScreenMatrices(viewport.Size, out var proj, out var view);
- _renderHandle.SetProjView(proj, view);
- _renderHandle.SetModelTransform(Matrix3x2.Identity);
+ _renderHandle.UseRenderTarget(viewport.RenderTarget);
+ _renderHandle.Viewport(Box2i.FromDimensions(Vector2i.Zero, screenSize));
- var rounded = roundedPos - entityPostRenderTarget.Size / 2;
+ _renderHandle.UseShader(entry.Sprite.PostShader);
+ CalcScreenMatrices(viewport.Size, out var proj, out var view);
+ _renderHandle.SetProjView(proj, view);
+ _renderHandle.SetModelTransform(Matrix3x2.Identity);
- var box = Box2i.FromDimensions(rounded, entityPostRenderTarget.Size);
+ var rounded = roundedPos - entityPostRenderTarget.Size / 2;
+ var box = Box2i.FromDimensions(rounded, entityPostRenderTarget.Size);
- _renderHandle.DrawTextureScreen(entityPostRenderTarget.Texture,
- box.BottomLeft, box.BottomRight, box.TopLeft, box.TopRight,
- Color.White, null);
+ _renderHandle.DrawTextureScreen(entityPostRenderTarget.Texture,
+ box.BottomLeft, box.BottomRight, box.TopLeft, box.TopRight,
+ Color.White, null);
- _renderHandle.SetProjView(oldProj, oldView);
- _renderHandle.UseShader(null);
- }
- }
+ _renderHandle.SetProjView(oldProj, oldView);
+ _renderHandle.UseShader(null);
+ }
- // draw remainder of overlays
- for (; overlayIndex < worldOverlays.Count; overlayIndex++)
- {
- if (!flushed)
- {
- FlushRenderQueue();
- flushed = true;
- }
+ private void DrawEntitiesOverFov(Viewport viewport, IEye eye)
+ {
+ if (_drawingSpriteList.Count == 0)
+ return;
- RenderSingleWorldOverlay(worldOverlays[overlayIndex], viewport, OverlaySpace.WorldSpaceEntities, worldAABB, worldBounds);
+ var spriteSystem = _entityManager.System();
+ var screenSize = viewport.Size;
+ RenderTexture? entityPostRenderTarget = null;
+
+ for (var i = 0; i < _drawOverFovSprites.Count; i++)
+ {
+ ref var entry = ref _drawingSpriteList[_drawOverFovSprites[i]];
+ DrawEntity(viewport, eye, spriteSystem, ref entry, ref entityPostRenderTarget, screenSize);
}
- ArrayPool.Shared.Return(indexList);
entityPostRenderTarget?.DisposeDeferred();
+ FlushRenderQueue();
_debugStats.Entities += _drawingSpriteList.Count;
+ _drawOverFovSprites.Clear();
_drawingSpriteList.Clear();
- FlushRenderQueue();
}
private void DrawLoadingScreen(IRenderHandle handle)
@@ -437,7 +474,7 @@ private void DrawLoadingScreen(IRenderHandle handle)
_loadingScreenManager.DrawLoadingScreen(handle, ScreenSize);
}
- private void RenderInRenderTarget(RenderTargetBase rt, Action a, Color? clearColor=default)
+ private void RenderInRenderTarget(RenderTargetBase rt, Action a, Color? clearColor = default)
{
// TODO: for the love of god all this state pushing/popping needs to be cleaned up.
@@ -556,6 +593,8 @@ private void RenderViewport(Viewport viewport)
if (_entityManager.GetComponent(mapUid).LightingEnabled)
ApplyFovToBuffer(viewport, eye);
}
+
+ DrawEntitiesOverFov(viewport, eye);
}
_lightingReady = false;