|
| 1 | +// SPDX-FileCopyrightText: 2025 Quantum-cross <7065792+Quantum-cross@users.noreply.github.com> |
| 2 | +// |
| 3 | +// SPDX-License-Identifier: AGPL-3.0-or-later |
| 4 | + |
| 5 | +using System.Numerics; |
| 6 | +using Content.Shared.Atmos; |
| 7 | +using Content.Shared.Atmos.Components; |
| 8 | +using Content.Shared.Atmos.EntitySystems; |
| 9 | +using Content.Shared.CCVar; |
| 10 | +using Robust.Client.Graphics; |
| 11 | +using Robust.Shared.Configuration; |
| 12 | +using Robust.Shared.Enums; |
| 13 | +using Robust.Shared.Map; |
| 14 | +using Robust.Shared.Map.Components; |
| 15 | +using Robust.Shared.Prototypes; |
| 16 | + |
| 17 | +namespace Content.Client.Atmos.Overlays; |
| 18 | + |
| 19 | +public sealed class GasTileHeatOverlay : Overlay |
| 20 | +{ |
| 21 | + public override bool RequestScreenTexture { get; set; } = true; |
| 22 | + private static readonly ProtoId<ShaderPrototype> UnshadedShader = "unshaded"; |
| 23 | + private static readonly ProtoId<ShaderPrototype> HeatOverlayShader = "Heat"; |
| 24 | + |
| 25 | + [Dependency] private readonly IEntityManager _entManager = default!; |
| 26 | + [Dependency] private readonly IMapManager _mapManager = default!; |
| 27 | + [Dependency] private readonly IPrototypeManager _proto = default!; |
| 28 | + [Dependency] private readonly IClyde _clyde = default!; |
| 29 | + [Dependency] private readonly IConfigurationManager _configManager = default!; |
| 30 | + private readonly SharedTransformSystem _xformSys; |
| 31 | + |
| 32 | + private IRenderTexture? _heatTarget; |
| 33 | + private IRenderTexture? _heatBlurTarget; |
| 34 | + |
| 35 | + public override OverlaySpace Space => OverlaySpace.WorldSpace; |
| 36 | + private readonly ShaderInstance _shader; |
| 37 | + |
| 38 | + public GasTileHeatOverlay() |
| 39 | + { |
| 40 | + IoCManager.InjectDependencies(this); |
| 41 | + _xformSys = _entManager.System<SharedTransformSystem>(); |
| 42 | + |
| 43 | + _shader = _proto.Index(HeatOverlayShader).InstanceUnique(); |
| 44 | + |
| 45 | + _configManager.OnValueChanged(CCVars.ReducedMotion, SetReducedMotion, invokeImmediately: true); |
| 46 | + |
| 47 | + } |
| 48 | + |
| 49 | + private void SetReducedMotion(bool reducedMotion) |
| 50 | + { |
| 51 | + _shader.SetParameter("strength_scale", reducedMotion ? 0.5f : 1f); |
| 52 | + _shader.SetParameter("speed_scale", reducedMotion ? 0.25f : 1f); |
| 53 | + } |
| 54 | + |
| 55 | + protected override bool BeforeDraw(in OverlayDrawArgs args) |
| 56 | + { |
| 57 | + if (args.MapId == MapId.Nullspace) |
| 58 | + return false; |
| 59 | + |
| 60 | + var target = args.Viewport.RenderTarget; |
| 61 | + |
| 62 | + // Probably the resolution of the game window changed, remake the textures. |
| 63 | + if (_heatTarget?.Texture.Size != target.Size) |
| 64 | + { |
| 65 | + _heatTarget?.Dispose(); |
| 66 | + _heatTarget = _clyde.CreateRenderTarget( |
| 67 | + target.Size, |
| 68 | + new RenderTargetFormatParameters(RenderTargetColorFormat.Rgba8Srgb), |
| 69 | + name: nameof(GasTileHeatOverlay)); |
| 70 | + } |
| 71 | + if (_heatBlurTarget?.Texture.Size != target.Size) |
| 72 | + { |
| 73 | + _heatBlurTarget?.Dispose(); |
| 74 | + _heatBlurTarget = _clyde.CreateRenderTarget( |
| 75 | + target.Size, |
| 76 | + new RenderTargetFormatParameters(RenderTargetColorFormat.Rgba8Srgb), |
| 77 | + name: $"{nameof(GasTileHeatOverlay)}-blur"); |
| 78 | + } |
| 79 | + |
| 80 | + var overlayQuery = _entManager.GetEntityQuery<GasTileOverlayComponent>(); |
| 81 | + |
| 82 | + args.WorldHandle.UseShader(_proto.Index(UnshadedShader).Instance()); |
| 83 | + |
| 84 | + var mapId = args.MapId; |
| 85 | + var worldAABB = args.WorldAABB; |
| 86 | + var worldBounds = args.WorldBounds; |
| 87 | + var worldHandle = args.WorldHandle; |
| 88 | + var worldToViewportLocal = args.Viewport.GetWorldToLocalMatrix(); |
| 89 | + |
| 90 | + // If there is no distortion after checking all visible tiles, we can bail early |
| 91 | + var anyDistortion = false; |
| 92 | + |
| 93 | + // We're rendering in the context of the heat target texture, which will encode data as to where and how strong |
| 94 | + // the heat distortion will be |
| 95 | + args.WorldHandle.RenderInRenderTarget(_heatTarget, |
| 96 | + () => |
| 97 | + { |
| 98 | + List<Entity<MapGridComponent>> grids = new(); |
| 99 | + _mapManager.FindGridsIntersecting(mapId, worldAABB, ref grids); |
| 100 | + foreach (var grid in grids) |
| 101 | + { |
| 102 | + if (!overlayQuery.TryGetComponent(grid.Owner, out var comp)) |
| 103 | + continue; |
| 104 | + |
| 105 | + var gridEntToWorld = _xformSys.GetWorldMatrix(grid.Owner); |
| 106 | + var gridEntToViewportLocal = gridEntToWorld * worldToViewportLocal; |
| 107 | + |
| 108 | + if (!Matrix3x2.Invert(gridEntToViewportLocal, out var viewportLocalToGridEnt)) |
| 109 | + continue; |
| 110 | + |
| 111 | + var uvToUi = Matrix3Helpers.CreateScale(_heatTarget.Size.X, -_heatTarget.Size.Y); |
| 112 | + var uvToGridEnt = uvToUi * viewportLocalToGridEnt; |
| 113 | + |
| 114 | + // Because we want the actual distortion to be calculated based on the grid coordinates*, we need |
| 115 | + // to pass a matrix transformation to go from the viewport coordinates to grid coordinates. |
| 116 | + // * (why? because otherwise the effect would shimmer like crazy as you moved around, think |
| 117 | + // moving a piece of warped glass above a picture instead of placing the warped glass on the |
| 118 | + // paper and moving them together) |
| 119 | + _shader.SetParameter("grid_ent_from_viewport_local", uvToGridEnt); |
| 120 | + |
| 121 | + // Draw commands (like DrawRect) will be using grid coordinates from here |
| 122 | + worldHandle.SetTransform(gridEntToViewportLocal); |
| 123 | + |
| 124 | + // We only care about tiles that fit in these bounds |
| 125 | + var floatBounds = worldToViewportLocal.TransformBox(worldBounds).Enlarged(grid.Comp.TileSize); |
| 126 | + var localBounds = new Box2i( |
| 127 | + (int) MathF.Floor(floatBounds.Left), |
| 128 | + (int) MathF.Floor(floatBounds.Bottom), |
| 129 | + (int) MathF.Ceiling(floatBounds.Right), |
| 130 | + (int) MathF.Ceiling(floatBounds.Top)); |
| 131 | + |
| 132 | + // for each tile and its gas ---> |
| 133 | + foreach (var chunk in comp.Chunks.Values) |
| 134 | + { |
| 135 | + var enumerator = new GasChunkEnumerator(chunk); |
| 136 | + |
| 137 | + while (enumerator.MoveNext(out var tileGas)) |
| 138 | + { |
| 139 | + // ---> |
| 140 | + // Check and make sure the tile is within the viewport/screen |
| 141 | + var tilePosition = chunk.Origin + (enumerator.X, enumerator.Y); |
| 142 | + if (!localBounds.Contains(tilePosition)) |
| 143 | + continue; |
| 144 | + |
| 145 | + // Get the distortion strength from the temperature and bail if it's not hot enough |
| 146 | + var strength = SharedGasTileOverlaySystem.GetHeatDistortionStrength(tileGas.Temperature); |
| 147 | + if (strength <= 0f) |
| 148 | + continue; |
| 149 | + |
| 150 | + anyDistortion = true; |
| 151 | + // Encode the strength in the red channel, then 1.0 alpha if it's an active tile. |
| 152 | + // BlurRenderTarget will then apply a blur around the edge, but we don't want it to bleed |
| 153 | + // past the tile. |
| 154 | + // So we use this alpha channel to chop the lower alpha values off in the shader to fit a |
| 155 | + // fit mask back into the tile. |
| 156 | + worldHandle.DrawRect( |
| 157 | + Box2.CenteredAround(tilePosition + new Vector2(0.5f, 0.5f), grid.Comp.TileSizeVector), |
| 158 | + new Color(strength,0f, 0f, strength > 0f ? 1.0f : 0f)); |
| 159 | + } |
| 160 | + } |
| 161 | + } |
| 162 | + }, |
| 163 | + // This clears the buffer to all zero first... |
| 164 | + new Color(0, 0, 0, 0)); |
| 165 | + |
| 166 | + // no distortion, no need to render |
| 167 | + if (!anyDistortion) |
| 168 | + { |
| 169 | + // Return the draw handle to normal settings |
| 170 | + args.WorldHandle.UseShader(null); |
| 171 | + args.WorldHandle.SetTransform(Matrix3x2.Identity); |
| 172 | + return false; |
| 173 | + } |
| 174 | + |
| 175 | + // Clear to draw |
| 176 | + return true; |
| 177 | + } |
| 178 | + |
| 179 | + protected override void Draw(in OverlayDrawArgs args) |
| 180 | + { |
| 181 | + if (ScreenTexture is null || _heatTarget is null || _heatBlurTarget is null) |
| 182 | + return; |
| 183 | + |
| 184 | + // Blur to soften the edges of the distortion. the lower parts of the alpha channel need to get cut off in the |
| 185 | + // distortion shader to keep them in tile bounds. |
| 186 | + _clyde.BlurRenderTarget(args.Viewport, _heatTarget, _heatBlurTarget, args.Viewport.Eye!, 14f); |
| 187 | + |
| 188 | + // Set up and render the distortion |
| 189 | + _shader.SetParameter("SCREEN_TEXTURE", ScreenTexture); |
| 190 | + args.WorldHandle.UseShader(_shader); |
| 191 | + args.WorldHandle.DrawTextureRect(_heatTarget.Texture, args.WorldBounds); |
| 192 | + |
| 193 | + // Return the draw handle to normal settings |
| 194 | + args.WorldHandle.UseShader(null); |
| 195 | + args.WorldHandle.SetTransform(Matrix3x2.Identity); |
| 196 | + } |
| 197 | + |
| 198 | + protected override void DisposeBehavior() |
| 199 | + { |
| 200 | + _heatTarget = null; |
| 201 | + _heatBlurTarget = null; |
| 202 | + _configManager.UnsubValueChanged(CCVars.ReducedMotion, SetReducedMotion); |
| 203 | + base.DisposeBehavior(); |
| 204 | + } |
| 205 | +} |
0 commit comments