Add heat distortion shader for hot gases (#39107)
This commit is contained in:
@@ -19,6 +19,7 @@ namespace Content.Client.Atmos.EntitySystems
|
|||||||
[Dependency] private readonly SharedTransformSystem _xformSys = default!;
|
[Dependency] private readonly SharedTransformSystem _xformSys = default!;
|
||||||
|
|
||||||
private GasTileOverlay _overlay = default!;
|
private GasTileOverlay _overlay = default!;
|
||||||
|
private GasTileHeatOverlay _heatOverlay = default!;
|
||||||
|
|
||||||
public override void Initialize()
|
public override void Initialize()
|
||||||
{
|
{
|
||||||
@@ -28,12 +29,16 @@ namespace Content.Client.Atmos.EntitySystems
|
|||||||
|
|
||||||
_overlay = new GasTileOverlay(this, EntityManager, _resourceCache, ProtoMan, _spriteSys, _xformSys);
|
_overlay = new GasTileOverlay(this, EntityManager, _resourceCache, ProtoMan, _spriteSys, _xformSys);
|
||||||
_overlayMan.AddOverlay(_overlay);
|
_overlayMan.AddOverlay(_overlay);
|
||||||
|
|
||||||
|
_heatOverlay = new GasTileHeatOverlay();
|
||||||
|
_overlayMan.AddOverlay(_heatOverlay);
|
||||||
}
|
}
|
||||||
|
|
||||||
public override void Shutdown()
|
public override void Shutdown()
|
||||||
{
|
{
|
||||||
base.Shutdown();
|
base.Shutdown();
|
||||||
_overlayMan.RemoveOverlay<GasTileOverlay>();
|
_overlayMan.RemoveOverlay<GasTileOverlay>();
|
||||||
|
_overlayMan.RemoveOverlay<GasTileHeatOverlay>();
|
||||||
}
|
}
|
||||||
|
|
||||||
private void OnHandleState(EntityUid gridUid, GasTileOverlayComponent comp, ref ComponentHandleState args)
|
private void OnHandleState(EntityUid gridUid, GasTileOverlayComponent comp, ref ComponentHandleState args)
|
||||||
|
|||||||
210
Content.Client/Atmos/Overlays/GasTileHeatOverlay.cs
Normal file
210
Content.Client/Atmos/Overlays/GasTileHeatOverlay.cs
Normal file
@@ -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();
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -1,6 +1,4 @@
|
|||||||
using System.Linq;
|
|
||||||
using System.Runtime.CompilerServices;
|
using System.Runtime.CompilerServices;
|
||||||
using System.Threading.Tasks;
|
|
||||||
using Content.Server.Atmos.Components;
|
using Content.Server.Atmos.Components;
|
||||||
using Content.Shared.Atmos;
|
using Content.Shared.Atmos;
|
||||||
using Content.Shared.Atmos.Components;
|
using Content.Shared.Atmos.Components;
|
||||||
@@ -13,7 +11,6 @@ using JetBrains.Annotations;
|
|||||||
using Microsoft.Extensions.ObjectPool;
|
using Microsoft.Extensions.ObjectPool;
|
||||||
using Robust.Server.Player;
|
using Robust.Server.Player;
|
||||||
using Robust.Shared;
|
using Robust.Shared;
|
||||||
using Robust.Shared.Configuration;
|
|
||||||
using Robust.Shared.Enums;
|
using Robust.Shared.Enums;
|
||||||
using Robust.Shared.Map;
|
using Robust.Shared.Map;
|
||||||
using Robust.Shared.Map.Components;
|
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 IGameTiming _gameTiming = default!;
|
||||||
[Robust.Shared.IoC.Dependency] private readonly IPlayerManager _playerManager = 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 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 IParallelManager _parMan = default!;
|
||||||
[Robust.Shared.IoC.Dependency] private readonly AtmosphereSystem _atmosphereSystem = default!;
|
[Robust.Shared.IoC.Dependency] private readonly AtmosphereSystem _atmosphereSystem = default!;
|
||||||
[Robust.Shared.IoC.Dependency] private readonly ChunkingSystem _chunkingSys = 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<MapGridComponent> _gridQuery;
|
||||||
private EntityQuery<GasTileOverlayComponent> _query;
|
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()
|
public override void Initialize()
|
||||||
{
|
{
|
||||||
base.Initialize();
|
base.Initialize();
|
||||||
@@ -85,9 +87,10 @@ namespace Content.Server.Atmos.EntitySystems
|
|||||||
};
|
};
|
||||||
|
|
||||||
_playerManager.PlayerStatusChanged += OnPlayerStatusChanged;
|
_playerManager.PlayerStatusChanged += OnPlayerStatusChanged;
|
||||||
Subs.CVar(_confMan, CCVars.NetGasOverlayTickRate, UpdateTickRate, true);
|
Subs.CVar(ConfMan, CCVars.NetGasOverlayTickRate, UpdateTickRate, true);
|
||||||
Subs.CVar(_confMan, CCVars.GasOverlayThresholds, UpdateThresholds, true);
|
Subs.CVar(ConfMan, CCVars.GasOverlayThresholds, UpdateThresholds, true);
|
||||||
Subs.CVar(_confMan, CVars.NetPVS, OnPvsToggle, true);
|
Subs.CVar(ConfMan, CVars.NetPVS, OnPvsToggle, true);
|
||||||
|
Subs.CVar(ConfMan, CCVars.GasOverlayHeatThreshold, UpdateHeatThresholds, true);
|
||||||
|
|
||||||
SubscribeLocalEvent<RoundRestartCleanupEvent>(Reset);
|
SubscribeLocalEvent<RoundRestartCleanupEvent>(Reset);
|
||||||
SubscribeLocalEvent<GasTileOverlayComponent, ComponentStartup>(OnStartup);
|
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 UpdateTickRate(float value) => _updateInterval = value > 0.0f ? 1 / value : float.MaxValue;
|
||||||
private void UpdateThresholds(int value) => _thresholds = value;
|
private void UpdateThresholds(int value) => _thresholds = value;
|
||||||
|
private void UpdateHeatThresholds(float v) => _heatDistortionStrengthChangeTolerance = MathHelper.Clamp01(v);
|
||||||
|
|
||||||
[MethodImpl(MethodImplOptions.AggressiveInlining)]
|
[MethodImpl(MethodImplOptions.AggressiveInlining)]
|
||||||
public void Invalidate(Entity<GasTileOverlayComponent?> grid, Vector2i index)
|
public void Invalidate(Entity<GasTileOverlayComponent?> grid, Vector2i index)
|
||||||
@@ -175,7 +179,9 @@ namespace Content.Server.Atmos.EntitySystems
|
|||||||
|
|
||||||
public GasOverlayData GetOverlayData(GasMixture? mixture)
|
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++)
|
for (var i = 0; i < VisibleGasId.Length; i++)
|
||||||
{
|
{
|
||||||
@@ -215,15 +221,17 @@ namespace Content.Server.Atmos.EntitySystems
|
|||||||
}
|
}
|
||||||
|
|
||||||
var changed = false;
|
var changed = false;
|
||||||
|
var temp = tile.Hotspot.Valid ? tile.Hotspot.Temperature : tile.Air?.Temperature ?? Atmospherics.TCMB;
|
||||||
if (oldData.Equals(default))
|
if (oldData.Equals(default))
|
||||||
{
|
{
|
||||||
changed = true;
|
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;
|
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})
|
if (tile is {Air: not null, NoGridTile: false})
|
||||||
@@ -271,6 +279,20 @@ namespace Content.Server.Atmos.EntitySystems
|
|||||||
return true;
|
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()
|
private void UpdateOverlayData()
|
||||||
{
|
{
|
||||||
// TODO parallelize?
|
// TODO parallelize?
|
||||||
|
|||||||
@@ -1,5 +1,7 @@
|
|||||||
using Content.Shared.Atmos.Components;
|
using Content.Shared.Atmos.Components;
|
||||||
using Content.Shared.Atmos.Prototypes;
|
using Content.Shared.Atmos.Prototypes;
|
||||||
|
using Content.Shared.CCVar;
|
||||||
|
using Robust.Shared.Configuration;
|
||||||
using Robust.Shared.GameStates;
|
using Robust.Shared.GameStates;
|
||||||
using Robust.Shared.Prototypes;
|
using Robust.Shared.Prototypes;
|
||||||
using Robust.Shared.Serialization;
|
using Robust.Shared.Serialization;
|
||||||
@@ -8,11 +10,26 @@ namespace Content.Shared.Atmos.EntitySystems
|
|||||||
{
|
{
|
||||||
public abstract class SharedGasTileOverlaySystem : EntitySystem
|
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;
|
public const byte ChunkSize = 8;
|
||||||
protected float AccumulatedFrameTime;
|
protected float AccumulatedFrameTime;
|
||||||
protected bool PvsEnabled;
|
protected bool PvsEnabled;
|
||||||
|
|
||||||
[Dependency] protected readonly IPrototypeManager ProtoMan = default!;
|
[Dependency] protected readonly IPrototypeManager ProtoMan = default!;
|
||||||
|
[Dependency] protected readonly IConfigurationManager ConfMan = default!;
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// array of the ids of all visible gases.
|
/// array of the ids of all visible gases.
|
||||||
@@ -22,6 +39,11 @@ namespace Content.Shared.Atmos.EntitySystems
|
|||||||
public override void Initialize()
|
public override void Initialize()
|
||||||
{
|
{
|
||||||
base.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);
|
SubscribeLocalEvent<GasTileOverlayComponent, ComponentGetState>(OnGetState);
|
||||||
|
|
||||||
List<int> visibleGases = new();
|
List<int> visibleGases = new();
|
||||||
@@ -36,6 +58,29 @@ namespace Content.Shared.Atmos.EntitySystems
|
|||||||
VisibleGasId = visibleGases.ToArray();
|
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)
|
private void OnGetState(EntityUid uid, GasTileOverlayComponent component, ref ComponentGetState args)
|
||||||
{
|
{
|
||||||
if (PvsEnabled && !args.ReplayState)
|
if (PvsEnabled && !args.ReplayState)
|
||||||
@@ -72,14 +117,26 @@ namespace Content.Shared.Atmos.EntitySystems
|
|||||||
[ViewVariables]
|
[ViewVariables]
|
||||||
public readonly byte[] Opacity;
|
public readonly byte[] Opacity;
|
||||||
|
|
||||||
// TODO change fire color based on temps
|
/// <summary>
|
||||||
// But also: dont dirty on a 0.01 kelvin change in temperatures.
|
/// This temperature is currently only used by the GasTileHeatOverlay.
|
||||||
// Either have a temp tolerance, or map temperature -> byte levels
|
/// 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;
|
||||||
|
|
||||||
public GasOverlayData(byte fireState, byte[] opacity)
|
// TODO change fire color based on temps
|
||||||
|
|
||||||
|
public GasOverlayData(byte fireState, byte[] opacity, float temperature)
|
||||||
{
|
{
|
||||||
FireState = fireState;
|
FireState = fireState;
|
||||||
Opacity = opacity;
|
Opacity = opacity;
|
||||||
|
Temperature = temperature;
|
||||||
}
|
}
|
||||||
|
|
||||||
public bool Equals(GasOverlayData other)
|
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;
|
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]
|
[Serializable, NetSerializable]
|
||||||
public sealed class GasOverlayUpdateEvent : EntityEventArgs
|
public sealed class GasOverlayUpdateEvent : EntityEventArgs
|
||||||
{
|
{
|
||||||
|
|||||||
@@ -12,4 +12,23 @@ public sealed partial class CCVars
|
|||||||
|
|
||||||
public static readonly CVarDef<int> GasOverlayThresholds =
|
public static readonly CVarDef<int> GasOverlayThresholds =
|
||||||
CVarDef.Create("net.gasoverlaythresholds", 20);
|
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.");
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -115,3 +115,13 @@
|
|||||||
id: Hologram
|
id: Hologram
|
||||||
kind: source
|
kind: source
|
||||||
path: "/Textures/Shaders/hologram.swsl"
|
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
|
||||||
|
|||||||
90
Resources/Textures/Shaders/heat.swsl
Normal file
90
Resources/Textures/Shaders/heat.swsl
Normal file
@@ -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;
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user