Weather effects (#12528)
Co-authored-by: metalgearsloth <metalgearsloth@gmail.com>
This commit is contained in:
@@ -21,6 +21,7 @@ public sealed class ParallaxOverlay : Overlay
|
||||
|
||||
public ParallaxOverlay()
|
||||
{
|
||||
ZIndex = ParallaxSystem.ParallaxZIndex;
|
||||
IoCManager.InjectDependencies(this);
|
||||
_parallax = IoCManager.Resolve<IEntitySystemManager>().GetEntitySystem<ParallaxSystem>();
|
||||
|
||||
|
||||
@@ -15,6 +15,7 @@ public sealed class ParallaxSystem : SharedParallaxSystem
|
||||
[Dependency] private readonly IPrototypeManager _protoManager = default!;
|
||||
|
||||
private const string Fallback = "Default";
|
||||
public const int ParallaxZIndex = 0;
|
||||
|
||||
public override void Initialize()
|
||||
{
|
||||
|
||||
207
Content.Client/Weather/WeatherOverlay.cs
Normal file
207
Content.Client/Weather/WeatherOverlay.cs
Normal file
@@ -0,0 +1,207 @@
|
||||
using System.Linq;
|
||||
using Content.Client.Parallax;
|
||||
using Content.Shared.Weather;
|
||||
using OpenToolkit.Graphics.ES11;
|
||||
using Robust.Client.GameObjects;
|
||||
using Robust.Client.Graphics;
|
||||
using Robust.Client.ResourceManagement;
|
||||
using Robust.Client.Utility;
|
||||
using Robust.Shared.Enums;
|
||||
using Robust.Shared.Map;
|
||||
using Robust.Shared.Physics.Components;
|
||||
using Robust.Shared.Prototypes;
|
||||
using Robust.Shared.Timing;
|
||||
using Robust.Shared.Utility;
|
||||
|
||||
namespace Content.Client.Weather;
|
||||
|
||||
public sealed class WeatherOverlay : Overlay
|
||||
{
|
||||
[Dependency] private readonly IClyde _clyde = default!;
|
||||
[Dependency] private readonly IEntityManager _entManager = default!;
|
||||
[Dependency] private readonly IGameTiming _timing = default!;
|
||||
[Dependency] private readonly IMapManager _mapManager = default!;
|
||||
[Dependency] private readonly IPrototypeManager _protoManager = default!;
|
||||
[Dependency] private readonly IResourceCache _cache = default!;
|
||||
private readonly SharedTransformSystem _transform;
|
||||
private readonly SpriteSystem _sprite;
|
||||
private readonly WeatherSystem _weather;
|
||||
|
||||
public override OverlaySpace Space => OverlaySpace.WorldSpaceBelowFOV;
|
||||
|
||||
private IRenderTexture? _blep;
|
||||
|
||||
public WeatherOverlay(SharedTransformSystem transform, SpriteSystem sprite, WeatherSystem weather)
|
||||
{
|
||||
ZIndex = ParallaxSystem.ParallaxZIndex + 1;
|
||||
_transform = transform;
|
||||
_weather = weather;
|
||||
_sprite = sprite;
|
||||
IoCManager.InjectDependencies(this);
|
||||
}
|
||||
|
||||
protected override bool BeforeDraw(in OverlayDrawArgs args)
|
||||
{
|
||||
if (args.MapId == MapId.Nullspace)
|
||||
return false;
|
||||
|
||||
if (!_entManager.TryGetComponent<WeatherComponent>(_mapManager.GetMapEntityId(args.MapId), out var weather) ||
|
||||
weather.Weather == null)
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
return base.BeforeDraw(in args);
|
||||
}
|
||||
|
||||
protected override void Draw(in OverlayDrawArgs args)
|
||||
{
|
||||
var mapUid = _mapManager.GetMapEntityId(args.MapId);
|
||||
|
||||
if (!_entManager.TryGetComponent<WeatherComponent>(mapUid, out var weather) ||
|
||||
weather.Weather == null ||
|
||||
!_protoManager.TryIndex<WeatherPrototype>(weather.Weather, out var weatherProto))
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
var alpha = _weather.GetPercent(weather, mapUid, weatherProto);
|
||||
DrawWorld(args, weatherProto, alpha);
|
||||
}
|
||||
|
||||
private void DrawWorld(in OverlayDrawArgs args, WeatherPrototype weatherProto, float alpha)
|
||||
{
|
||||
var worldHandle = args.WorldHandle;
|
||||
var mapId = args.MapId;
|
||||
var worldAABB = args.WorldAABB;
|
||||
var worldBounds = args.WorldBounds;
|
||||
var invMatrix = args.Viewport.GetWorldToLocalMatrix();
|
||||
var position = args.Viewport.Eye?.Position.Position ?? Vector2.Zero;
|
||||
|
||||
if (_blep?.Texture.Size != args.Viewport.Size)
|
||||
{
|
||||
_blep?.Dispose();
|
||||
_blep = _clyde.CreateRenderTarget(args.Viewport.Size, new RenderTargetFormatParameters(RenderTargetColorFormat.Rgba8Srgb), name: "weather-stencil");
|
||||
}
|
||||
|
||||
// Cut out the irrelevant bits via stencil
|
||||
// This is why we don't just use parallax; we might want specific tiles to get drawn over
|
||||
// particularly for planet maps or stations.
|
||||
worldHandle.RenderInRenderTarget(_blep, () =>
|
||||
{
|
||||
var bodyQuery = _entManager.GetEntityQuery<PhysicsComponent>();
|
||||
var xformQuery = _entManager.GetEntityQuery<TransformComponent>();
|
||||
|
||||
foreach (var grid in _mapManager.FindGridsIntersecting(mapId, worldAABB))
|
||||
{
|
||||
var matrix = _transform.GetWorldMatrix(grid.Owner, xformQuery);
|
||||
Matrix3.Multiply(in matrix, in invMatrix, out var matty);
|
||||
worldHandle.SetTransform(matty);
|
||||
|
||||
foreach (var tile in grid.GetTilesIntersecting(worldAABB))
|
||||
{
|
||||
// Ignored tiles for stencil
|
||||
if (_weather.CanWeatherAffect(grid, tile, bodyQuery))
|
||||
{
|
||||
continue;
|
||||
}
|
||||
|
||||
var gridTile = new Box2(tile.GridIndices * grid.TileSize,
|
||||
(tile.GridIndices + Vector2i.One) * grid.TileSize);
|
||||
|
||||
worldHandle.DrawRect(gridTile, Color.White);
|
||||
}
|
||||
}
|
||||
|
||||
}, Color.Transparent);
|
||||
|
||||
worldHandle.SetTransform(Matrix3.Identity);
|
||||
worldHandle.UseShader(_protoManager.Index<ShaderPrototype>("StencilMask").Instance());
|
||||
worldHandle.DrawTextureRect(_blep.Texture, worldBounds);
|
||||
Texture? sprite = null;
|
||||
var curTime = _timing.RealTime;
|
||||
|
||||
switch (weatherProto.Sprite)
|
||||
{
|
||||
case SpriteSpecifier.Rsi rsi:
|
||||
var rsiActual = _cache.GetResource<RSIResource>(rsi.RsiPath).RSI;
|
||||
rsiActual.TryGetState(rsi.RsiState, out var state);
|
||||
var frames = state!.GetFrames(RSI.State.Direction.South);
|
||||
var delays = state.GetDelays();
|
||||
var totalDelay = delays.Sum();
|
||||
var time = curTime.TotalSeconds % totalDelay;
|
||||
var delaySum = 0f;
|
||||
|
||||
for (var i = 0; i < delays.Length; i++)
|
||||
{
|
||||
var delay = delays[i];
|
||||
delaySum += delay;
|
||||
|
||||
if (time > delaySum)
|
||||
continue;
|
||||
|
||||
sprite = frames[i];
|
||||
break;
|
||||
}
|
||||
|
||||
sprite ??= _sprite.Frame0(weatherProto.Sprite);
|
||||
break;
|
||||
case SpriteSpecifier.Texture texture:
|
||||
sprite = texture.GetTexture(_cache);
|
||||
break;
|
||||
default:
|
||||
throw new NotImplementedException();
|
||||
}
|
||||
|
||||
// Draw the rain
|
||||
worldHandle.UseShader(_protoManager.Index<ShaderPrototype>("StencilDraw").Instance());
|
||||
|
||||
// TODO: This is very similar to parallax but we need stencil support but we can probably combine these somehow
|
||||
// and not make it spaghetti, while getting the advantages of not-duped code?
|
||||
|
||||
|
||||
// Okay I have spent like 5 hours on this at this point and afaict you have one of the following comprises:
|
||||
// - No scrolling so the weather is always centered on the player
|
||||
// - Crappy looking rotation but strafing looks okay and scrolls
|
||||
// - Crappy looking strafing but rotation looks okay.
|
||||
// - No rotation
|
||||
// - Storing state across frames to do scrolling and just having it always do topdown.
|
||||
|
||||
// I have chosen no rotation.
|
||||
|
||||
const float scale = 1f;
|
||||
const float slowness = 0f;
|
||||
var scrolling = Vector2.Zero;
|
||||
|
||||
// Size of the texture in world units.
|
||||
var size = (sprite.Size / (float) EyeManager.PixelsPerMeter) * scale;
|
||||
var scrolled = scrolling * (float) curTime.TotalSeconds;
|
||||
|
||||
// Origin - start with the parallax shift itself.
|
||||
var originBL = position * slowness + scrolled;
|
||||
|
||||
// Centre the image.
|
||||
originBL -= size / 2;
|
||||
|
||||
// Remove offset so we can floor.
|
||||
var flooredBL = args.WorldAABB.BottomLeft - originBL;
|
||||
|
||||
// Floor to background size.
|
||||
flooredBL = (flooredBL / size).Floored() * size;
|
||||
|
||||
// Re-offset.
|
||||
flooredBL += originBL;
|
||||
|
||||
for (var x = flooredBL.X; x < args.WorldAABB.Right; x += size.X)
|
||||
{
|
||||
for (var y = flooredBL.Y; y < args.WorldAABB.Top; y += size.Y)
|
||||
{
|
||||
var box = Box2.FromDimensions((x, y), size);
|
||||
worldHandle.DrawTextureRect(sprite, box, (weatherProto.Color ?? Color.White).WithAlpha(alpha));
|
||||
}
|
||||
}
|
||||
|
||||
worldHandle.SetTransform(Matrix3.Identity);
|
||||
worldHandle.UseShader(null);
|
||||
}
|
||||
}
|
||||
219
Content.Client/Weather/WeatherSystem.cs
Normal file
219
Content.Client/Weather/WeatherSystem.cs
Normal file
@@ -0,0 +1,219 @@
|
||||
using Content.Shared.Weather;
|
||||
using Robust.Client.GameObjects;
|
||||
using Robust.Client.Graphics;
|
||||
using Robust.Client.Player;
|
||||
using Robust.Shared.GameStates;
|
||||
using Robust.Shared.Map;
|
||||
using Robust.Shared.Map.Components;
|
||||
using Robust.Shared.Physics;
|
||||
using Robust.Shared.Physics.Components;
|
||||
using Robust.Shared.Physics.Systems;
|
||||
using Robust.Shared.Player;
|
||||
|
||||
namespace Content.Client.Weather;
|
||||
|
||||
public sealed class WeatherSystem : SharedWeatherSystem
|
||||
{
|
||||
[Dependency] private readonly IOverlayManager _overlayManager = default!;
|
||||
[Dependency] private readonly IPlayerManager _playerManager = default!;
|
||||
[Dependency] private readonly AudioSystem _audio = default!;
|
||||
[Dependency] private readonly MetaDataSystem _metadata = default!;
|
||||
[Dependency] private readonly SharedPhysicsSystem _physics = default!;
|
||||
[Dependency] private readonly SharedTransformSystem _transform = default!;
|
||||
|
||||
// Consistency isn't really important, just want to avoid sharp changes and there's no way to lerp on engine nicely atm.
|
||||
private float _lastAlpha;
|
||||
private float _lastOcclusion;
|
||||
|
||||
private const float OcclusionLerpRate = 4f;
|
||||
private const float AlphaLerpRate = 4f;
|
||||
|
||||
public override void Initialize()
|
||||
{
|
||||
base.Initialize();
|
||||
_overlayManager.AddOverlay(new WeatherOverlay(_transform, EntityManager.System<SpriteSystem>(), this));
|
||||
SubscribeLocalEvent<WeatherComponent, ComponentHandleState>(OnWeatherHandleState);
|
||||
}
|
||||
|
||||
public override void Shutdown()
|
||||
{
|
||||
base.Shutdown();
|
||||
_overlayManager.RemoveOverlay<WeatherOverlay>();
|
||||
}
|
||||
|
||||
protected override void Run(EntityUid uid, WeatherComponent component, WeatherPrototype weather, WeatherState state, float frameTime)
|
||||
{
|
||||
base.Run(uid, component, weather, state, frameTime);
|
||||
|
||||
var ent = _playerManager.LocalPlayer?.ControlledEntity;
|
||||
|
||||
if (ent == null)
|
||||
return;
|
||||
|
||||
var mapUid = Transform(uid).MapUid;
|
||||
var entXform = Transform(ent.Value);
|
||||
|
||||
// Maybe have the viewports manage this?
|
||||
if (mapUid == null || entXform.MapUid != mapUid)
|
||||
{
|
||||
_lastOcclusion = 0f;
|
||||
_lastAlpha = 0f;
|
||||
component.Stream?.Stop();
|
||||
component.Stream = null;
|
||||
return;
|
||||
}
|
||||
|
||||
if (!Timing.IsFirstTimePredicted || weather.Sound == null)
|
||||
return;
|
||||
|
||||
component.Stream ??= _audio.PlayGlobal(weather.Sound, Filter.Local(), true);
|
||||
var volumeMod = MathF.Pow(10, weather.Sound.Params.Volume / 10f);
|
||||
|
||||
var stream = (AudioSystem.PlayingStream) component.Stream!;
|
||||
var alpha = GetPercent(component, mapUid.Value, weather);
|
||||
alpha = MathF.Pow(alpha, 2f) * volumeMod;
|
||||
// TODO: Lerp this occlusion.
|
||||
var occlusion = 0f;
|
||||
// TODO: Fade-out needs to be slower
|
||||
// TODO: HELPER PLZ
|
||||
|
||||
// Work out tiles nearby to determine volume.
|
||||
if (TryComp<MapGridComponent>(entXform.GridUid, out var grid))
|
||||
{
|
||||
// Floodfill to the nearest tile and use that for audio.
|
||||
var seed = grid.GetTileRef(entXform.Coordinates);
|
||||
var frontier = new Queue<TileRef>();
|
||||
frontier.Enqueue(seed);
|
||||
// If we don't have a nearest node don't play any sound.
|
||||
EntityCoordinates? nearestNode = null;
|
||||
var bodyQuery = GetEntityQuery<PhysicsComponent>();
|
||||
var visited = new HashSet<Vector2i>();
|
||||
|
||||
while (frontier.TryDequeue(out var node))
|
||||
{
|
||||
if (!visited.Add(node.GridIndices))
|
||||
continue;
|
||||
|
||||
if (!CanWeatherAffect(grid, node, bodyQuery))
|
||||
{
|
||||
// Add neighbors
|
||||
// TODO: Ideally we pick some deterministically random direction and use that
|
||||
// We can't just do that naively here because it will flicker between nearby tiles.
|
||||
for (var x = -1; x <= 1; x++)
|
||||
{
|
||||
for (var y = -1; y <= 1; y++)
|
||||
{
|
||||
if (Math.Abs(x) == 1 && Math.Abs(y) == 1 ||
|
||||
x == 0 && y == 0 ||
|
||||
(new Vector2(x, y) + node.GridIndices - seed.GridIndices).Length > 3)
|
||||
{
|
||||
continue;
|
||||
}
|
||||
|
||||
frontier.Enqueue(grid.GetTileRef(new Vector2i(x, y) + node.GridIndices));
|
||||
}
|
||||
}
|
||||
|
||||
continue;
|
||||
}
|
||||
|
||||
nearestNode = new EntityCoordinates(entXform.GridUid.Value,
|
||||
(Vector2) node.GridIndices + (grid.TileSize / 2f));
|
||||
break;
|
||||
}
|
||||
|
||||
if (nearestNode == null)
|
||||
alpha = 0f;
|
||||
else
|
||||
{
|
||||
var entPos = _transform.GetWorldPosition(entXform);
|
||||
var sourceRelative = nearestNode.Value.ToMap(EntityManager).Position - entPos;
|
||||
|
||||
if (sourceRelative.LengthSquared > 1f)
|
||||
{
|
||||
occlusion = _physics.IntersectRayPenetration(entXform.MapID,
|
||||
new CollisionRay(entPos, sourceRelative.Normalized, _audio.OcclusionCollisionMask),
|
||||
sourceRelative.Length, stream.TrackingEntity);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (MathHelper.CloseTo(_lastOcclusion, occlusion, 0.01f))
|
||||
_lastOcclusion = occlusion;
|
||||
else
|
||||
_lastOcclusion += (occlusion - _lastOcclusion) * OcclusionLerpRate * frameTime;
|
||||
|
||||
if (MathHelper.CloseTo(_lastAlpha, alpha, 0.01f))
|
||||
_lastAlpha = alpha;
|
||||
else
|
||||
_lastAlpha += (alpha - _lastAlpha) * AlphaLerpRate * frameTime;
|
||||
|
||||
// Full volume if not on grid
|
||||
Sawmill.Debug($"Setting alpha to {alpha:0.000}");
|
||||
stream.Source.SetVolumeDirect(_lastAlpha);
|
||||
stream.Source.SetOcclusion(_lastOcclusion);
|
||||
}
|
||||
|
||||
public float GetPercent(WeatherComponent component, EntityUid mapUid, WeatherPrototype weatherProto)
|
||||
{
|
||||
var pauseTime = _metadata.GetPauseTime(mapUid);
|
||||
var elapsed = Timing.CurTime - (component.StartTime + pauseTime);
|
||||
var duration = component.Duration;
|
||||
var remaining = duration - elapsed;
|
||||
float alpha;
|
||||
|
||||
if (elapsed < weatherProto.StartupTime)
|
||||
{
|
||||
alpha = (float) (elapsed / weatherProto.StartupTime);
|
||||
}
|
||||
else if (remaining < weatherProto.ShutdownTime)
|
||||
{
|
||||
alpha = (float) (remaining / weatherProto.ShutdownTime);
|
||||
}
|
||||
else
|
||||
{
|
||||
alpha = 1f;
|
||||
}
|
||||
|
||||
return alpha;
|
||||
}
|
||||
|
||||
protected override bool SetState(EntityUid uid, WeatherComponent component, WeatherState state, WeatherPrototype prototype)
|
||||
{
|
||||
if (!base.SetState(uid, component, state, prototype))
|
||||
return false;
|
||||
|
||||
if (!Timing.IsFirstTimePredicted)
|
||||
return true;
|
||||
|
||||
// TODO: Fades
|
||||
component.Stream?.Stop();
|
||||
component.Stream = null;
|
||||
component.Stream = _audio.PlayGlobal(prototype.Sound, Filter.Local(), true);
|
||||
return true;
|
||||
}
|
||||
|
||||
protected override void EndWeather(WeatherComponent component)
|
||||
{
|
||||
_lastOcclusion = 0f;
|
||||
_lastAlpha = 0f;
|
||||
base.EndWeather(component);
|
||||
}
|
||||
|
||||
private void OnWeatherHandleState(EntityUid uid, WeatherComponent component, ref ComponentHandleState args)
|
||||
{
|
||||
if (args.Current is not WeatherComponentState state)
|
||||
return;
|
||||
|
||||
if (component.Weather != state.Weather || !component.EndTime.Equals(state.EndTime) || !component.StartTime.Equals(state.StartTime))
|
||||
{
|
||||
EndWeather(component);
|
||||
|
||||
if (state.Weather != null)
|
||||
StartWeather(component, ProtoMan.Index<WeatherPrototype>(state.Weather));
|
||||
}
|
||||
|
||||
component.EndTime = state.EndTime;
|
||||
component.StartTime = state.StartTime;
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user