Vacuum shader (#46038)

* init

* slider

* fixes

* fixes

* vacuum

* toggle

* turned on be default lol

* typo

* mediump

* marker thingis

* GetWorldPositionRotationMatrixWithInv

* cvar dynamic

* fixes

* magic numbers bad

* discard

* refactors will continue until clean code improves

* format

* comm

---------

Co-authored-by: Pok245 <113675512+Pok27@users.noreply.github.com>
This commit is contained in:
InsoPL 2026-10-04 12:58:17 +00:00 • committed by GitHub
parent 64381ef101
commit cfd925d9e7
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
10 changed files with 715 additions and 410 deletions

View file

@ -0,0 +1,65 @@
using Content.Client.Atmos.Overlays;
using Content.Shared.CCVar;
using JetBrains.Annotations;
using Robust.Client.Graphics;
using Robust.Shared.Configuration;
namespace Content.Client.Atmos.EntitySystems;
/// <summary>
/// System responsible for rendering vacuum effects using <see cref="GasTileVacuumOverlay"/>.
/// </summary>
[UsedImplicitly]
public sealed partial class GasTileVacuumOverlaySystem : EntitySystem
{
[Dependency] private IOverlayManager _overlayMan = default!;
[Dependency] private IConfigurationManager _cfgManager = default!;
private GasTileVacuumOverlay? _gasTileVacuumOverlay;
private bool _vacuumOverlayEnabledDueToCvar; // due to CVar being enabled
private bool _vacuumOverlayEnabledIntensity; // due to non zero intensity in setting
public override void Initialize()
{
base.Initialize();
_cfgManager.OnValueChanged(CCVars.VacuumOverlay, OnVacuumOverlayChanged, true);
_cfgManager.OnValueChanged(CCVars.VacuumOverlayIntensity, OnVacuumOverlayIntensityChanged, true);
}
public override void Shutdown()
{
base.Shutdown();
_cfgManager.UnsubValueChanged(CCVars.VacuumOverlay, OnVacuumOverlayChanged);
_cfgManager.UnsubValueChanged(CCVars.VacuumOverlayIntensity, OnVacuumOverlayIntensityChanged);
_overlayMan.RemoveOverlay<GasTileVacuumOverlay>();
}
private void OnVacuumOverlayChanged(bool enabled)
{
_vacuumOverlayEnabledDueToCvar = enabled;
UpdateOverlay();
}
private void OnVacuumOverlayIntensityChanged(float intensity)
{
_vacuumOverlayEnabledIntensity = intensity > 0f;
UpdateOverlay();
}
private void UpdateOverlay()
{
if (_vacuumOverlayEnabledDueToCvar && _vacuumOverlayEnabledIntensity)
{
_gasTileVacuumOverlay ??= new GasTileVacuumOverlay();
if (!_overlayMan.HasOverlay<GasTileVacuumOverlay>())
_overlayMan.AddOverlay(_gasTileVacuumOverlay);
}
else
{
_overlayMan.RemoveOverlay<GasTileVacuumOverlay>();
}
}
}

View file

@ -0,0 +1,196 @@
using System.Numerics;
using Content.Client.Graphics;
using Content.Shared.Atmos;
using Content.Shared.Atmos.Components;
using Content.Shared.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;
using Color = Robust.Shared.Maths.Color;
namespace Content.Client.Atmos.Overlays;
/// <summary>
/// Overlay responsible for rendering vacuum overlay.
/// </summary>
public sealed partial class GasTileVacuumOverlay : Overlay
{
public override bool RequestScreenTexture { get; set; } = true;
private static readonly ProtoId<ShaderPrototype> UnshadedShader = "unshaded";
private static readonly ProtoId<ShaderPrototype> VacuumOverlayShader = "VacuumDesaturation";
private static readonly Color EmptyColor = new(0, 0, 0, 0);
private static readonly Color MarkerColor = new(255f, 0, 0);
[Dependency] private IEntityManager _entManager = default!;
[Dependency] private IPrototypeManager _proto = default!;
[Dependency] private IClyde _clyde = default!;
[Dependency] private IConfigurationManager _configManager = default!;
private readonly SharedMapSystem _maps;
private readonly SharedTransformSystem _xformSys;
private readonly ShaderInstance _unshader;
private readonly ShaderInstance _shader;
private List<Entity<MapGridComponent>> _intersectingGrids = new();
private readonly OverlayResourceCache<CachedResources> _resources = new();
// Overlay settings
private float _intensity; // overlay intensity. 0.0f = turned off, 1.0f = full grayscale
public override OverlaySpace Space => OverlaySpace.WorldSpace;
public GasTileVacuumOverlay()
{
IoCManager.InjectDependencies(this);
_maps = _entManager.System<SharedMapSystem>();
_xformSys = _entManager.System<SharedTransformSystem>();
_unshader = _proto.Index(UnshadedShader).Instance();
_shader = _proto.Index(VacuumOverlayShader).InstanceUnique();
_configManager.OnValueChanged(CCVars.VacuumOverlayIntensity, SetVacuumOverlayIntensity, invokeImmediately: true);
}
private void SetVacuumOverlayIntensity(float intensity)
{
_intensity = MathHelper.Clamp(intensity, 0f, 1f);
_shader.SetParameter("intensity", _intensity);
}
protected override bool BeforeDraw(in OverlayDrawArgs args)
{
if (args.MapId == MapId.Nullspace)
return false;
var res = _resources.GetForViewport(args.Viewport, static _ => new CachedResources());
var target = args.Viewport.RenderTarget;
// Probably the resolution of the game window changed, remake the textures.
if (res.VacuumTarget?.Texture.Size != target.Size)
{
res.VacuumTarget?.Dispose();
res.VacuumTarget = _clyde.CreateRenderTarget(
target.Size,
new RenderTargetFormatParameters(RenderTargetColorFormat.Rgba8Srgb),
name: $"{nameof(GasTileVacuumOverlay)}-blur");
}
var overlayQuery = _entManager.GetEntityQuery<GasTileOverlayComponent>();
args.WorldHandle.UseShader(_unshader);
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 Vacuum after checking all visible tiles, we can bail early
var anyVacuum = false;
// We're rendering in the context of the vacuum target texture, which will encode data as to where vacuum effect will be
args.WorldHandle.RenderInRenderTarget(res.VacuumTarget,
() =>
{
_intersectingGrids.Clear();
_maps.FindGridsIntersecting(mapId, worldAABB, ref _intersectingGrids);
foreach (var grid in _intersectingGrids)
{
if (!overlayQuery.TryGetComponent(grid.Owner, out var comp))
continue;
var (_, _, gridEntToWorld, worldToGridLocal) = _xformSys.GetWorldPositionRotationMatrixWithInv(grid.Owner);
var gridEntToViewportLocal = gridEntToWorld * worldToViewportLocal;
if (!Matrix3x2.Invert(gridEntToViewportLocal, out _))
continue;
// 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 = worldToGridLocal.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;
if (tileGas.ByteGasTemperature.Value != ThermalByte.StateVacuum)
continue;
// Encode the strength in the red channel
// alpha set to 1 as tile is active
worldHandle.DrawRect(Box2.CenteredAround(tilePosition + grid.Comp.TileSizeHalfVector,
grid.Comp.TileSizeVector),
MarkerColor);
anyVacuum = true;
}
}
}
},
// This clears the buffer to all zero first...
EmptyColor);
// no distortion, no need to render
if (!anyVacuum)
{
args.WorldHandle.UseShader(null);
args.WorldHandle.SetTransform(Matrix3x2.Identity);
return false;
}
return true;
}
protected override void Draw(in OverlayDrawArgs args)
{
var res = _resources.GetForViewport(args.Viewport, static _ => new CachedResources());
if (ScreenTexture is null || res.VacuumTarget is null)
return;
_shader.SetParameter("SCREEN_TEXTURE", ScreenTexture);
args.WorldHandle.UseShader(_shader);
args.WorldHandle.DrawTextureRect(res.VacuumTarget.Texture, args.WorldBounds);
args.WorldHandle.UseShader(null);
args.WorldHandle.SetTransform(Matrix3x2.Identity);
}
protected override void DisposeBehavior()
{
_resources.Dispose();
_configManager.UnsubValueChanged(CCVars.VacuumOverlayIntensity, SetVacuumOverlayIntensity);
base.DisposeBehavior();
}
internal sealed class CachedResources : IDisposable
{
public IRenderTexture? VacuumTarget;
public void Dispose()
{
VacuumTarget?.Dispose();
}
}
}

