Update postshaders for multi-shader support (#44735)

This commit is contained in:
metalgearsloth 2026-08-01 17:05:21 +10:00 • committed by GitHub
parent 066a4f9b58
commit b822e090ef
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
10 changed files with 256 additions and 99 deletions

View file

@ -0,0 +1,25 @@
namespace Content.Client.Graphics;
public static class ContentPostShaderIds
{
public const string Stealth = "stealth";
public const string FloorOcclusion = "floor-occlusion";
public const string Holopad = "holopad";
public const string InteractionOutline = "interaction-outline";
public const string TargetOutline = "target-outline";
public const string DragDropOutline = "drag-drop-outline";
public static readonly string[] BeforeOutlines =
{
InteractionOutline,
TargetOutline,
DragDropOutline,
};
public static readonly string[] AfterBaseEffects =
{
Stealth,
FloorOcclusion,
Holopad,
};
}

View file

@ -1,3 +1,4 @@
using Content.Client.Graphics;
using Content.Shared.Chat.TypingIndicator;
using Content.Shared.Holopad;
using Robust.Client.GameObjects;
@ -30,10 +31,10 @@ public sealed partial class HolopadSystem : SharedHolopadSystem
private void OnShaderRender(Entity<HolopadHologramComponent> entity, ref BeforePostShaderRenderEvent ev)
{
if (ev.Sprite.PostShader == null)
if (ev.Id != ContentPostShaderIds.Holopad)
return;
UpdateHologramSprite(entity, entity.Comp.LinkedEntity);
UpdateHologramSprite(entity, entity.Comp.LinkedEntity, ev.Shader);
}
private void OnTypingChanged(TypingChangedEvent ev, EntitySessionEventArgs args)
@ -50,7 +51,7 @@ public sealed partial class HolopadSystem : SharedHolopadSystem
RaiseNetworkEvent(netEv);
}
private void UpdateHologramSprite(EntityUid hologram, EntityUid? target)
private void UpdateHologramSprite(EntityUid hologram, EntityUid? target, ShaderInstance? shader = null)
{
// Get required components
if (!TryComp<SpriteComponent>(hologram, out var hologramSprite) ||
@ -110,15 +111,19 @@ public sealed partial class HolopadSystem : SharedHolopadSystem
hologramSprite.LayerSetShader(i, "unshaded");
}
UpdateHologramShader(hologram, hologramSprite, holopadhologram);
UpdateHologramShader(hologram, hologramSprite, holopadhologram, shader);
}
private void UpdateHologramShader(EntityUid uid, SpriteComponent sprite, HolopadHologramComponent holopadHologram)
private void UpdateHologramShader(
EntityUid uid,
SpriteComponent sprite,
HolopadHologramComponent holopadHologram,
ShaderInstance? shader = null)
{
// Find the texture height of the largest layer
float texHeight = sprite.AllLayers.Max(x => x.PixelSize.Y);
var instance = ProtoMan.Index<ShaderPrototype>(holopadHologram.ShaderName).InstanceUnique();
var instance = shader ?? ProtoMan.Index<ShaderPrototype>(holopadHologram.ShaderName).InstanceUnique();
instance.SetParameter("color1", new Vector3(holopadHologram.Color1.R, holopadHologram.Color1.G, holopadHologram.Color1.B));
instance.SetParameter("color2", new Vector3(holopadHologram.Color2.R, holopadHologram.Color2.G, holopadHologram.Color2.B));
instance.SetParameter("alpha", holopadHologram.Alpha);
@ -126,7 +131,10 @@ public sealed partial class HolopadSystem : SharedHolopadSystem
instance.SetParameter("texHeight", texHeight);
instance.SetParameter("t", (float)_timing.CurTime.TotalSeconds * holopadHologram.ScrollRate);
sprite.PostShader = instance;
sprite.RaiseShaderEvent = true;
_sprite.SetPostShader((uid, sprite), new SpriteComponent.PostShaderArgs(ContentPostShaderIds.Holopad, instance)
{
RaiseShaderEvent = true,
Before = ContentPostShaderIds.BeforeOutlines,
});
}
}

