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]; + } +}