View file

@ -44,6 +44,7 @@
<ui:OptionSlider Name="SpeechBubbleTextOpacitySlider" Title="{Loc 'ui-options-speech-bubble-text-opacity'}" />
<ui:OptionSlider Name="SpeechBubbleSpeakerOpacitySlider" Title="{Loc 'ui-options-speech-bubble-speaker-opacity'}" />
<ui:OptionSlider Name="SpeechBubbleBackgroundOpacitySlider" Title="{Loc 'ui-options-speech-bubble-background-opacity'}" />
<ui:OptionSlider Name="VacuumOverlayIntensityOpacitySlider" Title="{Loc 'ui-options-vacuum-overlay-intensity'}" />
<CheckBox Name="AutoFillHighlightsCheckBox" Text="{Loc 'ui-options-auto-fill-highlights'}" />
<ui:OptionColorSlider Name="HighlightsColorSlider"
Title="{Loc 'ui-options-highlights-color'}"

View file

@ -2,14 +2,19 @@ using Content.Shared.CCVar;
using Robust.Client.AutoGenerated;
using Robust.Client.UserInterface;
using Robust.Client.UserInterface.XAML;
using Robust.Shared.Configuration;
namespace Content.Client.Options.UI.Tabs;
[GenerateTypedNameReferences]
public sealed partial class AccessibilityTab : Control
{
[Dependency] private IConfigurationManager _cfgManager = default!;
public AccessibilityTab()
{
IoCManager.InjectDependencies(this);
RobustXamlLoader.Load(this);
Control.AddOptionCheckBox(CCVars.ChatEnableColorName, EnableColorNameCheckBox);
@ -30,11 +35,14 @@ public sealed partial class AccessibilityTab : Control
Control.AddOptionPercentSlider(CCVars.SpeechBubbleTextOpacity, SpeechBubbleTextOpacitySlider);
Control.AddOptionPercentSlider(CCVars.SpeechBubbleSpeakerOpacity, SpeechBubbleSpeakerOpacitySlider);
Control.AddOptionPercentSlider(CCVars.SpeechBubbleBackgroundOpacity, SpeechBubbleBackgroundOpacitySlider);
Control.AddOptionPercentSlider(CCVars.VacuumOverlayIntensity, VacuumOverlayIntensityOpacitySlider);
Control.AddOptionCheckBox(CCVars.ChatAutoFillHighlights, AutoFillHighlightsCheckBox);
Control.AddOptionColorSlider(CCVars.ChatHighlightsColor, HighlightsColorSlider);
Control.AddOptionCheckBox(CCVars.AccessibilityClientCensorNudity, CensorNudityCheckBox);
VacuumOverlayIntensityOpacitySlider.Visible = _cfgManager.GetCVar(CCVars.VacuumOverlay);
Control.Initialize();
}
}