View file

@ -1,4 +1,9 @@
namespace Content.Client.Interactable.Components;
[RegisterComponent]
public sealed partial class InteractionOutlineComponent : Component;
public sealed partial class InteractionOutlineComponent : Component
{
public bool InRange;
public int LastRenderScale;
public bool Active;
}

View file

@ -1,6 +1,7 @@
using System.Numerics;
using Content.Client.CombatMode;
using Content.Client.Gameplay;
using Content.Client.Graphics;
using Content.Client.Outline;
using Content.Shared.ActionBlocker;
using Content.Shared.CCVar;
@ -101,7 +102,8 @@ public sealed partial class DragDropSystem : SharedDragDropSystem
private ShaderInstance? _dropTargetInRangeShader;
private ShaderInstance? _dropTargetOutOfRangeShader;
private readonly List<SpriteComponent> _highlightedSprites = new();
private readonly HashSet<SpriteComponent> _highlightedSprites = new();
private readonly HashSet<SpriteComponent> _nextHighlightedSprites = new();
public override void Initialize()
{
@ -422,8 +424,7 @@ public sealed partial class DragDropSystem : SharedDragDropSystem
// highlights the possible targets which are visible
// and able to be dropped on by the current dragged entity
// remove current highlights
RemoveHighlights();
_nextHighlightedSprites.Clear();
// find possible targets on screen even if not reachable
// TODO: Duplicated in SpriteSystem and TargetOutlineSystem. Should probably be cached somewhere for a frame?
@ -455,32 +456,52 @@ public sealed partial class DragDropSystem : SharedDragDropSystem
&& _interactionSystem.InRangeUnobstructed(user.Value, entity);
}
if (inRangeSprite.PostShader != null &&
inRangeSprite.PostShader != _dropTargetInRangeShader &&
inRangeSprite.PostShader != _dropTargetOutOfRangeShader)
{
continue;
}
// highlight depending on whether its in or out of range
inRangeSprite.PostShader = valid.Value ? _dropTargetInRangeShader : _dropTargetOutOfRangeShader;
SetDragDropPostShader((entity, inRangeSprite), valid.Value ? _dropTargetInRangeShader! : _dropTargetOutOfRangeShader!);
inRangeSprite.RenderOrder = EntityManager.CurrentTick.Value;
_highlightedSprites.Add(inRangeSprite);
_nextHighlightedSprites.Add(inRangeSprite);
}
foreach (var highlightedSprite in _highlightedSprites)
{
if (_nextHighlightedSprites.Contains(highlightedSprite))
continue;
_sprite.RemovePostShader(highlightedSprite, ContentPostShaderIds.DragDropOutline);
highlightedSprite.RenderOrder = 0;
}
_highlightedSprites.Clear();
foreach (var highlightedSprite in _nextHighlightedSprites)
{
_highlightedSprites.Add(highlightedSprite);
}
}
private void SetDragDropPostShader(Entity<SpriteComponent?> sprite, ShaderInstance shader)
{
if (_sprite.TryGetPostShader(sprite, ContentPostShaderIds.DragDropOutline, out var entry) &&
entry.Shader == shader)
{
return;
}
_sprite.SetPostShader(sprite, new SpriteComponent.PostShaderArgs(ContentPostShaderIds.DragDropOutline, shader)
{
After = ContentPostShaderIds.AfterBaseEffects,
});
}
private void RemoveHighlights()
{
foreach (var highlightedSprite in _highlightedSprites)
{
if (highlightedSprite.PostShader != _dropTargetInRangeShader && highlightedSprite.PostShader != _dropTargetOutOfRangeShader)
continue;
highlightedSprite.PostShader = null;
_sprite.RemovePostShader(highlightedSprite, ContentPostShaderIds.DragDropOutline);
highlightedSprite.RenderOrder = 0;
}
_highlightedSprites.Clear();
_nextHighlightedSprites.Clear();
}
/// <summary>

