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();