diff --git a/Content.Client/Atmos/EntitySystems/GasTileOverlaySystem.cs b/Content.Client/Atmos/EntitySystems/GasTileOverlaySystem.cs index ad26436946..d7894265c8 100644 --- a/Content.Client/Atmos/EntitySystems/GasTileOverlaySystem.cs +++ b/Content.Client/Atmos/EntitySystems/GasTileOverlaySystem.cs @@ -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(); + _overlayMan.RemoveOverlay(); } 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 index 0000000000..36f0a065c1 --- /dev/null +++ b/Content.Client/Atmos/Overlays/GasTileHeatOverlay.cs @@ -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 UnshadedShader = "unshaded"; + private static readonly ProtoId 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(); + + _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(); + + 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(); + + 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> 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(); + } +} diff --git a/Content.Server/Atmos/EntitySystems/GasTileOverlaySystem.cs b/Content.Server/Atmos/EntitySystems/GasTileOverlaySystem.cs index 4882e93d23..e63a57c3b6 100644 --- a/Content.Server/Atmos/EntitySystems/GasTileOverlaySystem.cs +++ b/Content.Server/Atmos/EntitySystems/GasTileOverlaySystem.cs @@ -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 _gridQuery; private EntityQuery _query; + /// + /// 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" + /// + 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(Reset); SubscribeLocalEvent(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 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; } + /// + /// This function determines whether the change in temperature is significant enough to warrant dirtying the tile data. + /// + 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? diff --git a/Content.Shared/Atmos/EntitySystems/SharedGasTileOverlaySystem.cs b/Content.Shared/Atmos/EntitySystems/SharedGasTileOverlaySystem.cs index 8e7dfdedaf..1c7da938d4 100644 --- a/Content.Shared/Atmos/EntitySystems/SharedGasTileOverlaySystem.cs +++ b/Content.Shared/Atmos/EntitySystems/SharedGasTileOverlaySystem.cs @@ -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 { + /// + /// The temperature at which the heat distortion effect starts to be applied. + /// + private float _tempAtMinHeatDistortion; + /// + /// The temperature at which the heat distortion effect is at maximum strength. + /// + private float _tempAtMaxHeatDistortion; + /// + /// Calculated linear slope and intercept to map temperature to a heat distortion strength from 0.0 to 1.0 + /// + 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!; /// /// 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(OnGetState); List 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; - // 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 + /// + /// 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 + /// and as these are the only + /// values at which the heat distortion varies. + /// Additionally, it will only update when the heat distortion strength changes by + /// . By default, this is 5%, which corresponds to + /// 20 steps from to . + /// For 325K to 1000K with 5% tolerance, then this field will dirty only if it differs by 33.75K, or 20 steps. + /// + [ViewVariables] + public readonly float Temperature; - public GasOverlayData(byte fireState, byte[] opacity) + // TODO change fire color based on temps + + 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; } } + /// + /// Calculate the heat distortion from a temperature. + /// Returns 0.0f below TempAtMinHeatDistortion and 1.0f above TempAtMaxHeatDistortion. + /// + /// + /// + public float GetHeatDistortionStrength(float temp) + { + return MathHelper.Clamp01(temp * _heatDistortionSlope + _heatDistortionIntercept); + } + [Serializable, NetSerializable] public sealed class GasOverlayUpdateEvent : EntityEventArgs { diff --git a/Content.Shared/CCVar/CCVars.Net.cs b/Content.Shared/CCVar/CCVars.Net.cs index b7465def2e..df8dc6932d 100644 --- a/Content.Shared/CCVar/CCVars.Net.cs +++ b/Content.Shared/CCVar/CCVars.Net.cs @@ -12,4 +12,23 @@ public sealed partial class CCVars public static readonly CVarDef GasOverlayThresholds = CVarDef.Create("net.gasoverlaythresholds", 20); + + public static readonly CVarDef 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 GasOverlayHeatMinimum = + CVarDef.Create("net.gasoverlayheatminimum", + 325f, + CVar.SERVER | CVar.REPLICATED, + "Temperature at which heat distortion effect will begin to apply."); + + public static readonly CVarDef GasOverlayHeatMaximum = + CVarDef.Create("net.gasoverlayheatmaximum", + 1000f, + CVar.SERVER | CVar.REPLICATED, + "Temperature at which heat distortion effect will be at maximum strength."); } diff --git a/Resources/Prototypes/Shaders/shaders.yml b/Resources/Prototypes/Shaders/shaders.yml index 057abf0ac2..f7c704909e 100644 --- a/Resources/Prototypes/Shaders/shaders.yml +++ b/Resources/Prototypes/Shaders/shaders.yml @@ -115,3 +115,13 @@ 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 index 0000000000..8e478f471a --- /dev/null +++ b/Resources/Textures/Shaders/heat.swsl @@ -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; +}