diff --git a/Robust.Client/GameObjects/Components/Renderable/SpriteComponent.cs b/Robust.Client/GameObjects/Components/Renderable/SpriteComponent.cs
index 5ddd6c623be..cc385f16395 100644
--- a/Robust.Client/GameObjects/Components/Renderable/SpriteComponent.cs
+++ b/Robust.Client/GameObjects/Components/Renderable/SpriteComponent.cs
@@ -202,34 +202,10 @@ public bool ContainerOccluded
[ViewVariables(VVAccess.ReadWrite)] internal bool _inertUpdateQueued;
///
- /// Shader instance to use when drawing the final sprite to the world.
- ///
- [ViewVariables(VVAccess.ReadWrite)]
- public ShaderInstance? PostShader
- {
- 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;
- }
-
- ///
- /// Whether to pass the screen texture to the .
- ///
- ///
- /// Should be false unless you really need it.
- ///
- [DataField]
- public bool GetScreenTexture;
-
- ///
- /// If true, this raise a entity system event before rendering this sprite, allowing systems to modify the
- /// shader parameters. Usually this can just be done via a frame-update, but some shaders require
- /// information about the viewport / eye.
+ /// Shaders to use when drawing the final sprite to the world.
///
[DataField]
- public bool RaiseShaderEvent;
+ public SortedDictionary PostShaders = new();
[ViewVariables] internal Dictionary LayerMap { get; set; } = new();
[ViewVariables] internal List Layers = new();
diff --git a/Robust.Client/GameObjects/Components/Renderable/SpriteShaderData.cs b/Robust.Client/GameObjects/Components/Renderable/SpriteShaderData.cs
new file mode 100644
index 00000000000..fdb6a2257d3
--- /dev/null
+++ b/Robust.Client/GameObjects/Components/Renderable/SpriteShaderData.cs
@@ -0,0 +1,63 @@
+using Robust.Client.Graphics;
+using Robust.Shared.Maths;
+using Robust.Shared.Prototypes;
+using Robust.Shared.Serialization.Manager.Attributes;
+
+namespace Robust.Client.GameObjects;
+
+[DataDefinition]
+public sealed partial class SpriteShaderData(
+ ProtoId protoId,
+ bool raiseShaderEvent = false,
+ bool getScreenTexture = false,
+ bool mutable = true,
+ Color? color = null,
+ ShaderInstance? instance = null)
+{
+ public SpriteShaderData() : this(default)
+ {
+ }
+
+ ///
+ /// Proto ID of the shader.
+ ///
+ [DataField]
+ public ProtoId ProtoId = protoId;
+
+ ///
+ /// If true, this raise a entity system event before rendering this sprite, allowing systems to modify the
+ /// shader parameters. Usually this can just be done via a frame-update, but some shaders require
+ /// information about the viewport / eye.
+ ///
+ [DataField]
+ public bool RaiseShaderEvent = raiseShaderEvent;
+
+ ///
+ /// Whether to pass the screen texture to the .
+ ///
+ ///
+ /// Should be false unless you really need it.
+ ///
+ [DataField]
+ public bool GetScreenTexture = getScreenTexture;
+
+ ///
+ /// Is the shader mutable
+ ///
+ [DataField]
+ public bool Mutable = mutable;
+
+ ///
+ /// Color to apply when rendering shader
+ ///
+ [DataField]
+ public Color? Color = color;
+
+ public ShaderInstance? Instance = instance;
+
+ public SpriteShaderData Copy()
+ {
+ var shader = Instance is not { Disposed: true } ? Instance?.Mutable is true ? Instance.Duplicate() : Instance : null;
+ return new SpriteShaderData(ProtoId, RaiseShaderEvent, GetScreenTexture, Mutable, Color, shader);
+ }
+}
diff --git a/Robust.Client/GameObjects/EntitySystems/SpriteSystem.Component.cs b/Robust.Client/GameObjects/EntitySystems/SpriteSystem.Component.cs
index f96f1bb9ede..49bb10fed1e 100644
--- a/Robust.Client/GameObjects/EntitySystems/SpriteSystem.Component.cs
+++ b/Robust.Client/GameObjects/EntitySystems/SpriteSystem.Component.cs
@@ -82,9 +82,6 @@ in target
target.Comp.IsInert = source.Comp.IsInert;
target.Comp.LayerMap = source.Comp.LayerMap.ShallowClone();
- target.Comp.PostShader = source.Comp.PostShader is {Mutable: true}
- ? source.Comp.PostShader.Duplicate()
- : source.Comp.PostShader;
target.Comp.RenderOrder = source.Comp.RenderOrder;
target.Comp.GranularLayersRendering = source.Comp.GranularLayersRendering;
diff --git a/Robust.Client/GameObjects/EntitySystems/SpriteSystem.cs b/Robust.Client/GameObjects/EntitySystems/SpriteSystem.cs
index 0780a670f15..de61a237c88 100644
--- a/Robust.Client/GameObjects/EntitySystems/SpriteSystem.cs
+++ b/Robust.Client/GameObjects/EntitySystems/SpriteSystem.cs
@@ -230,18 +230,14 @@ public Texture GetFrame(SpriteSpecifier spriteSpec, TimeSpan curTime, bool loop
}
}
+
///
/// This event gets raised before a sprite gets drawn using it's post-shader.
///
- public sealed class BeforePostShaderRenderEvent : EntityEventArgs
- {
- public readonly SpriteComponent Sprite;
- public readonly IClydeViewport Viewport;
-
- public BeforePostShaderRenderEvent(SpriteComponent sprite, IClydeViewport viewport)
- {
- Sprite = sprite;
- Viewport = viewport;
- }
- }
+ [ByRefEvent]
+ public readonly record struct BeforePostShaderRenderEvent(
+ ProtoId Shader,
+ ShaderInstance Instance,
+ SpriteComponent Sprite,
+ IClydeViewport Viewport);
}
diff --git a/Robust.Client/Graphics/Clyde/Clyde.Constants.cs b/Robust.Client/Graphics/Clyde/Clyde.Constants.cs
index 161095556b3..b41f34c48b8 100644
--- a/Robust.Client/Graphics/Clyde/Clyde.Constants.cs
+++ b/Robust.Client/Graphics/Clyde/Clyde.Constants.cs
@@ -15,13 +15,15 @@ private static readonly (string, uint)[] BaseShaderAttribLocations =
private const int UniITexturePixelSize = 2;
private const int UniIMainTexture = 3;
private const int UniILightTexture = 4;
- private const int UniCount = 5;
+ private const int UniIFragCoordOffset = 5;
+ private const int UniCount = 6;
private const string UniModUV = "modifyUV";
private const string UniModelMatrix = "modelMatrix";
private const string UniTexturePixelSize = "TEXTURE_PIXEL_SIZE";
private const string UniMainTexture = "TEXTURE";
private const string UniLightTexture = "lightMap"; // TODO CLYDE consistent shader variable naming
+ private const string UniFragCoordOffset = "FragCoordOffset";
private const string UniProjViewMatrices = "projectionViewMatrices";
private const string UniUniformConstants = "uniformConstants";
diff --git a/Robust.Client/Graphics/Clyde/Clyde.GridRendering.cs b/Robust.Client/Graphics/Clyde/Clyde.GridRendering.cs
index 933eb05b774..7d989a17e95 100644
--- a/Robust.Client/Graphics/Clyde/Clyde.GridRendering.cs
+++ b/Robust.Client/Graphics/Clyde/Clyde.GridRendering.cs
@@ -83,6 +83,7 @@ private void _drawGrids(Viewport viewport, Box2 worldAABB, Box2Rotated worldBoun
gridProgram.SetUniformTextureMaybe(UniIMainTexture, TextureUnit.Texture0);
gridProgram.SetUniformTextureMaybe(UniILightTexture, TextureUnit.Texture1);
gridProgram.SetUniform(UniIModUV, new Vector4(0, 0, 1, 1));
+ gridProgram.SetUniformMaybe(UniIFragCoordOffset, new Vector2(0, 0));
}
gridProgram.SetUniform(UniIModelMatrix, _transformSystem.GetWorldMatrix(mapGrid));
diff --git a/Robust.Client/Graphics/Clyde/Clyde.HLR.cs b/Robust.Client/Graphics/Clyde/Clyde.HLR.cs
index 09ddbacdf48..1f268c8b33f 100644
--- a/Robust.Client/Graphics/Clyde/Clyde.HLR.cs
+++ b/Robust.Client/Graphics/Clyde/Clyde.HLR.cs
@@ -16,6 +16,7 @@
using Robust.Shared.Maths;
using Robust.Shared.Profiling;
using Robust.Shared.Utility;
+using SixLabors.ImageSharp.PixelFormats;
namespace Robust.Client.Graphics.Clyde
{
@@ -33,6 +34,9 @@ internal partial class Clyde
public static float PostShadeScale = 1.25f;
private List _overlays = new();
+ private readonly List<(ShaderInstance, Color)> _shadersToRender = new();
+ private readonly HashSet _rtSizes = new();
+ private readonly Dictionary _entityPostRenderTargets = new();
public void Render()
{
@@ -308,7 +312,8 @@ private void DrawEntities(Viewport viewport, Box2Rotated worldBounds, Box2 world
var screenSize = viewport.Size;
var overlayIndex = 0;
- RenderTexture? entityPostRenderTarget = null;
+ _rtSizes.Clear();
+
bool flushed = false;
for (var i = 0; i < _drawingSpriteList.Count; i++)
{
@@ -333,98 +338,195 @@ private void DrawEntities(Viewport viewport, Box2Rotated worldBounds, Box2 world
RenderSingleWorldOverlay(overlay, viewport, OverlaySpace.WorldSpaceEntities, worldAABB, worldBounds);
}
- 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++;
+
+ // check that sprite size is valid
+ if (screenSpriteSize.X == 0 || screenSpriteSize.Y == 0)
+ continue;
+
+ _rtSizes.Add(screenSpriteSize);
+
+ // 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.
+ var roundedPos = (Vector2i)entry.SpriteScreenBB.Center;
+ var flippedPos = new Vector2i(roundedPos.X, screenSize.Y - roundedPos.Y);
+ flippedPos -= screenSpriteSize / 2;
+ var offsetViewport = Box2i.FromDimensions(-flippedPos, screenSize);
+
+ if (!_entityPostRenderTargets.TryGetValue(screenSpriteSize, out var rts))
{
- // 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)
+ if (entry.Sprite.PostShaders.Count > 0)
{
- FlushRenderQueue();
- var tex = CopyScreenTexture(viewport.RenderTarget);
- if (tex == null)
- exit = true;
- else
- entry.Sprite.PostShader.SetParameter("SCREEN_TEXTURE", tex);
+ rts = CreateEntityPostRenderTargetPair(screenSpriteSize);
+ _entityPostRenderTargets.Add(screenSpriteSize, rts);
}
+ }
+ // Shouldn't really happen
+ else if (rts.rt.Size != screenSpriteSize || rts.rtIntermediate.Size != screenSpriteSize)
+ {
+ rts.rt.Dispose();
+ rts.rtIntermediate.Dispose();
+
+ rts = CreateEntityPostRenderTargetPair(screenSpriteSize);
+ _entityPostRenderTargets[screenSpriteSize] = rts;
+ }
+
+ var noScreenTexture = false;
+ Texture? screenTexture = null;
+ _shadersToRender.Clear();
+
+ foreach (var shaderData in entry.Sprite.PostShaders.Values)
+ {
+ if (rts == default)
+ continue;
- // check that sprite size is valid
- if (!exit && screenSpriteSize.X > 0 && screenSpriteSize.Y > 0)
+ if (shaderData.GetScreenTexture)
{
- // 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)
+ if (noScreenTexture)
+ continue;
+
+ if (screenTexture == null)
{
- entityPostRenderTarget = CreateRenderTarget(screenSpriteSize,
- new RenderTargetFormatParameters(RenderTargetColorFormat.Rgba8Srgb, true),
- name: nameof(entityPostRenderTarget));
+ FlushRenderQueue();
+ screenTexture = CopyScreenTexture(viewport.RenderTarget);
+ if (screenTexture == null)
+ {
+ noScreenTexture = true;
+ continue;
+ }
}
+ }
- _renderHandle.UseRenderTarget(entityPostRenderTarget);
- _renderHandle.Clear(default, 0, ClearBufferMask.ColorBufferBit | ClearBufferMask.StencilBufferBit);
+ if (shaderData.Instance is not { } instance || instance.Disposed)
+ {
+ var proto = _proto.Index(shaderData.ProtoId);
+ instance = shaderData.Mutable ? proto.InstanceUnique() : proto.Instance();
+ shaderData.Instance = instance;
+ }
- // 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 (!noScreenTexture && shaderData.GetScreenTexture && screenTexture is { } tex)
+ instance.SetParameter("SCREEN_TEXTURE", tex);
- if (entry.Sprite.RaiseShaderEvent)
- _entityManager.EventBus.RaiseLocalEvent(entry.Uid,
- new BeforePostShaderRenderEvent(entry.Sprite, viewport), false);
- }
- }
+ _shadersToRender.Add((instance, shaderData.Color ?? Color.White));
- spriteSystem.RenderSprite(new(entry.Uid, entry.Sprite), _renderHandle.DrawingHandleWorld, eye.Rotation, entry.WorldRot, entry.WorldPos);
+ if (!shaderData.RaiseShaderEvent)
+ continue;
- if (entry.Sprite.PostShader != null && entityPostRenderTarget != null)
+ var shaderEv =
+ new BeforePostShaderRenderEvent(shaderData.ProtoId, instance, entry.Sprite, viewport);
+ _entityManager.EventBus.RaiseLocalEvent(entry.Uid, ref shaderEv);
+ }
+
+ if (_shadersToRender.Count > 0)
{
- var oldProj = _currentMatrixProj;
- var oldView = _currentMatrixView;
+ _renderHandle.UseRenderTarget(rts.rt);
+ _renderHandle.Clear(default, 0, ClearBufferMask.ColorBufferBit | ClearBufferMask.StencilBufferBit);
+ _renderHandle.Viewport(offsetViewport);
+ }
- _renderHandle.UseRenderTarget(viewport.RenderTarget);
- _renderHandle.Viewport(Box2i.FromDimensions(Vector2i.Zero, screenSize));
+ spriteSystem.RenderSprite(new(entry.Uid, entry.Sprite),
+ _renderHandle.DrawingHandleWorld,
+ eye.Rotation,
+ entry.WorldRot,
+ entry.WorldPos);
- _renderHandle.UseShader(entry.Sprite.PostShader);
- CalcScreenMatrices(viewport.Size, out var proj, out var view);
- _renderHandle.SetProjView(proj, view);
- _renderHandle.SetModelTransform(Matrix3x2.Identity);
+ if (_shadersToRender.Count == 0 || rts == default)
+ continue;
- var rounded = roundedPos - entityPostRenderTarget.Size / 2;
+ var state = PushRenderStateFull();
- var box = Box2i.FromDimensions(rounded, entityPostRenderTarget.Size);
+ var rounded = roundedPos - screenSpriteSize / 2;
- _renderHandle.DrawTextureScreen(entityPostRenderTarget.Texture,
- box.BottomLeft, box.BottomRight, box.TopLeft, box.TopRight,
- Color.White, null);
+ var box = Box2i.FromDimensions(rounded, screenSpriteSize);
- _renderHandle.SetProjView(oldProj, oldView);
- _renderHandle.UseShader(null);
+ CalcScreenMatrices(screenSize, out var proj, out var view);
+ _renderHandle.SetProjView(proj, view);
+ _renderHandle.SetModelTransform(Matrix3x2.Identity);
+
+ FlushRenderQueue();
+
+ foreach (var (shader, color) in _shadersToRender)
+ {
+ if (shader.Disposed)
+ continue;
+
+ BlitRenderTarget((RenderTexture) rts.rt, (RenderTexture) rts.rtIntermediate);
+
+ _renderHandle.UseRenderTarget(rts.rt);
+ _renderHandle.Viewport(offsetViewport);
+ _renderHandle.SetFragCoordOffset((Vector2) flippedPos);
+ _renderHandle.Clear(default, 0, ClearBufferMask.ColorBufferBit | ClearBufferMask.StencilBufferBit);
+
+ // This prevents shadows applying over and over again on each iteration. We don't want to blend shadows.
+ // Could disable blending entirely temporarily, but this would affect all shaders, including default
+ // one, which would result in black squares
+ var clydeShader = (ClydeShaderInstance) shader;
+ var loadedInst = _shaderInstances[clydeShader.Handle];
+ var originalBlend = loadedInst.BlendMode;
+ loadedInst.BlendMode = ShaderBlendMode.None;
+
+ _renderHandle.UseShader(shader);
+ _renderHandle.DrawTextureScreen(rts.rtIntermediate.Texture,
+ box.BottomLeft,
+ box.BottomRight,
+ box.TopLeft,
+ box.TopRight,
+ in color,
+ null);
+
+ FlushRenderQueue();
+ loadedInst.BlendMode = originalBlend;
}
+
+ // Disable light/shadow rendering when drawing final image since shadows are already applied when
+ // rendering sprite onto render target. Alternatively, we can use unshaded shader,
+ // but it requires storing its instance somewhere and more lines of code overall
+ var oldLightingReady = _lightingReady;
+ _lightingReady = false;
+
+ _renderHandle.UseShader(null);
+ _renderHandle.UseRenderTarget(viewport.RenderTarget);
+ _renderHandle.Viewport(Box2i.FromDimensions(Vector2i.Zero, screenSize));
+
+ CalcScreenMatrices(viewport.Size, out proj, out view);
+ _renderHandle.SetProjView(proj, view);
+ _renderHandle.SetModelTransform(Matrix3x2.Identity);
+ _renderHandle.SetFragCoordOffset(Vector2.Zero);
+
+ _renderHandle.DrawTextureScreen(rts.rt.Texture,
+ box.BottomLeft,
+ box.BottomRight,
+ box.TopLeft,
+ box.TopRight,
+ Color.White,
+ null);
+
+ FlushRenderQueue();
+
+ _lightingReady = oldLightingReady;
+
+ PopRenderStateFull(state);
+ }
+
+ foreach (var (size, rts) in _entityPostRenderTargets)
+ {
+ if (_rtSizes.Contains(size))
+ continue;
+
+ rts.rt.Dispose();
+ rts.rtIntermediate.Dispose();
+
+ _entityPostRenderTargets.Remove(size);
}
// draw remainder of overlays
@@ -440,13 +542,20 @@ private void DrawEntities(Viewport viewport, Box2Rotated worldBounds, Box2 world
}
ArrayPool.Shared.Return(indexList);
- entityPostRenderTarget?.DisposeDeferred();
_debugStats.Entities += _drawingSpriteList.Count;
_drawingSpriteList.Clear();
FlushRenderQueue();
}
+ private (IRenderTexture, IRenderTexture) CreateEntityPostRenderTargetPair(Vector2i size)
+ {
+ var format = new RenderTargetFormatParameters(RenderTargetColorFormat.Rgba8Srgb, true);
+ var rt = CreateRenderTarget(size, format, name: $"entityPostRenderTarget_{size}");
+ var rtIntermediate = CreateRenderTarget(size, format, name: $"entityPostRenderTargetIntermediate_{size}");
+ return (rt, rtIntermediate);
+ }
+
private void DrawLoadingScreen(IRenderHandle handle)
{
ClearFramebuffer(Color.Black);
@@ -629,5 +738,16 @@ private static Box2Rotated CalcWorldBounds(Viewport viewport)
return new Box2Rotated(aabb, rotation, aabb.Center);
}
+
+ private void ShutdownHLR()
+ {
+ foreach (var (rt, rtIntermediate) in _entityPostRenderTargets.Values)
+ {
+ rt.Dispose();
+ rtIntermediate.Dispose();
+ }
+
+ _entityPostRenderTargets.Clear();
+ }
}
}
diff --git a/Robust.Client/Graphics/Clyde/Clyde.Helpers.cs b/Robust.Client/Graphics/Clyde/Clyde.Helpers.cs
index a71b0e827ee..df00ecc9bf0 100644
--- a/Robust.Client/Graphics/Clyde/Clyde.Helpers.cs
+++ b/Robust.Client/Graphics/Clyde/Clyde.Helpers.cs
@@ -70,6 +70,24 @@ private static long EstPixelSize(PixelInternalFormat format)
};
}
+ private void BlitRenderTarget(RenderTexture src, RenderTexture dst)
+ {
+ var srcLoaded = RtToLoaded(src);
+ var dstLoaded = RtToLoaded(dst);
+
+ GL.BindFramebuffer(FramebufferTarget.ReadFramebuffer, srcLoaded.FramebufferHandle.Handle);
+ CheckGlError();
+ GL.BindFramebuffer(FramebufferTarget.DrawFramebuffer, dstLoaded.FramebufferHandle.Handle);
+ CheckGlError();
+ GL.BlitFramebuffer(
+ 0, 0, src.Size.X, src.Size.Y,
+ 0, 0, dst.Size.X, dst.Size.Y,
+ ClearBufferMask.ColorBufferBit,
+ BlitFramebufferFilter.Nearest);
+ CheckGlError();
+ }
+
+
// Sets up uniforms (It'd be nice to move this, or make some contextual stuff implicit, but things got complicated.)
[MethodImpl(MethodImplOptions.AggressiveInlining)]
private void SetupGlobalUniformsImmediate(GLShaderProgram program, ClydeTexture? tex)
diff --git a/Robust.Client/Graphics/Clyde/Clyde.RenderHandle.cs b/Robust.Client/Graphics/Clyde/Clyde.RenderHandle.cs
index 82efebf4bac..1d7533518ea 100644
--- a/Robust.Client/Graphics/Clyde/Clyde.RenderHandle.cs
+++ b/Robust.Client/Graphics/Clyde/Clyde.RenderHandle.cs
@@ -47,6 +47,16 @@ public Matrix3x2 GetModelTransform()
return _clyde.DrawGetModelTransform();
}
+ public void SetFragCoordOffset(Vector2 offset)
+ {
+ _clyde.DrawSetFragCoordOffset(offset);
+ }
+
+ public Vector2 GetFragCoordOffset()
+ {
+ return _clyde.DrawGetFragCoordOffset();
+ }
+
public void SetProjView(in Matrix3x2 proj, in Matrix3x2 view)
{
_clyde.DrawSetProjViewTransform(proj, view);
diff --git a/Robust.Client/Graphics/Clyde/Clyde.Rendering.cs b/Robust.Client/Graphics/Clyde/Clyde.Rendering.cs
index 7d6f67220c2..23ace08bcf0 100644
--- a/Robust.Client/Graphics/Clyde/Clyde.Rendering.cs
+++ b/Robust.Client/Graphics/Clyde/Clyde.Rendering.cs
@@ -62,6 +62,9 @@ internal partial class Clyde
// For DrawPrimitives OTOH the model matrix is passed along with the render command so is applied in the shader.
private Matrix3x2 _currentMatrixModel = Matrix3x2.Identity;
+ // FRACOORD offset for fragment shaders
+ private Vector2 _currentFragCoordOffset = Vector2.Zero;
+
// Buffers and data for the batching system. Written into during (queue) and processed during (submit).
private readonly Vertex2D[] BatchVertexData = new Vertex2D[MaxBatchQuads * 4];
@@ -299,6 +302,8 @@ private void DrawCommandBatch(ref RenderCommandDrawBatch command)
program.SetUniformMaybe(UniITexturePixelSize, Vector2.One / loadedTexture.Size);
+ program.SetUniformMaybe(UniIFragCoordOffset, command.FragCoordOffset);
+
SetBlendFunc(loaded.BlendMode);
var primitiveType = MapPrimitiveType(command.PrimitiveType);
@@ -582,6 +587,16 @@ private Matrix3x2 DrawGetModelTransform()
return _currentMatrixModel;
}
+ private void DrawSetFragCoordOffset(Vector2 offset)
+ {
+ _currentFragCoordOffset = offset;
+ }
+
+ private Vector2 DrawGetFragCoordOffset()
+ {
+ return _currentFragCoordOffset;
+ }
+
private void DrawSetProjViewTransform(in Matrix3x2 proj, in Matrix3x2 view)
{
BreakBatch();
@@ -841,6 +856,7 @@ private void DrawPrimitives(DrawPrimitiveTopology primitiveTopology, ClydeHandle
command.DrawBatch.Count = indices.Length;
command.DrawBatch.ModelMatrix = _currentMatrixModel;
+ command.DrawBatch.FragCoordOffset = _currentFragCoordOffset;
_debugStats.LastBatches += 1;
_debugStats.LastClydeDrawCalls += 1;
@@ -867,6 +883,7 @@ private void DrawPrimitives(DrawPrimitiveTopology primitiveTopology, ClydeHandle
command.DrawBatch.Count = vertices.Length;
command.DrawBatch.ModelMatrix = _currentMatrixModel;
+ command.DrawBatch.FragCoordOffset = _currentFragCoordOffset;
_debugStats.LastBatches += 1;
_debugStats.LastClydeDrawCalls += 1;
@@ -1025,6 +1042,7 @@ private void FinishBatch()
command.DrawBatch.Count = currentIndex - metaData.StartIndex;
command.DrawBatch.ModelMatrix = Matrix3x2.Identity;
+ command.DrawBatch.FragCoordOffset = _currentFragCoordOffset;
_debugStats.LastBatches += 1;
}
@@ -1196,6 +1214,8 @@ private struct RenderCommandDrawBatch
// TODO: this makes the render commands so much more large please remove.
public Matrix3x2 ModelMatrix;
+
+ public Vector2 FragCoordOffset;
}
private struct RenderCommandProjViewMatrix
diff --git a/Robust.Client/Graphics/Clyde/Clyde.cs b/Robust.Client/Graphics/Clyde/Clyde.cs
index fc03dd5ed80..986a4d7a57f 100644
--- a/Robust.Client/Graphics/Clyde/Clyde.cs
+++ b/Robust.Client/Graphics/Clyde/Clyde.cs
@@ -582,6 +582,7 @@ public void Shutdown()
{
_glContext?.Shutdown();
ShutdownWindowing();
+ ShutdownHLR();
}
private bool IsMainThread()
diff --git a/Robust.Client/Graphics/Clyde/GLObjects/Clyde.ShaderProgram.cs b/Robust.Client/Graphics/Clyde/GLObjects/Clyde.ShaderProgram.cs
index c70ec3734f1..2065e8f4f6f 100644
--- a/Robust.Client/Graphics/Clyde/GLObjects/Clyde.ShaderProgram.cs
+++ b/Robust.Client/Graphics/Clyde/GLObjects/Clyde.ShaderProgram.cs
@@ -191,6 +191,9 @@ private bool InitIntUniform(int id, out int index)
case UniITexturePixelSize:
name = UniTexturePixelSize;
break;
+ case UniIFragCoordOffset:
+ name = UniFragCoordOffset;
+ break;
default:
throw new ArgumentOutOfRangeException();
}
diff --git a/Robust.Client/Graphics/Clyde/Shaders/base-default.frag b/Robust.Client/Graphics/Clyde/Shaders/base-default.frag
index df99a6dd7a5..7e0674a0997 100644
--- a/Robust.Client/Graphics/Clyde/Shaders/base-default.frag
+++ b/Robust.Client/Graphics/Clyde/Shaders/base-default.frag
@@ -19,11 +19,14 @@ varying highp vec4 VtxModulate;
// TODO CLYDE consistent shader variable naming
uniform sampler2D lightMap;
+// Offset to FRAGCOORD.xy
+uniform highp vec2 FragCoordOffset;
+
// [SHADER_HEADER_CODE]
void main()
{
- highp vec4 FRAGCOORD = gl_FragCoord;
+ highp vec4 FRAGCOORD = gl_FragCoord + vec4(FragCoordOffset, 0.0, 0.0);
// The output colour. This should get set by the shader code block.
// This will get modified by the LIGHT and MODULATE vectors.
diff --git a/Robust.Client/Graphics/Clyde/Shaders/base-raw.frag b/Robust.Client/Graphics/Clyde/Shaders/base-raw.frag
index 1033ac07a7a..3d1246543ad 100644
--- a/Robust.Client/Graphics/Clyde/Shaders/base-raw.frag
+++ b/Robust.Client/Graphics/Clyde/Shaders/base-raw.frag
@@ -4,11 +4,13 @@ varying highp vec2 UV2;
// TODO CLYDE consistent shader variable naming
uniform sampler2D lightMap;
+uniform highp vec2 FragCoordOffset;
+
// [SHADER_HEADER_CODE]
void main()
{
- highp vec4 FRAGCOORD = gl_FragCoord;
+ highp vec4 FRAGCOORD = gl_FragCoord + vec4(FragCoordOffset, 0.0, 0.0);
lowp vec4 COLOR = vec4(0.0);
diff --git a/Robust.Shared/GameObjects/Components/Renderable/SpriteShaderKey.cs b/Robust.Shared/GameObjects/Components/Renderable/SpriteShaderKey.cs
new file mode 100644
index 00000000000..c51a4c85f13
--- /dev/null
+++ b/Robust.Shared/GameObjects/Components/Renderable/SpriteShaderKey.cs
@@ -0,0 +1,45 @@
+using System;
+using Robust.Shared.Serialization.TypeSerializers.Implementations;
+using Robust.Shared.ViewVariables;
+
+namespace Robust.Shared.GameObjects;
+
+///
+/// Key for sprite shaders SortedDictionary
+///
+/// The id of the shader. Used for equality checks and hash comparison
+/// Render order of the shader, shaders with higher render order will draw later
+///
+/// This will be automatically validated by .
+/// It is serialized into an integer which represents RenderOrder, id is automatically generated in this case
+///
+[Serializable]
+public readonly struct SpriteShaderKey(string id, int renderOrder = 0)
+ : IEquatable, IComparable
+{
+ [ViewVariables]
+ public readonly string Id = id;
+
+ [ViewVariables]
+ public readonly int RenderOrder = renderOrder;
+
+ public bool Equals(SpriteShaderKey other)
+ {
+ return Id == other.Id;
+ }
+
+ public override bool Equals(object? obj)
+ {
+ return obj is SpriteShaderKey other && Equals(other);
+ }
+
+ public override int GetHashCode()
+ {
+ return Id.GetHashCode();
+ }
+
+ public int CompareTo(SpriteShaderKey other)
+ {
+ return Id == other.Id ? 0 : RenderOrder.CompareTo(other.RenderOrder);
+ }
+}
diff --git a/Robust.Shared/Serialization/TypeSerializers/Implementations/SpriteShaderKeySerializer.cs b/Robust.Shared/Serialization/TypeSerializers/Implementations/SpriteShaderKeySerializer.cs
new file mode 100644
index 00000000000..0455e198065
--- /dev/null
+++ b/Robust.Shared/Serialization/TypeSerializers/Implementations/SpriteShaderKeySerializer.cs
@@ -0,0 +1,67 @@
+using System;
+using System.Globalization;
+using Robust.Shared.GameObjects;
+using Robust.Shared.IoC;
+using Robust.Shared.Serialization.Manager;
+using Robust.Shared.Serialization.Manager.Attributes;
+using Robust.Shared.Serialization.Markdown;
+using Robust.Shared.Serialization.Markdown.Validation;
+using Robust.Shared.Serialization.Markdown.Value;
+using Robust.Shared.Serialization.TypeSerializers.Interfaces;
+using Robust.Shared.Utility;
+
+namespace Robust.Shared.Serialization.TypeSerializers.Implementations;
+
+[TypeSerializer]
+public sealed class SpriteShaderKeySerializer :
+ ITypeSerializer,
+ ITypeCopyCreator
+{
+ public ValidationNode Validate(
+ ISerializationManager serializationManager,
+ ValueDataNode node,
+ IDependencyCollection dependencies,
+ ISerializationContext? context = null)
+ {
+ return Parse.TryInt32(node.Value, out _)
+ ? new ValidatedValueNode(node)
+ : new ErrorNode(node, $"Failed parsing sprite shader key render order (int) value: {node.Value}");
+ }
+
+ public SpriteShaderKey Read(
+ ISerializationManager serializationManager,
+ ValueDataNode node,
+ IDependencyCollection dependencies,
+ SerializationHookContext hookCtx,
+ ISerializationContext? context = null,
+ ISerializationManager.InstantiationDelegate? instanceProvider = null)
+ {
+ return new SpriteShaderKey(GenerateAutoId(), Parse.Int32(node.Value));
+ }
+
+ public DataNode Write(
+ ISerializationManager serializationManager,
+ SpriteShaderKey value,
+ IDependencyCollection dependencies,
+ bool alwaysWrite = false,
+ ISerializationContext? context = null)
+ {
+ return new ValueDataNode(value.RenderOrder.ToString(CultureInfo.InvariantCulture));
+ }
+
+ public SpriteShaderKey CreateCopy(
+ ISerializationManager serializationManager,
+ SpriteShaderKey source,
+ IDependencyCollection dependencies,
+ SerializationHookContext hookCtx,
+ ISerializationContext? context = null)
+ {
+ return new SpriteShaderKey(source.Id, source.RenderOrder);
+ }
+
+ private static string GenerateAutoId()
+ {
+ // Generate new guid and take 8 chars
+ return $"{Guid.NewGuid():N}"[..8];
+ }
+}