View file

@ -10,7 +10,6 @@ using Microsoft.Extensions.ObjectPool;
using Robust.Server.Player;
using Robust.Shared;
using Robust.Shared.Enums;
using Robust.Shared.Map;
using Robust.Shared.Map.Components;
using Robust.Shared.Player;
using Robust.Shared.Threading;
@ -20,467 +19,466 @@ using System.Runtime.CompilerServices;
// ReSharper disable once RedundantUsingDirective
namespace Content.Server.Atmos.EntitySystems
namespace Content.Server.Atmos.EntitySystems;
[UsedImplicitly]
public sealed partial class GasTileOverlaySystem : SharedGasTileOverlaySystem
{
[UsedImplicitly]
public sealed partial class GasTileOverlaySystem : SharedGasTileOverlaySystem
[Robust.Shared.IoC.Dependency] private IGameTiming _gameTiming = default!;
[Robust.Shared.IoC.Dependency] private IPlayerManager _playerManager = default!;
[Robust.Shared.IoC.Dependency] private IParallelManager _parMan = default!;
[Robust.Shared.IoC.Dependency] private AtmosphereSystem _atmosphereSystem = default!;
[Robust.Shared.IoC.Dependency] private ChunkingSystem _chunkingSys = default!;
[Robust.Shared.IoC.Dependency] private EntityQuery<MapGridComponent> _mapGridQuery;
[Robust.Shared.IoC.Dependency] private EntityQuery<GasTileOverlayComponent> _gasTileOverlayQuery;
/// <summary>
/// Per-tick cache of sessions.
/// </summary>
private readonly List<ICommonSession> _sessions = new();
private UpdatePlayerJob _updateJob;
private readonly Dictionary<ICommonSession, Dictionary<NetEntity, HashSet<Vector2i>>> _lastSentChunks = new();
// Oh look its more duplicated decal system code!
private readonly ObjectPool<HashSet<Vector2i>> _chunkIndexPool =
new DefaultObjectPool<HashSet<Vector2i>>(
new DefaultPooledObjectPolicy<HashSet<Vector2i>>(), 64);
private readonly ObjectPool<Dictionary<NetEntity, HashSet<Vector2i>>> _chunkViewerPool =
new DefaultObjectPool<Dictionary<NetEntity, HashSet<Vector2i>>>(
new DefaultPooledObjectPolicy<Dictionary<NetEntity, HashSet<Vector2i>>>(), 64);
private bool _doSessionUpdate;
/// <summary>
/// Overlay update interval, in seconds.
/// </summary>
private float _updateInterval;
private int _thresholds;
public override void Initialize()
{
[Robust.Shared.IoC.Dependency] private IGameTiming _gameTiming = default!;
[Robust.Shared.IoC.Dependency] private IPlayerManager _playerManager = default!;
[Robust.Shared.IoC.Dependency] private IParallelManager _parMan = default!;
[Robust.Shared.IoC.Dependency] private AtmosphereSystem _atmosphereSystem = default!;
[Robust.Shared.IoC.Dependency] private ChunkingSystem _chunkingSys = default!;
base.Initialize();
[Robust.Shared.IoC.Dependency] private EntityQuery<MapGridComponent> _mapGridQuery = default!;
[Robust.Shared.IoC.Dependency] private EntityQuery<GasTileOverlayComponent> _gasTileOverlayQuery = default!;
/// <summary>
/// Per-tick cache of sessions.
/// </summary>
private readonly List<ICommonSession> _sessions = new();
private UpdatePlayerJob _updateJob;
private readonly Dictionary<ICommonSession, Dictionary<NetEntity, HashSet<Vector2i>>> _lastSentChunks = new();
// Oh look its more duplicated decal system code!
private ObjectPool<HashSet<Vector2i>> _chunkIndexPool =
new DefaultObjectPool<HashSet<Vector2i>>(
new DefaultPooledObjectPolicy<HashSet<Vector2i>>(), 64);
private ObjectPool<Dictionary<NetEntity, HashSet<Vector2i>>> _chunkViewerPool =
new DefaultObjectPool<Dictionary<NetEntity, HashSet<Vector2i>>>(
new DefaultPooledObjectPolicy<Dictionary<NetEntity, HashSet<Vector2i>>>(), 64);
private bool _doSessionUpdate;
/// <summary>
/// Overlay update interval, in seconds.
/// </summary>
private float _updateInterval;
private int _thresholds;
public override void Initialize()
_updateJob = new UpdatePlayerJob()
{
base.Initialize();
EntManager = EntityManager,
System = this,
ChunkIndexPool = _chunkIndexPool,
Sessions = _sessions,
ChunkingSys = _chunkingSys,
ChunkViewerPool = _chunkViewerPool,
LastSentChunks = _lastSentChunks,
GridQuery = _mapGridQuery,
};
_updateJob = new UpdatePlayerJob()
_playerManager.PlayerStatusChanged += OnPlayerStatusChanged;
InitializeCVars();
SubscribeLocalEvent<RoundRestartCleanupEvent>(Reset);
SubscribeLocalEvent<GasTileOverlayComponent, ComponentStartup>(OnStartup);
}
private void OnStartup(EntityUid uid, GasTileOverlayComponent component, ComponentStartup args)
{
// This **shouldn't** be required, but just in case we ever get entity prototypes that have gas overlays, we
// need to ensure that we send an initial full state to players.
Dirty(uid, component);
}
public override void Shutdown()
{
base.Shutdown();
_playerManager.PlayerStatusChanged -= OnPlayerStatusChanged;
}
private void OnPvsToggle(bool value)
{
if (value == PvsEnabled)
return;
PvsEnabled = value;
if (value)
return;
foreach (var lastSent in _lastSentChunks.Values)
{
foreach (var set in lastSent.Values)
{
EntManager = EntityManager,
System = this,
ChunkIndexPool = _chunkIndexPool,
Sessions = _sessions,
ChunkingSys = _chunkingSys,
ChunkViewerPool = _chunkViewerPool,
LastSentChunks = _lastSentChunks,
GridQuery = _mapGridQuery,
};
_playerManager.PlayerStatusChanged += OnPlayerStatusChanged;
InitializeCVars();
SubscribeLocalEvent<RoundRestartCleanupEvent>(Reset);
SubscribeLocalEvent<GasTileOverlayComponent, ComponentStartup>(OnStartup);
set.Clear();
_chunkIndexPool.Return(set);
}
lastSent.Clear();
}
private void OnStartup(EntityUid uid, GasTileOverlayComponent component, ComponentStartup args)
// PVS was turned off, ensure data gets sent to all clients.
var query = AllEntityQuery<GasTileOverlayComponent, MetaDataComponent>();
while (query.MoveNext(out var uid, out var grid, out var meta))
{
// This **shouldn't** be required, but just in case we ever get entity prototypes that have gas overlays, we
// need to ensure that we send an initial full state to players.
Dirty(uid, component);
grid.ForceTick = _gameTiming.CurTick;
Dirty(uid, grid, meta);
}
}
public override void Shutdown()
private void UpdateTickRate(float value) => _updateInterval = value > 0.0f ? 1 / value : float.MaxValue;
private void UpdateThresholds(int value) => _thresholds = value;
[MethodImpl(MethodImplOptions.AggressiveInlining)]
public void Invalidate(Entity<GasTileOverlayComponent?> grid, Vector2i index)
{
if (_gasTileOverlayQuery.Resolve(grid.Owner, ref grid.Comp))
grid.Comp.InvalidTiles.Add(index);
}
private void OnPlayerStatusChanged(object? sender, SessionStatusEventArgs e)
{
if (e.NewStatus != SessionStatus.InGame)
{
base.Shutdown();
_playerManager.PlayerStatusChanged -= OnPlayerStatusChanged;
}
private void OnPvsToggle(bool value)
{
if (value == PvsEnabled)
return;
PvsEnabled = value;
if (value)
return;
foreach (var lastSent in _lastSentChunks.Values)
if (_lastSentChunks.Remove(e.Session, out var sets))
{
foreach (var set in lastSent.Values)
foreach (var set in sets.Values)
{
set.Clear();
_chunkIndexPool.Return(set);
}
lastSent.Clear();
}
// PVS was turned off, ensure data gets sent to all clients.
var query = AllEntityQuery<GasTileOverlayComponent, MetaDataComponent>();
while (query.MoveNext(out var uid, out var grid, out var meta))
{
grid.ForceTick = _gameTiming.CurTick;
Dirty(uid, grid, meta);
}
}
private void UpdateTickRate(float value) => _updateInterval = value > 0.0f ? 1 / value : float.MaxValue;
private void UpdateThresholds(int value) => _thresholds = value;
[MethodImpl(MethodImplOptions.AggressiveInlining)]
public void Invalidate(Entity<GasTileOverlayComponent?> grid, Vector2i index)
if (!_lastSentChunks.ContainsKey(e.Session))
{
if (_gasTileOverlayQuery.Resolve(grid.Owner, ref grid.Comp))
grid.Comp.InvalidTiles.Add(index);
_lastSentChunks[e.Session] = new();
}
}
private void OnPlayerStatusChanged(object? sender, SessionStatusEventArgs e)
private byte GetOpacity(float moles, float molesVisible, float molesVisibleMax)
{
return (byte) (ContentHelpers.RoundToLevels(
MathHelper.Clamp01((moles - molesVisible) /
(molesVisibleMax - molesVisible)) * 255, byte.MaxValue,
_thresholds) * 255 / (_thresholds - 1));
}
public GasOverlayData GetOverlayData(GasMixture? mixture)
{
ThermalByte byteTemp;
if (mixture == null)
{
if (e.NewStatus != SessionStatus.InGame)
byteTemp = new();
byteTemp.SetVacuum();
}
else
byteTemp = new(mixture.Temperature);
var data = new GasOverlayData(0, new byte[VisibleGasId.Length], byteTemp);
for (var i = 0; i < VisibleGasId.Length; i++)
{
var id = VisibleGasId[i];
var gas = _atmosphereSystem.GetGas(id);
var moles = mixture?[id] ?? 0f;
ref var opacity = ref data.Opacity[i];
if (moles < gas.GasMolesVisible)
{
if (_lastSentChunks.Remove(e.Session, out var sets))
{
foreach (var set in sets.Values)
{
set.Clear();
_chunkIndexPool.Return(set);
}
}
continue;
}
if (!_lastSentChunks.ContainsKey(e.Session))
{
_lastSentChunks[e.Session] = new();
}
}
private byte GetOpacity(float moles, float molesVisible, float molesVisibleMax)
{
return (byte) (ContentHelpers.RoundToLevels(
MathHelper.Clamp01((moles - molesVisible) /
(molesVisibleMax - molesVisible)) * 255, byte.MaxValue,
opacity = (byte) (ContentHelpers.RoundToLevels(
MathHelper.Clamp01((moles - gas.GasMolesVisible) /
(gas.GasMolesVisibleMax - gas.GasMolesVisible)) * 255, byte.MaxValue,
_thresholds) * 255 / (_thresholds - 1));
}
public GasOverlayData GetOverlayData(GasMixture? mixture)
return data;
}
/// <summary>
/// Updates the visuals for a tile on some grid chunk. Returns true if the visuals have changed.
/// </summary>
private bool UpdateChunkTile(GridAtmosphereComponent gridAtmosphere, GasOverlayChunk chunk, Vector2i index)
{
ref var oldData = ref chunk.TileData[chunk.GetDataIndex(index)];
if (!gridAtmosphere.Tiles.TryGetValue(index, out var tile))
{
ThermalByte byteTemp;
if (mixture == null)
{
byteTemp = new();
byteTemp.SetVacuum();
}
else
byteTemp = new(mixture.Temperature);
if (oldData.Equals(default))
return false;
var data = new GasOverlayData(0, new byte[VisibleGasId.Length], byteTemp);
chunk.LastUpdate = _gameTiming.CurTick;
oldData = default;
return true;
}
var changed = false;
ThermalByte newByteTemp = new();
if (tile.Hotspot.Valid)
newByteTemp.SetTemperature(tile.Hotspot.Temperature);
else if (!tile.Space && tile.Air?.TotalMoles <= ThermalByte.MinimalPressureThreshold)
newByteTemp.SetVacuum();
else if (!tile.Space && tile.Air != null)
newByteTemp = new(tile.Air.Temperature);
if (oldData.Equals(default))
{
changed = true;
oldData = new GasOverlayData(tile.Hotspot.State, new byte[VisibleGasId.Length], newByteTemp);
}
else if (oldData.FireState != tile.Hotspot.State ||
Math.Abs(oldData.ByteGasTemperature.Value - newByteTemp.Value) > 1 || // Dirty Temperature when there is more then 1 byte difference. That should measure up to minimum 4 degreese difference, 6 degreese on average.
oldData.ByteGasTemperature.Value != newByteTemp.Value && newByteTemp.Value > ThermalByte.TempResolution) // change of special ThermalByte value
{
changed = true;
oldData = new GasOverlayData(tile.Hotspot.State, oldData.Opacity, newByteTemp);
}
if (tile is {Air: not null, NoGridTile: false})
{
for (var i = 0; i < VisibleGasId.Length; i++)
{
var id = VisibleGasId[i];
var gas = _atmosphereSystem.GetGas(id);
var moles = mixture?[id] ?? 0f;
ref var opacity = ref data.Opacity[i];
var moles = tile.Air[id];
ref var oldOpacity = ref oldData.Opacity[i];
if (moles < gas.GasMolesVisible)
{
if (oldOpacity != 0)
{
oldOpacity = 0;
changed = true;
}
continue;
}
opacity = (byte) (ContentHelpers.RoundToLevels(
MathHelper.Clamp01((moles - gas.GasMolesVisible) /
(gas.GasMolesVisibleMax - gas.GasMolesVisible)) * 255, byte.MaxValue,
_thresholds) * 255 / (_thresholds - 1));
}
var opacity = GetOpacity(moles, gas.GasMolesVisible, gas.GasMolesVisibleMax);
return data;
if (oldOpacity == opacity)
continue;
oldOpacity = opacity;
changed = true;
}
}
else
{
for (var i = 0; i < VisibleGasId.Length; i++)
{
changed |= oldData.Opacity[i] != 0;
oldData.Opacity[i] = 0;
}
}
/// <summary>
/// Updates the visuals for a tile on some grid chunk. Returns true if the visuals have changed.
/// </summary>
private bool UpdateChunkTile(GridAtmosphereComponent gridAtmosphere, GasOverlayChunk chunk, Vector2i index)
if (!changed)
return false;
chunk.LastUpdate = _gameTiming.CurTick;
return true;
}
private void UpdateOverlayData()
{
// TODO parallelize?
var query = AllEntityQuery<GasTileOverlayComponent, GridAtmosphereComponent, MetaDataComponent>();
while (query.MoveNext(out var uid, out var overlay, out var gam, out var meta))
{
ref var oldData = ref chunk.TileData[chunk.GetDataIndex(index)];
if (!gridAtmosphere.Tiles.TryGetValue(index, out var tile))
{
if (oldData.Equals(default))
return false;
chunk.LastUpdate = _gameTiming.CurTick;
oldData = default;
return true;
}
var changed = false;
ThermalByte newByteTemp = new();
if (tile.Hotspot.Valid)
newByteTemp.SetTemperature(tile.Hotspot.Temperature);
else if (!tile.Space && tile.Air?.TotalMoles <= 5f)
newByteTemp.SetVacuum();
else if (!tile.Space && tile.Air != null)
newByteTemp = new(tile.Air.Temperature);
if (oldData.Equals(default))
foreach (var index in overlay.InvalidTiles)
{
changed = true;
oldData = new GasOverlayData(tile.Hotspot.State, new byte[VisibleGasId.Length], newByteTemp);
}
else if (oldData.FireState != tile.Hotspot.State ||
Math.Abs(oldData.ByteGasTemperature.Value - newByteTemp.Value) > 1 || // Dirty Temperature when there is more then 1 byte difference. That should measure up to minimum 4 degreese difference, 6 degreese on average.
(oldData.ByteGasTemperature.Value != newByteTemp.Value && newByteTemp.Value > ThermalByte.TempResolution)) // change of special ThermalByte value
{
changed = true;
oldData = new GasOverlayData(tile.Hotspot.State, oldData.Opacity, newByteTemp);
var chunkIndex = GetGasChunkIndices(index);
if (!overlay.Chunks.TryGetValue(chunkIndex, out var chunk))
overlay.Chunks[chunkIndex] = chunk = new GasOverlayChunk(chunkIndex);
changed |= UpdateChunkTile(gam, chunk, index);
}
if (tile is {Air: not null, NoGridTile: false})
{
for (var i = 0; i < VisibleGasId.Length; i++)
{
var id = VisibleGasId[i];
var gas = _atmosphereSystem.GetGas(id);
var moles = tile.Air[id];
ref var oldOpacity = ref oldData.Opacity[i];
if (changed)
Dirty(uid, overlay, meta);
if (moles < gas.GasMolesVisible)
{
if (oldOpacity != 0)
{
oldOpacity = 0;
changed = true;
}
continue;
}
var opacity = GetOpacity(moles, gas.GasMolesVisible, gas.GasMolesVisibleMax);
if (oldOpacity == opacity)
continue;
oldOpacity = opacity;
changed = true;
}
}
else
{
for (var i = 0; i < VisibleGasId.Length; i++)
{
changed |= oldData.Opacity[i] != 0;
oldData.Opacity[i] = 0;
}
}
if (!changed)
return false;
chunk.LastUpdate = _gameTiming.CurTick;
return true;
}
private void UpdateOverlayData()
{
// TODO parallelize?
var query = AllEntityQuery<GasTileOverlayComponent, GridAtmosphereComponent, MetaDataComponent>();
while (query.MoveNext(out var uid, out var overlay, out var gam, out var meta))
{
var changed = false;
foreach (var index in overlay.InvalidTiles)
{
var chunkIndex = GetGasChunkIndices(index);
if (!overlay.Chunks.TryGetValue(chunkIndex, out var chunk))
overlay.Chunks[chunkIndex] = chunk = new GasOverlayChunk(chunkIndex);
changed |= UpdateChunkTile(gam, chunk, index);
}
if (changed)
Dirty(uid, overlay, meta);
overlay.InvalidTiles.Clear();
}
}
public override void Update(float frameTime)
{
base.Update(frameTime);
AccumulatedFrameTime += frameTime;
if (_doSessionUpdate)
{
UpdateSessions();
return;
}
if (AccumulatedFrameTime < _updateInterval)
return;
AccumulatedFrameTime -= _updateInterval;
// First, update per-chunk visual data for any invalidated tiles.
UpdateOverlayData();
// Then, next tick we send the data to players.
// This is to avoid doing all the work in the same tick.
_doSessionUpdate = true;
}
public void UpdateSessions()
{
_doSessionUpdate = false;
if (!PvsEnabled)
return;
// Now we'll go through each player, then through each chunk in range of that player checking if the player is still in range
// If they are, check if they need the new data to send (i.e. if there's an overlay for the gas).
// Afterwards we reset all the chunk data for the next time we tick.
_sessions.Clear();
foreach (var player in _playerManager.Sessions)
{
if (player.Status != SessionStatus.InGame)
continue;
_sessions.Add(player);
}
if (_sessions.Count == 0)
return;
_parMan.ProcessNow(_updateJob, _sessions.Count);
_updateJob.LastSessionUpdate = _gameTiming.CurTick;
}
public void Reset(RoundRestartCleanupEvent ev)
{
foreach (var data in _lastSentChunks.Values)
{
foreach (var previous in data.Values)
{
previous.Clear();
_chunkIndexPool.Return(previous);
}
data.Clear();
}
}
#region Jobs
/// <summary>
/// Updates per player gas overlay data.
/// </summary>
private record struct UpdatePlayerJob : IParallelRobustJob
{
public int BatchSize => 2;
public IEntityManager EntManager;
public ChunkingSystem ChunkingSys;
public GasTileOverlaySystem System;
public ObjectPool<HashSet<Vector2i>> ChunkIndexPool;
public ObjectPool<Dictionary<NetEntity, HashSet<Vector2i>>> ChunkViewerPool;
public GameTick LastSessionUpdate;
public Dictionary<ICommonSession, Dictionary<NetEntity, HashSet<Vector2i>>> LastSentChunks;
public List<ICommonSession> Sessions;
public EntityQuery<MapGridComponent> GridQuery;
public void Execute(int index)
{
var playerSession = Sessions[index];
var chunksInRange = ChunkingSys.GetChunksForSession(playerSession, ChunkSize, ChunkIndexPool, ChunkViewerPool);
var previouslySent = LastSentChunks[playerSession];
var ev = new GasOverlayUpdateEvent();
foreach (var (netGrid, oldIndices) in previouslySent)
{
// Mark the whole grid as stale and flag for removal.
if (!chunksInRange.TryGetValue(netGrid, out var chunks))
{
previouslySent.Remove(netGrid);
// If grid was deleted then don't worry about sending it to the client.
if (!EntManager.TryGetEntity(netGrid, out var gridId) || GridQuery.HasComp(gridId.Value))
ev.RemovedChunks[netGrid] = oldIndices;
else
{
oldIndices.Clear();
ChunkIndexPool.Return(oldIndices);
}
continue;
}
var old = ChunkIndexPool.Get();
DebugTools.Assert(old.Count == 0);
foreach (var chunk in oldIndices)
{
if (!chunks.Contains(chunk))
old.Add(chunk);
}
if (old.Count == 0)
ChunkIndexPool.Return(old);
else
ev.RemovedChunks.Add(netGrid, old);
}
foreach (var (netGrid, gridChunks) in chunksInRange)
{
// Not all grids have atmospheres.
if (!EntManager.TryGetEntity(netGrid, out var grid) || !EntManager.TryGetComponent(grid, out GasTileOverlayComponent? overlay))
continue;
List<GasOverlayChunk> dataToSend = new();
ev.UpdatedChunks[netGrid] = dataToSend;
previouslySent.TryGetValue(netGrid, out var previousChunks);
foreach (var gIndex in gridChunks)
{
if (!overlay.Chunks.TryGetValue(gIndex, out var value))
continue;
// If the chunk was updated since we last sent it, send it again
if (value.LastUpdate > LastSessionUpdate)
{
dataToSend.Add(value);
continue;
}
// Always send it if we didn't previously send it
if (previousChunks == null || !previousChunks.Contains(gIndex))
dataToSend.Add(value);
}
previouslySent[netGrid] = gridChunks;
if (previousChunks != null)
{
previousChunks.Clear();
ChunkIndexPool.Return(previousChunks);
}
}
if (ev.UpdatedChunks.Count != 0 || ev.RemovedChunks.Count != 0)
System.RaiseNetworkEvent(ev, playerSession.Channel);
}
}
#endregion
private void InitializeCVars()
{
Subs.CVar(ConfMan, CCVars.NetGasOverlayTickRate, UpdateTickRate, true);
Subs.CVar(ConfMan, CCVars.GasOverlayThresholds, UpdateThresholds, true);
Subs.CVar(ConfMan, CVars.NetPVS, OnPvsToggle, true);
overlay.InvalidTiles.Clear();
}
}
public override void Update(float frameTime)
{
base.Update(frameTime);
AccumulatedFrameTime += frameTime;
if (_doSessionUpdate)
{
UpdateSessions();
return;
}
if (AccumulatedFrameTime < _updateInterval)
return;
AccumulatedFrameTime -= _updateInterval;
// First, update per-chunk visual data for any invalidated tiles.
UpdateOverlayData();
// Then, next tick we send the data to players.
// This is to avoid doing all the work in the same tick.
_doSessionUpdate = true;
}
public void UpdateSessions()
{
_doSessionUpdate = false;
if (!PvsEnabled)
return;
// Now we'll go through each player, then through each chunk in range of that player checking if the player is still in range
// If they are, check if they need the new data to send (i.e. if there's an overlay for the gas).
// Afterwards we reset all the chunk data for the next time we tick.
_sessions.Clear();
foreach (var player in _playerManager.Sessions)
{
if (player.Status != SessionStatus.InGame)
continue;
_sessions.Add(player);
}
if (_sessions.Count == 0)
return;
_parMan.ProcessNow(_updateJob, _sessions.Count);
_updateJob.LastSessionUpdate = _gameTiming.CurTick;
}
public void Reset(RoundRestartCleanupEvent ev)
{
foreach (var data in _lastSentChunks.Values)
{
foreach (var previous in data.Values)
{
previous.Clear();
_chunkIndexPool.Return(previous);
}
data.Clear();
}
}
#region Jobs
/// <summary>
/// Updates per player gas overlay data.
/// </summary>
private record struct UpdatePlayerJob : IParallelRobustJob
{
public int BatchSize => 2;
public IEntityManager EntManager;
public ChunkingSystem ChunkingSys;
public GasTileOverlaySystem System;
public ObjectPool<HashSet<Vector2i>> ChunkIndexPool;
public ObjectPool<Dictionary<NetEntity, HashSet<Vector2i>>> ChunkViewerPool;
public GameTick LastSessionUpdate;
public Dictionary<ICommonSession, Dictionary<NetEntity, HashSet<Vector2i>>> LastSentChunks;
public List<ICommonSession> Sessions;
public EntityQuery<MapGridComponent> GridQuery;
public void Execute(int index)
{
var playerSession = Sessions[index];
var chunksInRange = ChunkingSys.GetChunksForSession(playerSession, ChunkSize, ChunkIndexPool, ChunkViewerPool);
var previouslySent = LastSentChunks[playerSession];
var ev = new GasOverlayUpdateEvent();
foreach (var (netGrid, oldIndices) in previouslySent)
{
// Mark the whole grid as stale and flag for removal.
if (!chunksInRange.TryGetValue(netGrid, out var chunks))
{
previouslySent.Remove(netGrid);
// If grid was deleted then don't worry about sending it to the client.
if (!EntManager.TryGetEntity(netGrid, out var gridId) || GridQuery.HasComp(gridId.Value))
ev.RemovedChunks[netGrid] = oldIndices;
else
{
oldIndices.Clear();
ChunkIndexPool.Return(oldIndices);
}
continue;
}
var old = ChunkIndexPool.Get();
DebugTools.Assert(old.Count == 0);
foreach (var chunk in oldIndices)
{
if (!chunks.Contains(chunk))
old.Add(chunk);
}
if (old.Count == 0)
ChunkIndexPool.Return(old);
else
ev.RemovedChunks.Add(netGrid, old);
}
foreach (var (netGrid, gridChunks) in chunksInRange)
{
// Not all grids have atmospheres.
if (!EntManager.TryGetEntity(netGrid, out var grid) || !EntManager.TryGetComponent(grid, out GasTileOverlayComponent? overlay))
continue;
List<GasOverlayChunk> dataToSend = new();
ev.UpdatedChunks[netGrid] = dataToSend;
previouslySent.TryGetValue(netGrid, out var previousChunks);
foreach (var gIndex in gridChunks)
{
if (!overlay.Chunks.TryGetValue(gIndex, out var value))
continue;
// If the chunk was updated since we last sent it, send it again
if (value.LastUpdate > LastSessionUpdate)
{
dataToSend.Add(value);
continue;
}
// Always send it if we didn't previously send it
if (previousChunks == null || !previousChunks.Contains(gIndex))
dataToSend.Add(value);
}
previouslySent[netGrid] = gridChunks;
if (previousChunks != null)
{
previousChunks.Clear();
ChunkIndexPool.Return(previousChunks);
}
}
if (ev.UpdatedChunks.Count != 0 || ev.RemovedChunks.Count != 0)
System.RaiseNetworkEvent(ev, playerSession.Channel);
}
}
#endregion
private void InitializeCVars()
{
Subs.CVar(ConfMan, CCVars.NetGasOverlayTickRate, UpdateTickRate, true);
Subs.CVar(ConfMan, CCVars.GasOverlayThresholds, UpdateThresholds, true);
Subs.CVar(ConfMan, CVars.NetPVS, OnPvsToggle, true);
}
}