View file

@ -1,3 +1,4 @@
using Content.Client.Graphics;
using Content.Shared.Movement.Components;
using Content.Shared.Movement.Systems;
using Robust.Client.GameObjects;
@ -11,6 +12,7 @@ public sealed partial class FloorOcclusionSystem : SharedFloorOcclusionSystem
private static readonly ProtoId<ShaderPrototype> HorizontalCut = "HorizontalCut";
[Dependency] private EntityQuery<SpriteComponent> _spriteQuery = default!;
[Dependency] private SpriteSystem _sprite = default!;
public override void Initialize()
{
@ -48,16 +50,16 @@ public sealed partial class FloorOcclusionSystem : SharedFloorOcclusionSystem
var shader = ProtoMan.Index(HorizontalCut).Instance();
if (sprite.Comp.PostShader is not null && sprite.Comp.PostShader != shader)
return;
if (enabled)
{
sprite.Comp.PostShader = shader;
_sprite.SetPostShader(sprite, new SpriteComponent.PostShaderArgs(ContentPostShaderIds.FloorOcclusion, shader)
{
Before = ContentPostShaderIds.BeforeOutlines,
});
}
else
{
sprite.Comp.PostShader = null;
_sprite.RemovePostShader(sprite, ContentPostShaderIds.FloorOcclusion);
}
}
}

View file

