|
| 1 | +using Content.Client.Atmos.EntitySystems; |
| 2 | +using Content.Client.Graphics; |
| 3 | +using Content.Client.Resources; |
| 4 | +using Content.Shared.Atmos; |
| 5 | +using Content.Shared.Atmos.Components; |
| 6 | +using Content.Shared.Atmos.EntitySystems; |
| 7 | +using Content.Shared.CCVar; |
| 8 | +using Robust.Client.Graphics; |
| 9 | +using Robust.Client.ResourceManagement; |
| 10 | +using Robust.Shared.Configuration; |
| 11 | +using Robust.Shared.Enums; |
| 12 | +using Robust.Shared.Map; |
| 13 | +using Robust.Shared.Map.Components; |
| 14 | +using Robust.Shared.Prototypes; |
| 15 | +using System.Numerics; |
| 16 | +using Color = Robust.Shared.Maths.Color; |
| 17 | +using Texture = Robust.Client.Graphics.Texture; |
| 18 | + |
| 19 | +namespace Content.Client.Atmos.Overlays; |
| 20 | + |
| 21 | +/// <summary> |
| 22 | +/// Overlay responsible for rendering heat distortion shader. |
| 23 | +/// </summary> |
| 24 | +public sealed class GasTileHeatBlurOverlay : Overlay |
| 25 | +{ |
| 26 | + public override bool RequestScreenTexture { get; set; } = true; |
| 27 | + private static readonly ProtoId<ShaderPrototype> UnshadedShader = "unshaded"; |
| 28 | + private static readonly ProtoId<ShaderPrototype> HeatOverlayShader = "HeatBlur"; |
| 29 | + |
| 30 | + [Dependency] private readonly IEntityManager _entManager = default!; |
| 31 | + [Dependency] private readonly IMapManager _mapManager = default!; |
| 32 | + [Dependency] private readonly IPrototypeManager _proto = default!; |
| 33 | + [Dependency] private readonly IClyde _clyde = default!; |
| 34 | + [Dependency] private readonly IConfigurationManager _configManager = default!; |
| 35 | + [Dependency] private readonly IResourceCache _resourceCache = default!; |
| 36 | + |
| 37 | + private readonly SharedTransformSystem _xformSys; |
| 38 | + private readonly ShaderInstance _shader; |
| 39 | + |
| 40 | + private readonly Texture _noiseTexture; |
| 41 | + private readonly Texture _heatGradientTexture; |
| 42 | + private List<Entity<MapGridComponent>> _intersectingGrids = new(); |
| 43 | + private readonly OverlayResourceCache<CachedResources> _resources = new(); |
| 44 | + |
| 45 | + // Overlay settings |
| 46 | + private const float |
| 47 | + ShaderSpilling = 2.5f; // for example 4f - spills shader one tile from hotspot, 2.5f - spills it half tile |
| 48 | + |
| 49 | + private const float ShaderStrength = 0.04f; // Makes waves stronger |
| 50 | + private const float ShaderScale = 1f; // Makes more waves |
| 51 | + private const float ShaderSpeed = 0.4f; // Makes waves run faster |
| 52 | + |
| 53 | + // Overlay settings for reduced motion setting |
| 54 | + private const float ShaderStrengthForReducedMotion = 0.01f; |
| 55 | + private const float ShaderScaleReducedMotion = 0.5f; |
| 56 | + private const float ShaderSpeedReducedMotion = 0.25f; |
| 57 | + |
| 58 | + private const int MinDistortionTemp = 300; // Distortion starts to show up at this temperature in Kelvins |
| 59 | + private const int MaxDistortionTemp = 2000; // Maximum distortion strength at this temperature in Kelvins |
| 60 | + |
| 61 | + public override OverlaySpace Space => OverlaySpace.WorldSpace; |
| 62 | + |
| 63 | + public GasTileHeatBlurOverlay() |
| 64 | + { |
| 65 | + IoCManager.InjectDependencies(this); |
| 66 | + _xformSys = _entManager.System<SharedTransformSystem>(); |
| 67 | + |
| 68 | + _noiseTexture = _resourceCache.GetTexture("/Textures/Effects/HeatBlur/perlin_noise.png"); |
| 69 | + _heatGradientTexture = _resourceCache.GetTexture("/Textures/Effects/HeatBlur/soft_circle.png"); |
| 70 | + |
| 71 | + _shader = _proto.Index(HeatOverlayShader).InstanceUnique(); |
| 72 | + _configManager.OnValueChanged(CCVars.ReducedMotion, SetReducedMotion, invokeImmediately: true); |
| 73 | + } |
| 74 | + |
| 75 | + private void SetReducedMotion(bool reducedMotion) |
| 76 | + { |
| 77 | + _shader.SetParameter("strength_scale", reducedMotion ? ShaderStrengthForReducedMotion : ShaderStrength); |
| 78 | + _shader.SetParameter("spatial_scale", reducedMotion ? ShaderScaleReducedMotion : ShaderScale); |
| 79 | + _shader.SetParameter("speed_scale", reducedMotion ? ShaderSpeedReducedMotion : ShaderSpeed); |
| 80 | + } |
| 81 | + |
| 82 | + protected override bool BeforeDraw(in OverlayDrawArgs args) |
| 83 | + { |
| 84 | + if (args.MapId == MapId.Nullspace) |
| 85 | + return false; |
| 86 | + |
| 87 | + var res = _resources.GetForViewport(args.Viewport, static _ => new CachedResources()); |
| 88 | + |
| 89 | + var target = args.Viewport.RenderTarget; |
| 90 | + |
| 91 | + // Probably the resolution of the game window changed, remake the textures. |
| 92 | + if (res.HeatTarget?.Texture.Size != target.Size) |
| 93 | + { |
| 94 | + res.HeatTarget?.Dispose(); |
| 95 | + res.HeatTarget = _clyde.CreateRenderTarget( |
| 96 | + target.Size, |
| 97 | + new RenderTargetFormatParameters(RenderTargetColorFormat.Rgba8Srgb), |
| 98 | + name: nameof(GasTileHeatBlurOverlaySystem)); |
| 99 | + } |
| 100 | + |
| 101 | + if (res.HeatBlurTarget?.Texture.Size != target.Size) |
| 102 | + { |
| 103 | + res.HeatBlurTarget?.Dispose(); |
| 104 | + res.HeatBlurTarget = _clyde.CreateRenderTarget( |
| 105 | + target.Size, |
| 106 | + new RenderTargetFormatParameters(RenderTargetColorFormat.Rgba8Srgb), |
| 107 | + name: $"{nameof(GasTileHeatBlurOverlaySystem)}-blur"); |
| 108 | + } |
| 109 | + |
| 110 | + var overlayQuery = _entManager.GetEntityQuery<GasTileOverlayComponent>(); |
| 111 | + |
| 112 | + args.WorldHandle.UseShader(_proto.Index(UnshadedShader).Instance()); |
| 113 | + |
| 114 | + var mapId = args.MapId; |
| 115 | + var worldAABB = args.WorldAABB; |
| 116 | + var worldBounds = args.WorldBounds; |
| 117 | + var worldHandle = args.WorldHandle; |
| 118 | + var worldToViewportLocal = args.Viewport.GetWorldToLocalMatrix(); |
| 119 | + |
| 120 | + // If there is no distortion after checking all visible tiles, we can bail early |
| 121 | + var anyDistortion = false; |
| 122 | + |
| 123 | + // We're rendering in the context of the heat target texture, which will encode data as to where and how strong |
| 124 | + // the heat distortion will be |
| 125 | + args.WorldHandle.RenderInRenderTarget(res.HeatTarget, |
| 126 | + () => |
| 127 | + { |
| 128 | + _intersectingGrids.Clear(); |
| 129 | + _mapManager.FindGridsIntersecting(mapId, worldAABB, ref _intersectingGrids); |
| 130 | + foreach (var grid in _intersectingGrids) |
| 131 | + { |
| 132 | + if (!overlayQuery.TryGetComponent(grid.Owner, out var comp)) |
| 133 | + continue; |
| 134 | + |
| 135 | + var gridEntToWorld = _xformSys.GetWorldMatrix(grid.Owner); |
| 136 | + var gridEntToViewportLocal = gridEntToWorld * worldToViewportLocal; |
| 137 | + |
| 138 | + if (!Matrix3x2.Invert(gridEntToViewportLocal, out var viewportLocalToGridEnt)) |
| 139 | + continue; |
| 140 | + |
| 141 | + var uvToUi = Matrix3Helpers.CreateScale(res.HeatTarget.Size.X, -res.HeatTarget.Size.Y); |
| 142 | + var uvToGridEnt = uvToUi * viewportLocalToGridEnt; |
| 143 | + |
| 144 | + // Because we want the actual distortion to be calculated based on the grid coordinates*, we need |
| 145 | + // to pass a matrix transformation to go from the viewport coordinates to grid coordinates. |
| 146 | + // * (why? because otherwise the effect would shimmer like crazy as you moved around, think |
| 147 | + // moving a piece of warped glass above a picture instead of placing the warped glass on the |
| 148 | + // paper and moving them together) |
| 149 | + _shader.SetParameter("grid_ent_from_viewport_local", uvToGridEnt); |
| 150 | + |
| 151 | + // Draw commands (like DrawRect) will be using grid coordinates from here |
| 152 | + worldHandle.SetTransform(gridEntToViewportLocal); |
| 153 | + |
| 154 | + // We only care about tiles that fit in these bounds |
| 155 | + var worldToGridLocal = _xformSys.GetInvWorldMatrix(grid.Owner); |
| 156 | + var floatBounds = worldToGridLocal.TransformBox(worldBounds).Enlarged(grid.Comp.TileSize); |
| 157 | + |
| 158 | + var localBounds = new Box2i( |
| 159 | + (int)MathF.Floor(floatBounds.Left), |
| 160 | + (int)MathF.Floor(floatBounds.Bottom), |
| 161 | + (int)MathF.Ceiling(floatBounds.Right), |
| 162 | + (int)MathF.Ceiling(floatBounds.Top)); |
| 163 | + |
| 164 | + // for each tile and its gas ---> |
| 165 | + foreach (var chunk in comp.Chunks.Values) |
| 166 | + { |
| 167 | + var enumerator = new GasChunkEnumerator(chunk); |
| 168 | + |
| 169 | + while (enumerator.MoveNext(out var tileGas)) |
| 170 | + { |
| 171 | + // Check and make sure the tile is within the viewport/screen |
| 172 | + var tilePosition = chunk.Origin + (enumerator.X, enumerator.Y); |
| 173 | + if (!localBounds.Contains(tilePosition)) |
| 174 | + continue; |
| 175 | + |
| 176 | + // Get the distortion strength from the temperature and bail if it's not hot enough |
| 177 | + var strength = GetHeatDistortionStrength(tileGas.ByteGasTemperature); |
| 178 | + if (strength <= 0f) |
| 179 | + continue; |
| 180 | + |
| 181 | + anyDistortion = true; |
| 182 | + |
| 183 | + // Encode the strength in the red channel |
| 184 | + // alpha set to 1 as tile is active |
| 185 | + worldHandle.DrawTextureRect( |
| 186 | + _heatGradientTexture, |
| 187 | + Box2.CenteredAround(tilePosition + grid.Comp.TileSizeHalfVector, |
| 188 | + grid.Comp.TileSizeVector * ShaderSpilling), |
| 189 | + new Color(strength, 0f, 0f)); |
| 190 | + } |
| 191 | + } |
| 192 | + } |
| 193 | + }, |
| 194 | + // This clears the buffer to all zero first... |
| 195 | + new Color(0, 0, 0, 0)); |
| 196 | + |
| 197 | + // no distortion, no need to render |
| 198 | + if (!anyDistortion) |
| 199 | + { |
| 200 | + args.WorldHandle.UseShader(null); |
| 201 | + args.WorldHandle.SetTransform(Matrix3x2.Identity); |
| 202 | + return false; |
| 203 | + } |
| 204 | + |
| 205 | + return true; |
| 206 | + } |
| 207 | + |
| 208 | + protected override void Draw(in OverlayDrawArgs args) |
| 209 | + { |
| 210 | + var res = _resources.GetForViewport(args.Viewport, static _ => new CachedResources()); |
| 211 | + |
| 212 | + if (ScreenTexture is null || res.HeatTarget is null || res.HeatBlurTarget is null) |
| 213 | + return; |
| 214 | + |
| 215 | + _shader.SetParameter("SCREEN_TEXTURE", ScreenTexture); |
| 216 | + _shader.SetParameter("NOISE_TEXTURE", _noiseTexture); |
| 217 | + |
| 218 | + args.WorldHandle.UseShader(_shader); |
| 219 | + args.WorldHandle.DrawTextureRect(res.HeatTarget.Texture, args.WorldBounds); |
| 220 | + |
| 221 | + args.WorldHandle.UseShader(null); |
| 222 | + args.WorldHandle.SetTransform(Matrix3x2.Identity); |
| 223 | + } |
| 224 | + |
| 225 | + protected override void DisposeBehavior() |
| 226 | + { |
| 227 | + _resources.Dispose(); |
| 228 | + |
| 229 | + _configManager.UnsubValueChanged(CCVars.ReducedMotion, SetReducedMotion); |
| 230 | + base.DisposeBehavior(); |
| 231 | + } |
| 232 | + |
| 233 | + /// <summary> |
| 234 | + /// Gets the strength of the heat distortion effect based on the temperature of the tile. |
| 235 | + /// The strength is a value between 0 and 1, where 0 means no distortion and 1 means maximum distortion. |
| 236 | + /// </summary> |
| 237 | + /// <param name="temp">The temperature of the tile.</param> |
| 238 | + /// <returns>The strength of the heat distortion effect.</returns> |
| 239 | + /// <seealso cref="ThermalByte"/> |
| 240 | + private static float GetHeatDistortionStrength(ThermalByte temp) |
| 241 | + { |
| 242 | + if (!temp.TryGetTemperature(out var kelvinTemp)) |
| 243 | + { |
| 244 | + return 0f; |
| 245 | + } |
| 246 | + |
| 247 | + var strength = (kelvinTemp - MinDistortionTemp) / (MaxDistortionTemp - MinDistortionTemp); |
| 248 | + |
| 249 | + return MathHelper.Clamp01(strength); |
| 250 | + } |
| 251 | + |
| 252 | + internal sealed class CachedResources : IDisposable |
| 253 | + { |
| 254 | + public IRenderTexture? HeatTarget; |
| 255 | + public IRenderTexture? HeatBlurTarget; |
| 256 | + |
| 257 | + public void Dispose() |
| 258 | + { |
| 259 | + HeatTarget?.Dispose(); |
| 260 | + HeatBlurTarget?.Dispose(); |
| 261 | + } |
| 262 | + } |
| 263 | +} |
0 commit comments