View file

@ -162,6 +162,11 @@ public struct ThermalByte : IEquatable<ThermalByte>
public const float TempDegreeResolution = (TempMaximum - TempMinimum) / TempResolution;
public const float TempToByteFactor = TempResolution / (TempMaximum - TempMinimum);
/// <summary>
/// The minimum pressure gas must have for tile temperature to be networked to clients. Below this threshold thermal byte is set to StateVacuum.
/// </summary>
public const float MinimalPressureThreshold = 20f;
private byte _coreValue;
public ThermalByte(float temperatureKelvin)

View file

@ -43,6 +43,18 @@ public sealed partial class CCVars
public static readonly CVarDef<bool> DisableFlashEffect =
CVarDef.Create("accessibility.disable_flash_effect", false, CVar.CLIENTONLY | CVar.ARCHIVE);
/// <summary>
/// Decreases the intensity of the vacuum overlay 1.0f is maximum.
/// </summary>
public static readonly CVarDef<float> VacuumOverlayIntensity =
CVarDef.Create("accessibility.vacuum_overlay_intensity", 0.5f, CVar.CLIENTONLY | CVar.ARCHIVE);
/// <summary>
/// Toggles vacuum overlay serverside. If you do not want it as an option for your players use this.
/// </summary>
public static readonly CVarDef<bool> VacuumOverlay =
CVarDef.Create("accessibility.vacuum_overlay", true, CVar.ARCHIVE | CVar.REPLICATED | CVar.SERVER);
/// <summary>
/// Decreases motion on the heat distortion shader.
/// </summary>

View file

@ -403,6 +403,7 @@ ui-options-chat-window-opacity = Chat window opacity
ui-options-speech-bubble-text-opacity = Speech bubble text opacity
ui-options-speech-bubble-speaker-opacity = Speech bubble speaker opacity
ui-options-speech-bubble-background-opacity = Speech bubble background opacity
ui-options-vacuum-overlay-intensity = Vacuum overlay intensity
ui-options-censor-nudity = Censor character nudity

View file

@ -140,3 +140,8 @@
id: Sharpness
kind: source
path: "/Textures/Shaders/sharpness.swsl"
- type: shader
id: VacuumDesaturation
kind: source
path: "/Textures/Shaders/vacuum_desaturation.swsl"

View file

@ -0,0 +1,14 @@
uniform sampler2D SCREEN_TEXTURE;
uniform mediump float intensity;
void fragment()
{
if (texture2D(TEXTURE, UV).r < 0.1)
discard;
mediump vec4 screen_pixel = texture2D(SCREEN_TEXTURE, UV);
mediump float gray = dot(screen_pixel.rgb, vec3(0.299, 0.587, 0.114));
screen_pixel.rgb = mix(screen_pixel.rgb, vec3(gray), intensity);
COLOR = screen_pixel;
}