@ -1,5 +1,7 @@
using System.Collections.Generic;
using Content.Client.ContextMenu.UI;
using Content.Client.Gameplay;
using Content.Client.Graphics;
using Content.Client.Interactable.Components;
using Content.Client.Viewport;
using Content.Shared.CCVar;
@ -18,6 +20,11 @@ namespace Content.Client.Outline;
public sealed partial class InteractionOutlineSystem : EntitySystem
{
private static readonly ProtoId<ShaderPrototype> ShaderInRange = "SelectionOutlineInrange";
private static readonly ProtoId<ShaderPrototype> ShaderOutOfRange = "SelectionOutline";
private const float DefaultWidth = 1;
private readonly Dictionary<(bool InRange, int RenderScale), ShaderInstance> _shaderCache = new();
[Dependency] private IConfigurationManager _configManager = default!;
[Dependency] private IEyeManager _eyeManager = default!;
[Dependency] private IInputManager _inputManager = default!;
@ -25,9 +32,8 @@ public sealed partial class InteractionOutlineSystem : EntitySystem
[Dependency] private IStateManager _stateManager = default!;
[Dependency] private IUserInterfaceManager _uiManager = default!;
[Dependency] private SharedInteractionSystem _interactionSystem = default!;
[Dependency] private EntityQuery<InteractionOutlineComponent> _outlineQuery = default!;
[Dependency] private EntityQuery<SpriteComponent> _spriteQuery = default!;
[Dependency] private IPrototypeManager _prototypeManager = default!;
[Dependency] private SpriteSystem _sprite = default!;
/// <summary>
/// Whether to currently draw the outline. The outline may be temporarily disabled by other systems
@ -39,42 +45,34 @@ public sealed partial class InteractionOutlineSystem : EntitySystem
/// </summary>
private bool _cvarEnabled = true;
private const float OutlineWidth = 1;
private static readonly ProtoId<ShaderPrototype> ShaderInRange = "SelectionOutlineInrange";
private static readonly ProtoId<ShaderPrototype> ShaderOutOfRange = "SelectionOutline";
private ShaderInstance? _shaderInRange;
private ShaderInstance? _shaderOutOfRange;
private EntityUid? _lastHoveredEntity;
public override void Shutdown()
{
foreach (var shader in _shaderCache.Values)
{
shader.Dispose();
}
_shaderCache.Clear();
base.Shutdown();
}
public override void Initialize()
{
base.Initialize();
_shaderInRange = ProtoMan.Index(ShaderInRange).InstanceUnique();
_shaderOutOfRange = ProtoMan.Index(ShaderOutOfRange).InstanceUnique();
Subs.CVar(_configManager, CCVars.OutlineEnabled, SetCvarEnabled);
SubscribeLocalEvent<InteractionOutlineComponent, ComponentShutdown>(OnShutdown);
UpdatesAfter.Add(typeof(SharedEyeSystem));
}
private void CleanupLastHoveredShader()
private void OnShutdown(Entity<InteractionOutlineComponent> ent, ref ComponentShutdown args)
{
var uid = _lastHoveredEntity;
_lastHoveredEntity = null;
RemoveOutline(ent);
if (!_outlineQuery.HasComp(uid))
return;
if (!_spriteQuery.TryComp(uid, out var sprite))
return;
if (sprite.PostShader != _shaderInRange && sprite.PostShader != _shaderOutOfRange)
return;
sprite.PostShader = null;
if (_lastHoveredEntity == ent.Owner)
_lastHoveredEntity = null;
}
public void SetCvarEnabled(bool cvarEnabled)
@ -86,7 +84,11 @@ public sealed partial class InteractionOutlineSystem : EntitySystem
if (_cvarEnabled)
return;
CleanupLastHoveredShader();
if (_lastHoveredEntity == null || Deleted(_lastHoveredEntity))
return;
if (TryComp(_lastHoveredEntity, out InteractionOutlineComponent? outline))
RemoveOutline((_lastHoveredEntity.Value, outline));
}
public void SetEnabled(bool enabled)
@ -101,7 +103,11 @@ public sealed partial class InteractionOutlineSystem : EntitySystem
if (enabled)
return;
CleanupLastHoveredShader();
if (_lastHoveredEntity == null || Deleted(_lastHoveredEntity))
return;
if (TryComp(_lastHoveredEntity, out InteractionOutlineComponent? outline))
RemoveOutline((_lastHoveredEntity.Value, outline));
}
public override void FrameUpdate(float frameTime)
@ -156,24 +162,106 @@ public sealed partial class InteractionOutlineSystem : EntitySystem
renderScale = _eyeManager.MainViewport.GetRenderScale();
}
if (_lastHoveredEntity != entityToClick)
{
CleanupLastHoveredShader();
_lastHoveredEntity = entityToClick;
}
if (!_outlineQuery.HasComp(entityToClick))
return;
if (!_spriteQuery.TryComp(entityToClick, out var sprite))
return;
var inRange = false;
if (localSession.AttachedEntity != null)
if (localSession.AttachedEntity != null && !Deleted(entityToClick))
inRange = _interactionSystem.InRangeUnobstructed(localSession.AttachedEntity.Value, entityToClick.Value);
var shader = sprite.PostShader = inRange ? _shaderInRange : _shaderOutOfRange;
shader?.SetParameter("outline_width", OutlineWidth * renderScale);
InteractionOutlineComponent? outline;
if (entityToClick == _lastHoveredEntity)
{
if (entityToClick != null && TryComp(entityToClick, out outline))
{
UpdateOutline((entityToClick.Value, outline), inRange, renderScale);
}
return;
}
if (_lastHoveredEntity != null && !Deleted(_lastHoveredEntity) &&
TryComp(_lastHoveredEntity, out outline))
{
RemoveOutline((_lastHoveredEntity.Value, outline));
}
_lastHoveredEntity = entityToClick;
if (_lastHoveredEntity != null && TryComp(_lastHoveredEntity, out outline))
{
AddOutline((_lastHoveredEntity.Value, outline), inRange, renderScale);
}
}
private void AddOutline(Entity<InteractionOutlineComponent> ent, bool inInteractionRange, int renderScale)
{
ent.Comp.LastRenderScale = renderScale;
ent.Comp.InRange = inInteractionRange;
ent.Comp.Active = true;
if (!TryComp(ent.Owner, out SpriteComponent? sprite))
return;
SetOutlinePostShader((ent.Owner, sprite), inInteractionRange, renderScale);
}
private void RemoveOutline(Entity<InteractionOutlineComponent> ent)
{
if (TryComp(ent.Owner, out SpriteComponent? sprite))
{
_sprite.RemovePostShader((ent.Owner, sprite), ContentPostShaderIds.InteractionOutline);
sprite.RenderOrder = 0;
}
ent.Comp.Active = false;
}
private void UpdateOutline(Entity<InteractionOutlineComponent> ent, bool inInteractionRange, int renderScale)
{
if (!TryComp(ent.Owner, out SpriteComponent? sprite)
|| !_sprite.HasPostShader((ent.Owner, sprite), ContentPostShaderIds.InteractionOutline)
|| inInteractionRange == ent.Comp.InRange && ent.Comp.LastRenderScale == renderScale)
{
return;
}
ent.Comp.InRange = inInteractionRange;
ent.Comp.LastRenderScale = renderScale;
SetOutlinePostShader((ent.Owner, sprite), ent.Comp.InRange, ent.Comp.LastRenderScale);
}
private void SetOutlinePostShader(Entity<SpriteComponent?> sprite, bool inRange, int renderScale)
{
var shader = GetShader(inRange, renderScale);
if (_sprite.TryGetPostShader(sprite, ContentPostShaderIds.InteractionOutline, out var entry) &&
entry.Shader == shader)
{
return;
}
_sprite.SetPostShader(sprite, new SpriteComponent.PostShaderArgs(ContentPostShaderIds.InteractionOutline, shader)
{
After = ContentPostShaderIds.AfterBaseEffects,
});
}
private ShaderInstance GetShader(bool inRange, int renderScale)
{
var key = (inRange, renderScale);
if (_shaderCache.TryGetValue(key, out var shader))
return shader;
shader = MakeNewShader(inRange, renderScale);
_shaderCache.Add(key, shader);
return shader;
}
private ShaderInstance MakeNewShader(bool inRange, int renderScale)
{
var shaderName = inRange ? ShaderInRange : ShaderOutOfRange;
var instance = _prototypeManager.Index(shaderName).InstanceUnique();
instance.SetParameter("outline_width", DefaultWidth * renderScale);
return instance;
}
}

