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;