]> git.smokeofanarchy.ru Git - space-station-14.git/commitdiff
Add heat distortion shader for hot gases (#39107)
authorQuantum-cross <7065792+Quantum-cross@users.noreply.github.com>
Thu, 4 Sep 2025 03:17:39 +0000 (23:17 -0400)
committerGitHub <noreply@github.com>
Thu, 4 Sep 2025 03:17:39 +0000 (20:17 -0700)
Content.Client/Atmos/EntitySystems/GasTileOverlaySystem.cs
Content.Client/Atmos/Overlays/GasTileHeatOverlay.cs [new file with mode: 0644]
Content.Server/Atmos/EntitySystems/GasTileOverlaySystem.cs
Content.Shared/Atmos/EntitySystems/SharedGasTileOverlaySystem.cs
Content.Shared/CCVar/CCVars.Net.cs
Resources/Prototypes/Shaders/shaders.yml
Resources/Textures/Shaders/heat.swsl [new file with mode: 0644]

index ad2643694673d18ff8f9edf4ba7a9c33645fe2c4..d7894265c812be38d9b0838d302128500d3940f0 100644 (file)
@@ -19,6 +19,7 @@ namespace Content.Client.Atmos.EntitySystems
         [Dependency] private readonly SharedTransformSystem _xformSys = default!;
 
         private GasTileOverlay _overlay = default!;
+        private GasTileHeatOverlay _heatOverlay = default!;
 
         public override void Initialize()
         {
@@ -28,12 +29,16 @@ namespace Content.Client.Atmos.EntitySystems
 
             _overlay = new GasTileOverlay(this, EntityManager, _resourceCache, ProtoMan, _spriteSys, _xformSys);
             _overlayMan.AddOverlay(_overlay);
+
+            _heatOverlay = new GasTileHeatOverlay();
+            _overlayMan.AddOverlay(_heatOverlay);
         }
 
         public override void Shutdown()
         {
             base.Shutdown();
             _overlayMan.RemoveOverlay<GasTileOverlay>();
+            _overlayMan.RemoveOverlay<GasTileHeatOverlay>();
         }
 
         private void OnHandleState(EntityUid gridUid, GasTileOverlayComponent comp, ref ComponentHandleState args)
diff --git a/Content.Client/Atmos/Overlays/GasTileHeatOverlay.cs b/Content.Client/Atmos/Overlays/GasTileHeatOverlay.cs
new file mode 100644 (file)
index 0000000..36f0a06
--- /dev/null
@@ -0,0 +1,210 @@
+using System.Numerics;
+using Content.Shared.Atmos;
+using Content.Shared.Atmos.Components;
+using Content.Client.Atmos.EntitySystems;
+using Content.Shared.CCVar;
+using Robust.Client.Graphics;
+using Robust.Shared.Configuration;
+using Robust.Shared.Enums;
+using Robust.Shared.Map;
+using Robust.Shared.Map.Components;
+using Robust.Shared.Prototypes;
+
+namespace Content.Client.Atmos.Overlays;
+
+public sealed class GasTileHeatOverlay : Overlay
+{
+    public override bool RequestScreenTexture { get; set; } = true;
+    private static readonly ProtoId<ShaderPrototype> UnshadedShader = "unshaded";
+    private static readonly ProtoId<ShaderPrototype> HeatOverlayShader = "Heat";
+
+    [Dependency] private readonly IEntityManager _entManager = default!;
+    [Dependency] private readonly IMapManager _mapManager = default!;
+    [Dependency] private readonly IPrototypeManager _proto = default!;
+    [Dependency] private readonly IClyde _clyde = default!;
+    [Dependency] private readonly IConfigurationManager _configManager = default!;
+    // We can't resolve this immediately, because it's an entitysystem, but we will attempt to resolve and cache this
+    // once we begin to draw.
+    private GasTileOverlaySystem? _gasTileOverlay;
+    private readonly SharedTransformSystem _xformSys;
+
+    private IRenderTexture? _heatTarget;
+    private IRenderTexture? _heatBlurTarget;
+
+    public override OverlaySpace Space => OverlaySpace.WorldSpace;
+    private readonly ShaderInstance _shader;
+
+    public GasTileHeatOverlay()
+    {
+        IoCManager.InjectDependencies(this);
+        _xformSys = _entManager.System<SharedTransformSystem>();
+
+        _shader = _proto.Index(HeatOverlayShader).InstanceUnique();
+
+        _configManager.OnValueChanged(CCVars.ReducedMotion, SetReducedMotion, invokeImmediately: true);
+
+    }
+
+    private void SetReducedMotion(bool reducedMotion)
+    {
+        _shader.SetParameter("strength_scale", reducedMotion ? 0.5f : 1f);
+        _shader.SetParameter("speed_scale", reducedMotion ? 0.25f : 1f);
+    }
+
+    protected override bool BeforeDraw(in OverlayDrawArgs args)
+    {
+        if (args.MapId == MapId.Nullspace)
+            return false;
+
+        // If we haven't resolved this yet, give it a try or bail
+        _gasTileOverlay ??= _entManager.System<GasTileOverlaySystem>();
+
+        if (_gasTileOverlay == null)
+            return false;
+
+        var target = args.Viewport.RenderTarget;
+
+        // Probably the resolution of the game window changed, remake the textures.
+        if (_heatTarget?.Texture.Size != target.Size)
+        {
+            _heatTarget?.Dispose();
+            _heatTarget = _clyde.CreateRenderTarget(
+                target.Size,
+                new RenderTargetFormatParameters(RenderTargetColorFormat.Rgba8Srgb),
+                name: nameof(GasTileHeatOverlay));
+        }
+        if (_heatBlurTarget?.Texture.Size != target.Size)
+        {
+            _heatBlurTarget?.Dispose();
+            _heatBlurTarget = _clyde.CreateRenderTarget(
+                target.Size,
+                new RenderTargetFormatParameters(RenderTargetColorFormat.Rgba8Srgb),
+                name: $"{nameof(GasTileHeatOverlay)}-blur");
+        }
+
+        var overlayQuery = _entManager.GetEntityQuery<GasTileOverlayComponent>();
+
+        args.WorldHandle.UseShader(_proto.Index(UnshadedShader).Instance());
+
+        var mapId = args.MapId;
+        var worldAABB = args.WorldAABB;
+        var worldBounds = args.WorldBounds;
+        var worldHandle = args.WorldHandle;
+        var worldToViewportLocal = args.Viewport.GetWorldToLocalMatrix();
+
+        // If there is no distortion after checking all visible tiles, we can bail early
+        var anyDistortion = false;
+
+        // We're rendering in the context of the heat target texture, which will encode data as to where and how strong
+        // the heat distortion will be
+        args.WorldHandle.RenderInRenderTarget(_heatTarget,
+            () =>
+            {
+                List<Entity<MapGridComponent>> grids = new();
+                _mapManager.FindGridsIntersecting(mapId, worldAABB, ref grids);
+                foreach (var grid in grids)
+                {
+                    if (!overlayQuery.TryGetComponent(grid.Owner, out var comp))
+                        continue;
+
+                    var gridEntToWorld = _xformSys.GetWorldMatrix(grid.Owner);
+                    var gridEntToViewportLocal = gridEntToWorld * worldToViewportLocal;
+
+                    if (!Matrix3x2.Invert(gridEntToViewportLocal, out var viewportLocalToGridEnt))
+                        continue;
+
+                    var uvToUi = Matrix3Helpers.CreateScale(_heatTarget.Size.X, -_heatTarget.Size.Y);
+                    var uvToGridEnt = uvToUi * viewportLocalToGridEnt;
+
+                    // Because we want the actual distortion to be calculated based on the grid coordinates*, we need
+                    // to pass a matrix transformation to go from the viewport coordinates to grid coordinates.
+                    //   * (why? because otherwise the effect would shimmer like crazy as you moved around, think
+                    //      moving a piece of warped glass above a picture instead of placing the warped glass on the
+                    //      paper and moving them together)
+                    _shader.SetParameter("grid_ent_from_viewport_local", uvToGridEnt);
+
+                    // Draw commands (like DrawRect) will be using grid coordinates from here
+                    worldHandle.SetTransform(gridEntToViewportLocal);
+
+                    // We only care about tiles that fit in these bounds
+                    var floatBounds = worldToViewportLocal.TransformBox(worldBounds).Enlarged(grid.Comp.TileSize);
+                    var localBounds = new Box2i(
+                        (int)MathF.Floor(floatBounds.Left),
+                        (int)MathF.Floor(floatBounds.Bottom),
+                        (int)MathF.Ceiling(floatBounds.Right),
+                        (int)MathF.Ceiling(floatBounds.Top));
+
+                    // for each tile and its gas --->
+                    foreach (var chunk in comp.Chunks.Values)
+                    {
+                        var enumerator = new GasChunkEnumerator(chunk);
+
+                        while (enumerator.MoveNext(out var tileGas))
+                        {
+                            // --->
+                            // Check and make sure the tile is within the viewport/screen
+                            var tilePosition = chunk.Origin + (enumerator.X, enumerator.Y);
+                            if (!localBounds.Contains(tilePosition))
+                                continue;
+
+                            // Get the distortion strength from the temperature and bail if it's not hot enough
+                            var strength = _gasTileOverlay.GetHeatDistortionStrength(tileGas.Temperature);
+                            if (strength <= 0f)
+                                continue;
+
+                            anyDistortion = true;
+                            // Encode the strength in the red channel, then 1.0 alpha if it's an active tile.
+                            // BlurRenderTarget will then apply a blur around the edge, but we don't want it to bleed
+                            // past the tile.
+                            // So we use this alpha channel to chop the lower alpha values off in the shader to fit a
+                            // fit mask back into the tile.
+                            worldHandle.DrawRect(
+                                Box2.CenteredAround(tilePosition + new Vector2(0.5f, 0.5f), grid.Comp.TileSizeVector),
+                                new Color(strength,0f, 0f, strength > 0f ? 1.0f : 0f));
+                        }
+                    }
+                }
+            },
+            // This clears the buffer to all zero first...
+            new Color(0, 0, 0, 0));
+
+        // no distortion, no need to render
+        if (!anyDistortion)
+        {
+            // Return the draw handle to normal settings
+            args.WorldHandle.UseShader(null);
+            args.WorldHandle.SetTransform(Matrix3x2.Identity);
+            return false;
+        }
+
+        // Clear to draw
+        return true;
+    }
+
+    protected override void Draw(in OverlayDrawArgs args)
+    {
+        if (ScreenTexture is null || _heatTarget is null || _heatBlurTarget is null)
+            return;
+
+        // Blur to soften the edges of the distortion. the lower parts of the alpha channel need to get cut off in the
+        // distortion shader to keep them in tile bounds.
+        _clyde.BlurRenderTarget(args.Viewport, _heatTarget, _heatBlurTarget, args.Viewport.Eye!, 14f);
+
+        // Set up and render the distortion
+        _shader.SetParameter("SCREEN_TEXTURE", ScreenTexture);
+        args.WorldHandle.UseShader(_shader);
+        args.WorldHandle.DrawTextureRect(_heatTarget.Texture, args.WorldBounds);
+
+        // Return the draw handle to normal settings
+        args.WorldHandle.UseShader(null);
+        args.WorldHandle.SetTransform(Matrix3x2.Identity);
+    }
+
+    protected override void DisposeBehavior()
+    {
+        _heatTarget = null;
+        _heatBlurTarget = null;
+        _configManager.UnsubValueChanged(CCVars.ReducedMotion, SetReducedMotion);
+        base.DisposeBehavior();
+    }
+}
index 4882e93d23050a74d2754947574e55297850f92d..e63a57c3b6fcf146ddae1ea0e5123e5f6ed261fa 100644 (file)
@@ -1,6 +1,4 @@
-using System.Linq;
 using System.Runtime.CompilerServices;
-using System.Threading.Tasks;
 using Content.Server.Atmos.Components;
 using Content.Shared.Atmos;
 using Content.Shared.Atmos.Components;
@@ -13,7 +11,6 @@ using JetBrains.Annotations;
 using Microsoft.Extensions.ObjectPool;
 using Robust.Server.Player;
 using Robust.Shared;
-using Robust.Shared.Configuration;
 using Robust.Shared.Enums;
 using Robust.Shared.Map;
 using Robust.Shared.Map.Components;
@@ -32,7 +29,6 @@ namespace Content.Server.Atmos.EntitySystems
         [Robust.Shared.IoC.Dependency] private readonly IGameTiming _gameTiming = default!;
         [Robust.Shared.IoC.Dependency] private readonly IPlayerManager _playerManager = default!;
         [Robust.Shared.IoC.Dependency] private readonly IMapManager _mapManager = default!;
-        [Robust.Shared.IoC.Dependency] private readonly IConfigurationManager _confMan = default!;
         [Robust.Shared.IoC.Dependency] private readonly IParallelManager _parMan = default!;
         [Robust.Shared.IoC.Dependency] private readonly AtmosphereSystem _atmosphereSystem = default!;
         [Robust.Shared.IoC.Dependency] private readonly ChunkingSystem _chunkingSys = default!;
@@ -64,6 +60,12 @@ namespace Content.Server.Atmos.EntitySystems
         private EntityQuery<MapGridComponent> _gridQuery;
         private EntityQuery<GasTileOverlayComponent> _query;
 
+        /// <summary>
+        /// How much the distortion strength should change for the temperature of a tile to be dirtied.
+        /// The strength goes from 0.0f to 1.0f, so 0.05f gives it essentially 20 "steps"
+        /// </summary>
+        private float _heatDistortionStrengthChangeTolerance;
+
         public override void Initialize()
         {
             base.Initialize();
@@ -85,9 +87,10 @@ namespace Content.Server.Atmos.EntitySystems
             };
 
             _playerManager.PlayerStatusChanged += OnPlayerStatusChanged;
-            Subs.CVar(_confMan, CCVars.NetGasOverlayTickRate, UpdateTickRate, true);
-            Subs.CVar(_confMan, CCVars.GasOverlayThresholds, UpdateThresholds, true);
-            Subs.CVar(_confMan, CVars.NetPVS, OnPvsToggle, true);
+            Subs.CVar(ConfMan, CCVars.NetGasOverlayTickRate, UpdateTickRate, true);
+            Subs.CVar(ConfMan, CCVars.GasOverlayThresholds, UpdateThresholds, true);
+            Subs.CVar(ConfMan, CVars.NetPVS, OnPvsToggle, true);
+            Subs.CVar(ConfMan, CCVars.GasOverlayHeatThreshold, UpdateHeatThresholds, true);
 
             SubscribeLocalEvent<RoundRestartCleanupEvent>(Reset);
             SubscribeLocalEvent<GasTileOverlayComponent, ComponentStartup>(OnStartup);
@@ -137,6 +140,7 @@ namespace Content.Server.Atmos.EntitySystems
 
         private void UpdateTickRate(float value) => _updateInterval = value > 0.0f ? 1 / value : float.MaxValue;
         private void UpdateThresholds(int value) => _thresholds = value;
+        private void UpdateHeatThresholds(float v) => _heatDistortionStrengthChangeTolerance = MathHelper.Clamp01(v);
 
         [MethodImpl(MethodImplOptions.AggressiveInlining)]
         public void Invalidate(Entity<GasTileOverlayComponent?> grid, Vector2i index)
@@ -175,7 +179,9 @@ namespace Content.Server.Atmos.EntitySystems
 
         public GasOverlayData GetOverlayData(GasMixture? mixture)
         {
-            var data = new GasOverlayData(0, new byte[VisibleGasId.Length]);
+            var data = new GasOverlayData(0,
+                new byte[VisibleGasId.Length],
+                mixture?.Temperature ?? Atmospherics.TCMB);
 
             for (var i = 0; i < VisibleGasId.Length; i++)
             {
@@ -215,15 +221,17 @@ namespace Content.Server.Atmos.EntitySystems
             }
 
             var changed = false;
+            var temp = tile.Hotspot.Valid ? tile.Hotspot.Temperature : tile.Air?.Temperature ?? Atmospherics.TCMB;
             if (oldData.Equals(default))
             {
                 changed = true;
-                oldData = new GasOverlayData(tile.Hotspot.State, new byte[VisibleGasId.Length]);
+                oldData = new GasOverlayData(tile.Hotspot.State, new byte[VisibleGasId.Length], temp);
             }
-            else if (oldData.FireState != tile.Hotspot.State)
+            else if (oldData.FireState != tile.Hotspot.State ||
+                     CheckTemperatureTolerance(oldData.Temperature, temp, _heatDistortionStrengthChangeTolerance))
             {
                 changed = true;
-                oldData = new GasOverlayData(tile.Hotspot.State, oldData.Opacity);
+                oldData = new GasOverlayData(tile.Hotspot.State, oldData.Opacity, temp);
             }
 
             if (tile is {Air: not null, NoGridTile: false})
@@ -271,6 +279,20 @@ namespace Content.Server.Atmos.EntitySystems
             return true;
         }
 
+        /// <summary>
+        /// This function determines whether the change in temperature is significant enough to warrant dirtying the tile data.
+        /// </summary>
+        private bool CheckTemperatureTolerance(float tempA, float tempB, float tolerance)
+        {
+            var (strengthA, strengthB) = (GetHeatDistortionStrength(tempA), GetHeatDistortionStrength(tempB));
+
+            return (strengthA <= 0f && strengthB > 0f) || // change to or from 0
+                   (strengthB <= 0f && strengthA > 0f) ||
+                   (strengthA >= 1f && strengthB < 1f) || // change to or from 1
+                   (strengthB >= 1f && strengthA < 1f) ||
+                   Math.Abs(strengthA - strengthB) > tolerance; // other change within tolerance
+        }
+
         private void UpdateOverlayData()
         {
             // TODO parallelize?
index 8e7dfdedaf9076aea16d23e80f1bcf39d3f3aaf4..1c7da938d4d034a39cc03043023332c160142000 100644 (file)
@@ -1,5 +1,7 @@
 using Content.Shared.Atmos.Components;
 using Content.Shared.Atmos.Prototypes;
+using Content.Shared.CCVar;
+using Robust.Shared.Configuration;
 using Robust.Shared.GameStates;
 using Robust.Shared.Prototypes;
 using Robust.Shared.Serialization;
@@ -8,11 +10,26 @@ namespace Content.Shared.Atmos.EntitySystems
 {
     public abstract class SharedGasTileOverlaySystem : EntitySystem
     {
+        /// <summary>
+        /// The temperature at which the heat distortion effect starts to be applied.
+        /// </summary>
+        private float _tempAtMinHeatDistortion;
+        /// <summary>
+        /// The temperature at which the heat distortion effect is at maximum strength.
+        /// </summary>
+        private float _tempAtMaxHeatDistortion;
+        /// <summary>
+        /// Calculated linear slope and intercept to map temperature to a heat distortion strength from 0.0 to 1.0
+        /// </summary>
+        private float _heatDistortionSlope;
+        private float _heatDistortionIntercept;
+
         public const byte ChunkSize = 8;
         protected float AccumulatedFrameTime;
         protected bool PvsEnabled;
 
         [Dependency] protected readonly IPrototypeManager ProtoMan = default!;
+        [Dependency] protected readonly IConfigurationManager ConfMan = default!;
 
         /// <summary>
         ///     array of the ids of all visible gases.
@@ -22,6 +39,11 @@ namespace Content.Shared.Atmos.EntitySystems
         public override void Initialize()
         {
             base.Initialize();
+
+            // Make sure the heat distortion variables are updated if the CVars change
+            Subs.CVar(ConfMan, CCVars.GasOverlayHeatMinimum, UpdateMinHeat, true);
+            Subs.CVar(ConfMan, CCVars.GasOverlayHeatMaximum, UpdateMaxHeat, true);
+
             SubscribeLocalEvent<GasTileOverlayComponent, ComponentGetState>(OnGetState);
 
             List<int> visibleGases = new();
@@ -36,6 +58,29 @@ namespace Content.Shared.Atmos.EntitySystems
             VisibleGasId = visibleGases.ToArray();
         }
 
+        private void UpdateMaxHeat(float val)
+        {
+            _tempAtMaxHeatDistortion = val;
+            UpdateHeatSlopeAndIntercept();
+        }
+
+        private void UpdateMinHeat(float val)
+        {
+            _tempAtMinHeatDistortion = val;
+            UpdateHeatSlopeAndIntercept();
+        }
+
+        private void UpdateHeatSlopeAndIntercept()
+        {
+            // Make sure to avoid invalid settings (min == max or min > max)
+            // I'm not sure if CVars can have constraints or if CVar subscribers can reject changes.
+            var diff = _tempAtMinHeatDistortion < _tempAtMaxHeatDistortion
+                ? _tempAtMaxHeatDistortion - _tempAtMinHeatDistortion
+                : 0.001f;
+            _heatDistortionSlope = 1.0f / diff;
+            _heatDistortionIntercept = -_tempAtMinHeatDistortion * _heatDistortionSlope;
+        }
+
         private void OnGetState(EntityUid uid, GasTileOverlayComponent component, ref ComponentGetState args)
         {
             if (PvsEnabled && !args.ReplayState)
@@ -72,14 +117,26 @@ namespace Content.Shared.Atmos.EntitySystems
             [ViewVariables]
             public readonly byte[] Opacity;
 
+            /// <summary>
+            /// This temperature is currently only used by the GasTileHeatOverlay.
+            /// This value will only reflect the true temperature of the gas when the temperature is between
+            /// <see cref="SharedGasTileOverlaySystem._tempAtMinHeatDistortion"/> and <see cref="SharedGasTileOverlaySystem._tempAtMaxHeatDistortion"/> as these are the only
+            /// values at which the heat distortion varies.
+            /// Additionally, it will only update when the heat distortion strength changes by
+            /// <see cref="_heatDistortionStrengthChangeTolerance"/>. By default, this is 5%, which corresponds to
+            /// 20 steps from <see cref="SharedGasTileOverlaySystem._tempAtMinHeatDistortion"/> to <see cref="SharedGasTileOverlaySystem._tempAtMaxHeatDistortion"/>.
+            /// For 325K to 1000K with 5% tolerance, then this field will dirty only if it differs by 33.75K, or 20 steps.
+            /// </summary>
+            [ViewVariables]
+            public readonly float Temperature;
+
             // TODO change fire color based on temps
-            // But also: dont dirty on a 0.01 kelvin change in temperatures.
-            // Either have a temp tolerance, or map temperature -> byte levels
 
-            public GasOverlayData(byte fireState, byte[] opacity)
+            public GasOverlayData(byte fireState, byte[] opacity, float temperature)
             {
                 FireState = fireState;
                 Opacity = opacity;
+                Temperature = temperature;
             }
 
             public bool Equals(GasOverlayData other)
@@ -99,10 +156,26 @@ namespace Content.Shared.Atmos.EntitySystems
                     }
                 }
 
+                // This is only checking if two datas are equal -- a different routine is used to check if the
+                // temperature differs enough to dirty the chunk using a much wider tolerance.
+                if (!MathHelper.CloseToPercent(Temperature, other.Temperature))
+                    return false;
+
                 return true;
             }
         }
 
+        /// <summary>
+        /// Calculate the heat distortion from a temperature.
+        /// Returns 0.0f below TempAtMinHeatDistortion and 1.0f above TempAtMaxHeatDistortion.
+        /// </summary>
+        /// <param name="temp"></param>
+        /// <returns></returns>
+        public float GetHeatDistortionStrength(float temp)
+        {
+            return MathHelper.Clamp01(temp * _heatDistortionSlope + _heatDistortionIntercept);
+        }
+
         [Serializable, NetSerializable]
         public sealed class GasOverlayUpdateEvent : EntityEventArgs
         {
index b7465def2ebb32fae3734f08135194e1f82fac36..df8dc6932dadbeb184c4c8af673135f6d54b66fe 100644 (file)
@@ -12,4 +12,23 @@ public sealed partial class CCVars
 
     public static readonly CVarDef<int> GasOverlayThresholds =
         CVarDef.Create("net.gasoverlaythresholds", 20);
+
+    public static readonly CVarDef<float> GasOverlayHeatThreshold =
+        CVarDef.Create("net.gasoverlayheatthreshold",
+            0.05f,
+            CVar.SERVER | CVar.REPLICATED,
+            "Threshold for sending tile temperature updates to client in percent of distortion strength," +
+            "from 0.0 to 1.0. Example: 0.05 = 5%, which means heat distortion will appear in 20 'steps'.");
+
+    public static readonly CVarDef<float> GasOverlayHeatMinimum =
+        CVarDef.Create("net.gasoverlayheatminimum",
+            325f,
+            CVar.SERVER | CVar.REPLICATED,
+            "Temperature at which heat distortion effect will begin to apply.");
+
+    public static readonly CVarDef<float> GasOverlayHeatMaximum =
+        CVarDef.Create("net.gasoverlayheatmaximum",
+            1000f,
+            CVar.SERVER | CVar.REPLICATED,
+            "Temperature at which heat distortion effect will be at maximum strength.");
 }
index 057abf0ac239881b2c42274d48acd0ec437d3bd5..f7c704909e732f80042fdfd4259227ec06445d6e 100644 (file)
   id: Hologram
   kind: source
   path: "/Textures/Shaders/hologram.swsl"
+
+- type: shader
+  id: Heat
+  kind: source
+  path: "/Textures/Shaders/heat.swsl"
+  params:
+    spatial_scale: 1.0
+    strength_scale: 1.0
+    speed_scale: 1.0
+    grid_ent_from_viewport_local: 1,0,0,1,0,1
diff --git a/Resources/Textures/Shaders/heat.swsl b/Resources/Textures/Shaders/heat.swsl
new file mode 100644 (file)
index 0000000..8e478f4
--- /dev/null
@@ -0,0 +1,90 @@
+uniform sampler2D SCREEN_TEXTURE;
+
+// Number of frequencies to combine, can't be a parameter/uniform else it causes problems in compatibility mode
+// I have no idea why
+const highp int N = 32;
+
+uniform highp float spatial_scale; // spatial scaling of modes, higher = fine turbulence, lower = coarse turbulence
+uniform highp float strength_scale; // distortion strength
+uniform highp float speed_scale; // scaling factor on the speed of the animation
+// Matrix to convert screen coordinates into grid coordinates
+// This is to "pin" the effect to the grid, so that it does not shimmer as you move
+uniform highp mat3 grid_ent_from_viewport_local;
+
+const highp float TWO_PI = 6.28318530718;
+ // This is just the default target values so that the external parameters can be normalized to 1
+const highp float strength_factor = 0.0005;
+const highp float spatial_factor = 22.0;
+
+// 1D pseudo-random function
+highp float random_1d(highp float n) {
+    return fract(sin(n * 12.9898) * 43758.5453);
+}
+
+// Kolmogorov amplitude, power spectrum goes as k^(–11/6)
+highp float kolAmp(highp float k) {
+    return pow(k, -11.0 / 6.0);
+}
+
+void fragment() {
+
+    highp vec2 ps = vec2(1.0/SCREEN_PIXEL_SIZE.x, 1.0/SCREEN_PIXEL_SIZE.y);
+    highp float aspectratio = ps.x / ps.y;
+
+    // scale the scale factor with the number of modes just cuz it works reasonably
+    highp float s_scale = spatial_scale * spatial_factor / sqrt(float(N));
+
+    // Coordinates to use to calculate the effects, convert to grid coordinates
+    highp vec2 uvW = (grid_ent_from_viewport_local * vec3(UV.x, UV.y, 1.0)).xy;
+    // Scale the coordinates
+    uvW *= s_scale;
+
+    // accumulate phase gradienta
+    highp vec2 grad = vec2(0.0);
+
+    for (lowp int i = 0; i < N; i++) {
+        // float cast of the index
+        highp float fi = float(i);
+
+        // Pick a random direction
+        highp float ang = random_1d(fi + 1.0) * TWO_PI;
+        highp vec2 dir = vec2(cos(ang), sin(ang));
+
+        // Pick a random spatial frequency from 0.5 to 30
+        highp float k = mix(0.5, 30.0, random_1d(fi + 17.0));
+
+        // Pick a random speed from 0.05 to 0.20
+        highp float speed = mix(3., 8., random_1d(fi + 33.0));
+
+        // Pick a random phase offset
+        highp float phi_0 = random_1d(fi + 49.0) * TWO_PI;
+
+        // phase argument
+        highp float t = dot(dir, uvW) * k + TIME * speed * speed_scale + phi_0;
+
+        // analytical gradient: ∇[sin(t)] = cos(t) * ∇t
+        // ∇t = k * dir * scale (scale is factored out)
+        grad += kolAmp(k) * cos(t) * k * dir;
+    }
+    // Spatial scaling (coarse or fine turbulence)
+    grad *= s_scale;
+
+    // The texture should have been blurred using a previous operation
+    // We use the alpha channel to cut off the blur that bleeds outside the tile, then we rescale
+    // the mask back up to 0.0 to 1.0
+    highp float mask = clamp((zTexture(UV).a - 0.5)*2.0, 0.00, 1.0);
+
+    // Calculate warped UV using the turbulence gradient
+    // The strength of the turbulence is encoded into the red channel of TEXTURE
+    // Give it a little polynomial boost: https://www.wolframalpha.com/input?i=-x%5E2+%2B2x+from+0+to+1
+    highp float heatStrength = zTexture(UV).r*1.0;
+    heatStrength = clamp(-heatStrength*heatStrength + 2.0*heatStrength, 0.0, 1.0);
+    highp vec2 uvDist = UV + (strength_scale * strength_factor * heatStrength * mask) * grad;
+
+    // Apply to the texture
+    COLOR = texture2D(SCREEN_TEXTURE, uvDist);
+
+    // Uncomment the following two lines to view the strength buffer directly
+    // COLOR.rgb = vec3(heatStrength * mask);
+    // COLOR.a = mask;
+}