View file

@ -1,4 +1,5 @@
using System.Numerics;
using Content.Client.Graphics;
using Content.Shared.Interaction;
using Content.Shared.Whitelist;
using Robust.Client.GameObjects;
@ -27,6 +28,7 @@ public sealed partial class TargetOutlineSystem : EntitySystem
[Dependency] private EntityWhitelistSystem _whitelistSystem = default!;
[Dependency] private SharedTransformSystem _transformSystem = default!;
[Dependency] private EntityQuery<SpriteComponent> _spriteQuery = default!;
[Dependency] private SpriteSystem _sprite = default!;
private bool _enabled = false;
@ -154,9 +156,9 @@ public sealed partial class TargetOutlineSystem : EntitySystem
if (!valid)
{
// was this previously valid?
if (_highlightedSprites.Remove(sprite) && (sprite.PostShader == _shaderTargetValid || sprite.PostShader == _shaderTargetInvalid))
if (_highlightedSprites.Remove(sprite))
{
sprite.PostShader = null;
_sprite.RemovePostShader(sprite, ContentPostShaderIds.TargetOutline);
sprite.RenderOrder = 0;
}
@ -173,13 +175,11 @@ public sealed partial class TargetOutlineSystem : EntitySystem
valid = (origin - target).LengthSquared() <= Range;
}
if (sprite.PostShader != null &&
sprite.PostShader != _shaderTargetValid &&
sprite.PostShader != _shaderTargetInvalid)
return;
// highlight depending on whether its in or out of range
sprite.PostShader = valid ? _shaderTargetValid : _shaderTargetInvalid;
_sprite.SetPostShader(sprite, new SpriteComponent.PostShaderArgs(ContentPostShaderIds.TargetOutline, valid ? _shaderTargetValid! : _shaderTargetInvalid!)
{
After = ContentPostShaderIds.AfterBaseEffects,
});
sprite.RenderOrder = EntityManager.CurrentTick.Value;
_highlightedSprites.Add(sprite);
}
@ -189,10 +189,7 @@ public sealed partial class TargetOutlineSystem : EntitySystem
{
foreach (var sprite in _highlightedSprites)
{
if (sprite.PostShader != _shaderTargetValid && sprite.PostShader != _shaderTargetInvalid)
continue;
sprite.PostShader = null;
_sprite.RemovePostShader(sprite, ContentPostShaderIds.TargetOutline);
sprite.RenderOrder = 0;
}

View file

@ -1,4 +1,5 @@
using Content.Client.Interactable.Components;
using Content.Client.Graphics;
using Content.Shared.Stealth;
using Content.Shared.Stealth.Components;
using Robust.Client.GameObjects;
@ -42,9 +43,19 @@ public sealed partial class StealthSystem : SharedStealthSystem
return;
_sprite.SetColor((uid, sprite), Color.White);
sprite.PostShader = enabled ? _shader : null;
sprite.GetScreenTexture = enabled;
sprite.RaiseShaderEvent = enabled;
if (enabled)
{
_sprite.SetPostShader((uid, sprite), new SpriteComponent.PostShaderArgs(ContentPostShaderIds.Stealth, _shader)
{
GetScreenTexture = true,
RaiseShaderEvent = true,
Before = ContentPostShaderIds.BeforeOutlines,
});
}
else
{
_sprite.RemovePostShader((uid, sprite), ContentPostShaderIds.Stealth);
}
if (!enabled)
{
@ -53,11 +64,8 @@ public sealed partial class StealthSystem : SharedStealthSystem
return;
}
if (TryComp(uid, out InteractionOutlineComponent? outline))
{
RemCompDeferred(uid, outline);
if (HasComp<InteractionOutlineComponent>(uid))
component.HadOutline = true;
}
}
private void OnStartup(EntityUid uid, StealthComponent component, ComponentStartup args)

