From f67976da4d65b6509589776f550c9cee39e70812 Mon Sep 17 00:00:00 2001 From: Peptide90 Date: Fri, 17 Jul 2026 16:27:38 +0100 Subject: [PATCH] Adds chained post shaders --- .../Components/Renderable/SpriteComponent.cs | 33 ++++++-- .../EntitySystems/SpriteSystem.Component.cs | 7 ++ .../EntitySystems/SpriteSystem.Setters.cs | 23 ++++++ Robust.Client/Graphics/Clyde/Clyde.HLR.cs | 80 +++++++++++++++---- 4 files changed, 120 insertions(+), 23 deletions(-) diff --git a/Robust.Client/GameObjects/Components/Renderable/SpriteComponent.cs b/Robust.Client/GameObjects/Components/Renderable/SpriteComponent.cs index d1dbd6587c6..e2e29212b5d 100644 --- a/Robust.Client/GameObjects/Components/Renderable/SpriteComponent.cs +++ b/Robust.Client/GameObjects/Components/Renderable/SpriteComponent.cs @@ -202,16 +202,35 @@ public bool ContainerOccluded [ViewVariables(VVAccess.ReadWrite)] internal bool _inertUpdateQueued; /// - /// Shader instance to use when drawing the final sprite to the world. + /// Primary shader instance to use when drawing the final sprite to the world. + /// Additional shaders in are applied afterwards. /// [ViewVariables(VVAccess.ReadWrite)] - public ShaderInstance? PostShader + public ShaderInstance? PostShader { get; set; } + + /// + /// Additional post-shaders applied in order after . + /// Use and + /// to modify the chain. + /// + [ViewVariables] + public IReadOnlyList PostShaderChain => _postShaderChain; + + internal readonly List _postShaderChain = new(); + + internal int PostShaderCount => _postShaderChain.Count + (PostShader == null ? 0 : 1); + + internal ShaderInstance GetPostShader(int index) { - get; - // This will get obsoleted, but I only want to mark it as obsolete when multi-shader support is added, so - // that people can use the appropriate method and don't migrate to an incorrect new method that wont - // be obsoleted. - set; + if (PostShader != null) + { + if (index == 0) + return PostShader; + + index--; + } + + return _postShaderChain[index]; } /// diff --git a/Robust.Client/GameObjects/EntitySystems/SpriteSystem.Component.cs b/Robust.Client/GameObjects/EntitySystems/SpriteSystem.Component.cs index f96f1bb9ede..0c484fbd8b3 100644 --- a/Robust.Client/GameObjects/EntitySystems/SpriteSystem.Component.cs +++ b/Robust.Client/GameObjects/EntitySystems/SpriteSystem.Component.cs @@ -85,6 +85,13 @@ in target target.Comp.PostShader = source.Comp.PostShader is {Mutable: true} ? source.Comp.PostShader.Duplicate() : source.Comp.PostShader; + target.Comp._postShaderChain.Clear(); + foreach (var postShader in source.Comp._postShaderChain) + { + target.Comp._postShaderChain.Add(postShader.Mutable + ? postShader.Duplicate() + : postShader); + } target.Comp.RenderOrder = source.Comp.RenderOrder; target.Comp.GranularLayersRendering = source.Comp.GranularLayersRendering; diff --git a/Robust.Client/GameObjects/EntitySystems/SpriteSystem.Setters.cs b/Robust.Client/GameObjects/EntitySystems/SpriteSystem.Setters.cs index 75b2571cfea..ace860e06c3 100644 --- a/Robust.Client/GameObjects/EntitySystems/SpriteSystem.Setters.cs +++ b/Robust.Client/GameObjects/EntitySystems/SpriteSystem.Setters.cs @@ -163,4 +163,27 @@ public void SetGranularLayersRendering(Entity sprite, bool val _tree.QueueTreeUpdate(sprite!); DirtyBounds(sprite!); } + + /// + /// Adds an additional post-shader to the end of a sprite's post-shader chain. + /// The primary is applied before this chain. + /// + public bool AddPostShader(Entity sprite, ShaderInstance shader) + { + if (!_query.Resolve(sprite.Owner, ref sprite.Comp) || + sprite.Comp._postShaderChain.Contains(shader)) + return false; + + sprite.Comp._postShaderChain.Add(shader); + return true; + } + + /// + /// Removes the first matching additional post-shader from a sprite's post-shader chain. + /// + public bool RemovePostShader(Entity sprite, ShaderInstance shader) + { + return _query.Resolve(sprite.Owner, ref sprite.Comp) && + sprite.Comp._postShaderChain.Remove(shader); + } } diff --git a/Robust.Client/Graphics/Clyde/Clyde.HLR.cs b/Robust.Client/Graphics/Clyde/Clyde.HLR.cs index 684f75c4229..59d5aaba578 100644 --- a/Robust.Client/Graphics/Clyde/Clyde.HLR.cs +++ b/Robust.Client/Graphics/Clyde/Clyde.HLR.cs @@ -292,6 +292,7 @@ private void DrawEntities(Viewport viewport, Box2Rotated worldBounds, Box2 world var overlayIndex = 0; RenderTexture? entityPostRenderTarget = null; + RenderTexture? entityPostRenderTarget2 = null; bool flushed = false; for (var i = 0; i < _drawingSpriteList.Count; i++) { @@ -317,7 +318,8 @@ private void DrawEntities(Viewport viewport, Box2Rotated worldBounds, Box2 world } Vector2i roundedPos = default; - if (entry.Sprite.PostShader != null) + var applyPostShaders = false; + if (entry.Sprite.PostShaderCount > 0) { // 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(); @@ -331,14 +333,17 @@ private void DrawEntities(Viewport viewport, Box2Rotated worldBounds, Box2 world screenSpriteSize.Y++; bool exit = false; - if (entry.Sprite.GetScreenTexture && entry.Sprite.PostShader != null) + if (entry.Sprite.GetScreenTexture) { FlushRenderQueue(); var tex = CopyScreenTexture(viewport.RenderTarget); if (tex == null) exit = true; else - entry.Sprite.PostShader.SetParameter("SCREEN_TEXTURE", tex); + { + for (var shaderIndex = 0; shaderIndex < entry.Sprite.PostShaderCount; shaderIndex++) + entry.Sprite.GetPostShader(shaderIndex).SetParameter("SCREEN_TEXTURE", tex); + } } // check that sprite size is valid @@ -365,6 +370,16 @@ private void DrawEntities(Viewport viewport, Box2Rotated worldBounds, Box2 world name: nameof(entityPostRenderTarget)); } + if (entry.Sprite.PostShaderCount > 1 && + (entityPostRenderTarget2 == null + || entityPostRenderTarget2.Size.X < entityPostRenderTarget.Size.X + || entityPostRenderTarget2.Size.Y < entityPostRenderTarget.Size.Y)) + { + entityPostRenderTarget2 = CreateRenderTarget(entityPostRenderTarget.Size, + new RenderTargetFormatParameters(RenderTargetColorFormat.Rgba8Srgb, true), + name: nameof(entityPostRenderTarget2)); + } + _renderHandle.UseRenderTarget(entityPostRenderTarget); _renderHandle.Clear(default, 0, ClearBufferMask.ColorBufferBit | ClearBufferMask.StencilBufferBit); @@ -379,34 +394,66 @@ private void DrawEntities(Viewport viewport, Box2Rotated worldBounds, Box2 world if (entry.Sprite.RaiseShaderEvent) _entityManager.EventBus.RaiseLocalEvent(entry.Uid, new BeforePostShaderRenderEvent(entry.Sprite, viewport), false); + + applyPostShaders = true; } } spriteSystem.RenderSprite(new(entry.Uid, entry.Sprite), _renderHandle.DrawingHandleWorld, eye.Rotation, entry.WorldRot, entry.WorldPos); - if (entry.Sprite.PostShader != null && entityPostRenderTarget != null) + if (applyPostShaders && entityPostRenderTarget != null) { var oldProj = _currentMatrixProj; var oldView = _currentMatrixView; + _renderHandle.SetModelTransform(Matrix3x2.Identity); - _renderHandle.UseRenderTarget(viewport.RenderTarget); - _renderHandle.Viewport(Box2i.FromDimensions(Vector2i.Zero, screenSize)); + var source = entityPostRenderTarget; + for (var shaderIndex = 0; shaderIndex < entry.Sprite.PostShaderCount; shaderIndex++) + { + var finalShader = shaderIndex == entry.Sprite.PostShaderCount - 1; + Box2i box; - _renderHandle.UseShader(entry.Sprite.PostShader); - CalcScreenMatrices(viewport.Size, out var proj, out var view); - _renderHandle.SetProjView(proj, view); - _renderHandle.SetModelTransform(Matrix3x2.Identity); + if (finalShader) + { + _renderHandle.UseRenderTarget(viewport.RenderTarget); + _renderHandle.Viewport(Box2i.FromDimensions(Vector2i.Zero, screenSize)); + CalcScreenMatrices(viewport.Size, out var proj, out var view); + _renderHandle.SetProjView(proj, view); - var rounded = roundedPos - entityPostRenderTarget.Size / 2; + var rounded = roundedPos - source.Size / 2; + box = Box2i.FromDimensions(rounded, source.Size); + } + else + { + DebugTools.Assert(entityPostRenderTarget2 != null); + var destination = ReferenceEquals(source, entityPostRenderTarget) + ? entityPostRenderTarget2 + : entityPostRenderTarget; + + _renderHandle.UseRenderTarget(destination); + _renderHandle.Viewport(Box2i.FromDimensions(Vector2i.Zero, destination.Size)); + _renderHandle.Clear(default, 0, + ClearBufferMask.ColorBufferBit | ClearBufferMask.StencilBufferBit); + CalcScreenMatrices(destination.Size, out var proj, out var view); + _renderHandle.SetProjView(proj, view); + box = Box2i.FromDimensions(Vector2i.Zero, source.Size); + } - var box = Box2i.FromDimensions(rounded, entityPostRenderTarget.Size); + _renderHandle.UseShader(entry.Sprite.GetPostShader(shaderIndex)); + _renderHandle.DrawTextureScreen(source.Texture, + box.BottomLeft, box.BottomRight, box.TopLeft, box.TopRight, + Color.White, null); + _renderHandle.UseShader(null); - _renderHandle.DrawTextureScreen(entityPostRenderTarget.Texture, - box.BottomLeft, box.BottomRight, box.TopLeft, box.TopRight, - Color.White, null); + if (!finalShader) + { + source = ReferenceEquals(source, entityPostRenderTarget) + ? entityPostRenderTarget2! + : entityPostRenderTarget; + } + } _renderHandle.SetProjView(oldProj, oldView); - _renderHandle.UseShader(null); } } @@ -424,6 +471,7 @@ private void DrawEntities(Viewport viewport, Box2Rotated worldBounds, Box2 world ArrayPool.Shared.Return(indexList); entityPostRenderTarget?.DisposeDeferred(); + entityPostRenderTarget2?.DisposeDeferred(); _debugStats.Entities += _drawingSpriteList.Count; _drawingSpriteList.Clear();