View file

@ -9,5 +9,5 @@ void fragment()
highp vec4 tex = zTexture(UV);
highp float modifier = (UV.y > c ^^ !below) ? 1.0 : alphaModifier;
COLOR = vec4(tex.x, tex.y, tex.z, tex.w * modifier);
COLOR = vec4(tex.rgb * modifier, tex.a * modifier);
}

View file

@ -70,11 +70,14 @@ void fragment() {
maxa = max(a, maxa);
mina = min(a, mina);
a = texture2D(TEXTURE, UV + vec2(outline_width, outline_width)*ps).a;
maxa = max(a, maxa);
mina = min(a, mina);
a = texture2D(TEXTURE, UV + vec2(outline_width, outline_width)*ps).a;
maxa = max(a, maxa);
mina = min(a, mina);
lowp float sampledLight = outline_fullbright ? 1.0 : clamp( (pow( zGrayscale_BT601(lightSample.rgb) * light_whitepoint, light_gamma) / light_whitepoint ) * light_boost, 0.0, 1.0);
COLOR = mix(col, outline_color * vec4(vec3(1.0), sampledLight), maxa - col.a);
highp float outlineAmount = max(maxa - col.a, 0.0);
highp float outlineAlpha = outline_color.a * sampledLight * outlineAmount;
highp vec4 outline = vec4(outline_color.rgb * outlineAlpha, outlineAlpha);
COLOR = mix(outline, col, step(0.01, col.a));
lightSample = vec3(1.0);
}