Events/RadiationStorm: Fancy radiation shader & SFX (#5612)

This commit is contained in:
E F R
2021-12-01 20:21:17 +00:00
committed by GitHub
parent dff78f239d
commit 9216d279af
20 changed files with 240 additions and 116 deletions

View File

@@ -12,10 +12,12 @@ namespace Content.Client.StationEvents
private bool _decay;
private float _radsPerSecond;
private float _range;
private TimeSpan _startTime;
private TimeSpan _endTime;
public override float RadsPerSecond => _radsPerSecond;
public override float Range => _range;
public override TimeSpan StartTime => _startTime;
public override TimeSpan EndTime => _endTime;
public override bool Draw => _draw;
public override bool Decay => _decay;
@@ -33,6 +35,7 @@ namespace Content.Client.StationEvents
_range = state.Range;
_draw = state.Draw;
_decay = state.Decay;
_startTime = state.StartTime;
_endTime = state.EndTime;
}
}

View File

@@ -1,152 +1,138 @@
using System;
using System.Collections.Generic;
using System.Linq;
using JetBrains.Annotations;
using Robust.Client.Graphics;
using Robust.Client.Player;
using Robust.Shared.Enums;
using Robust.Shared.GameObjects;
using Robust.Shared.Timing;
using Robust.Shared.IoC;
using Robust.Shared.Map;
using Robust.Shared.Maths;
using Robust.Shared.Timing;
using Robust.Shared.Prototypes;
namespace Content.Client.StationEvents
{
[UsedImplicitly]
public sealed class RadiationPulseOverlay : Overlay
public class RadiationPulseOverlay : Overlay
{
[Dependency] private readonly IEntityManager _entityManager = default!;
[Dependency] private readonly IPrototypeManager _prototypeManager = default!;
[Dependency] private readonly IGameTiming _gameTiming = default!;
[Dependency] private readonly IMapManager _mapManager = default!;
[Dependency] private readonly IPlayerManager _playerManager = default!;
[Dependency] private readonly IEyeManager _eyeManager = default!;
/// <summary>
/// Current color of a pulse
/// </summary>
private readonly Dictionary<IEntity, Color> _colors = new();
/// <summary>
/// Whether our alpha is increasing or decreasing and at what time does it flip (or stop)
/// </summary>
private readonly Dictionary<IEntity, (bool EasingIn, TimeSpan TransitionTime)> _transitions =
new();
/// <summary>
/// How much the alpha changes per second for each pulse
/// </summary>
private readonly Dictionary<IEntity, float> _alphaRateOfChange = new();
private TimeSpan _lastTick;
private const float MaxDist = 15.0f;
public override OverlaySpace Space => OverlaySpace.WorldSpace;
public override bool RequestScreenTexture => true;
private TimeSpan _lastTick = default;
private readonly ShaderInstance _baseShader;
private readonly Dictionary<EntityUid, (ShaderInstance shd, RadiationShaderInstance instance)> _pulses = new();
public RadiationPulseOverlay()
{
IoCManager.InjectDependencies(this);
_lastTick = _gameTiming.CurTime;
}
/// <summary>
/// Get the current color for the entity,
/// accounting for what its alpha should be and whether it should be transitioning in or out
/// </summary>
/// <param name="entity"></param>
/// <param name="elapsedTime">frametime</param>
/// <param name="endTime"></param>
/// <returns></returns>
private Color GetColor(IEntity entity, float elapsedTime, TimeSpan endTime)
{
var currentTime = _gameTiming.CurTime;
// New pulse
if (!_colors.ContainsKey(entity))
{
UpdateTransition(entity, currentTime, endTime);
}
var currentColor = _colors[entity];
var alphaChange = _alphaRateOfChange[entity] * elapsedTime;
if (!_transitions[entity].EasingIn)
{
alphaChange *= -1;
}
if (currentTime > _transitions[entity].TransitionTime)
{
UpdateTransition(entity, currentTime, endTime);
}
_colors[entity] = _colors[entity].WithAlpha(currentColor.A + alphaChange);
return _colors[entity];
}
private void UpdateTransition(IEntity entity, TimeSpan currentTime, TimeSpan endTime)
{
bool easingIn;
TimeSpan transitionTime;
if (!_transitions.TryGetValue(entity, out var transition))
{
// Start as false because it will immediately be flipped
easingIn = false;
transitionTime = (endTime - currentTime) / 2 + currentTime;
}
else
{
easingIn = transition.EasingIn;
transitionTime = endTime;
}
_transitions[entity] = (!easingIn, transitionTime);
_colors[entity] = Color.LimeGreen.WithAlpha(0.0f);
_alphaRateOfChange[entity] = 1.0f / (float) (transitionTime - currentTime).TotalSeconds;
_baseShader = _prototypeManager.Index<ShaderPrototype>("Radiation").Instance().Duplicate();
}
protected override void Draw(in OverlayDrawArgs args)
{
// PVS should control the overlay pretty well so the overlay doesn't get instantiated unless we're near one...
var playerEntity = _playerManager.LocalPlayer?.ControlledEntity;
RadiationQuery(args.Viewport.Eye);
if (playerEntity == null)
{
if (_pulses.Count == 0)
return;
}
var radiationPulses = _entityManager
.EntityQuery<RadiationPulseComponent>(true)
.ToList();
if (radiationPulses.Count == 0)
{
if (ScreenTexture == null)
return;
}
var elapsedTime = (float) (_gameTiming.CurTime - _lastTick).TotalSeconds;
_lastTick = _gameTiming.CurTime;
var worldHandle = args.WorldHandle;
var viewport = _eyeManager.GetWorldViewport();
foreach (var grid in _mapManager.FindGridsIntersecting(playerEntity.Transform.MapID, viewport))
var viewport = args.Viewport;
foreach ((var shd, var instance) in _pulses.Values)
{
worldHandle.SetTransform(grid.WorldMatrix);
foreach (var pulse in radiationPulses)
{
if (!pulse.Draw || grid.Index != pulse.Owner.Transform.GridID) continue;
// To be clear, this needs to use "inside-viewport" pixels.
// In other words, specifically NOT IViewportControl.WorldToScreen (which uses outer coordinates).
var tempCoords = viewport.WorldToLocal(instance.CurrentMapCoords);
tempCoords.Y = viewport.Size.Y - tempCoords.Y;
shd?.SetParameter("renderScale", viewport.RenderScale);
shd?.SetParameter("positionInput", tempCoords);
shd?.SetParameter("range", instance.Range);
var life = (_lastTick - instance.Start) / (instance.End - instance.Start);
shd?.SetParameter("life", (float) life);
var pulseTransform = pulse.Owner.Transform;
// There's probably a very good reason not to do this.
// Oh well!
shd?.SetParameter("SCREEN_TEXTURE", viewport.RenderTarget.Texture);
var maxVisibleCircleArea = viewport.Enlarged(pulse.Range * 64);
if (!maxVisibleCircleArea.Contains(pulseTransform.WorldPosition)) continue;
worldHandle.DrawCircle(
pulseTransform.LocalPosition,
pulse.Range,
GetColor(pulse.Owner, pulse.Decay ? elapsedTime : 0, pulse.EndTime));
}
worldHandle.UseShader(shd);
worldHandle.DrawRect(Box2.CenteredAround(instance.CurrentMapCoords, new Vector2(instance.Range, instance.Range) * 2f), Color.White);
}
}
//Queries all pulses on the map and either adds or removes them from the list of rendered pulses based on whether they should be drawn (in range? on the same z-level/map? pulse entity still exists?)
private void RadiationQuery(IEye? currentEye)
{
if (currentEye == null)
{
_pulses.Clear();
return;
}
_lastTick = _gameTiming.CurTime;
var currentEyeLoc = currentEye.Position;
var pulses = _entityManager.EntityQuery<RadiationPulseComponent>();
foreach (var pulse in pulses) //Add all pulses that are not added yet but qualify
{
var pulseEntity = pulse.Owner;
if (!_pulses.Keys.Contains(pulseEntity.Uid) && PulseQualifies(pulseEntity, currentEyeLoc))
{
_pulses.Add(
pulseEntity.Uid,
(
_baseShader.Duplicate(),
new RadiationShaderInstance(
pulseEntity.Transform.MapPosition.Position,
pulse.Range,
pulse.StartTime,
pulse.EndTime
)
)
);
}
}
var activeShaderIds = _pulses.Keys;
foreach (var activePulseUid in activeShaderIds) //Remove all pulses that are added and no longer qualify
{
if (_entityManager.TryGetEntity(activePulseUid, out var pulseEntity) &&
PulseQualifies(pulseEntity, currentEyeLoc) &&
pulseEntity.TryGetComponent<RadiationPulseComponent>(out var pulse))
{
var shaderInstance = _pulses[activePulseUid];
shaderInstance.instance.CurrentMapCoords = pulseEntity.Transform.MapPosition.Position;
shaderInstance.instance.Range = pulse.Range;
} else {
_pulses[activePulseUid].shd.Dispose();
_pulses.Remove(activePulseUid);
}
}
}
private bool PulseQualifies(IEntity pulseEntity, MapCoordinates currentEyeLoc)
{
return pulseEntity.Transform.MapID == currentEyeLoc.MapId && pulseEntity.Transform.Coordinates.InRange(_entityManager, EntityCoordinates.FromMap(_entityManager, pulseEntity.Transform.ParentUid, currentEyeLoc), MaxDist);
}
private sealed record RadiationShaderInstance(Vector2 CurrentMapCoords, float Range, TimeSpan Start, TimeSpan End)
{
public Vector2 CurrentMapCoords = CurrentMapCoords;
public float Range = Range;
public TimeSpan Start = Start;
public TimeSpan End = End;
};
}
}

View File

@@ -22,6 +22,7 @@ namespace Content.Server.Radiation
private float _duration;
private float _radsPerSecond = 8f;
private float _range = 5f;
private TimeSpan _startTime;
private TimeSpan _endTime;
private bool _draw = true;
private bool _decay = true;
@@ -58,7 +59,7 @@ namespace Content.Server.Radiation
}
}
[DataField("sound")] public SoundSpecifier Sound { get; set; } = new SoundPathSpecifier("/Audio/Weapons/Guns/Gunshots/laser3.ogg");
[DataField("sound")] public SoundSpecifier Sound { get; set; } = new SoundCollectionSpecifier("RadiationPulse");
[DataField("range")]
public override float Range
@@ -82,6 +83,7 @@ namespace Content.Server.Radiation
}
}
public override TimeSpan StartTime => _startTime;
public override TimeSpan EndTime => _endTime;
public void DoPulse()
@@ -89,6 +91,7 @@ namespace Content.Server.Radiation
if (Decay)
{
var currentTime = _gameTiming.CurTime;
_startTime = currentTime;
_duration = _random.NextFloat() * (MaxPulseLifespan - MinPulseLifespan) + MinPulseLifespan;
_endTime = currentTime + TimeSpan.FromSeconds(_duration);
}
@@ -100,7 +103,7 @@ namespace Content.Server.Radiation
public override ComponentState GetComponentState()
{
return new RadiationPulseState(_radsPerSecond, _range, Draw, Decay, _endTime);
return new RadiationPulseState(_radsPerSecond, _range, Draw, Decay, _startTime, _endTime);
}
public void Update(float frameTime)

View File

@@ -92,6 +92,13 @@ namespace Content.Server.StationEvents.Events
ResetTimeUntilPulse();
}
public static void SpawnPulseAt(EntityCoordinates at)
{
var pulse = IoCManager.Resolve<IEntityManager>()
.SpawnEntity("RadiationPulse", at);
pulse.GetComponent<RadiationPulseComponent>().DoPulse();
}
private bool TryFindRandomGrid(IMapGrid mapGrid, out EntityCoordinates coordinates)
{
if (!mapGrid.Index.IsValid())

View File

@@ -20,6 +20,7 @@ namespace Content.Shared.Radiation
public virtual bool Decay { get; set; }
public virtual bool Draw { get; set; }
public virtual TimeSpan StartTime { get; }
public virtual TimeSpan EndTime { get; }
}
@@ -33,14 +34,16 @@ namespace Content.Shared.Radiation
public readonly float Range;
public readonly bool Draw;
public readonly bool Decay;
public readonly TimeSpan StartTime;
public readonly TimeSpan EndTime;
public RadiationPulseState(float radsPerSecond, float range, bool draw, bool decay, TimeSpan endTime)
public RadiationPulseState(float radsPerSecond, float range, bool draw, bool decay, TimeSpan startTime, TimeSpan endTime)
{
RadsPerSecond = radsPerSecond;
Range = range;
Draw = draw;
Decay = decay;
StartTime = startTime;
EndTime = endTime;
}
}

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

View File

@@ -27,6 +27,14 @@
falloff: 5
intensity: 5
- type: shader
id: Radiation
kind: source
path: "/Textures/Shaders/radiation.swsl"
params:
positionInput: 0,0
life: 0
- type: shader
id: Texture
kind: source

View File

@@ -0,0 +1,15 @@
- type: soundCollection
id: RadiationPulse
files:
- /Audio/Effects/radpulse1.ogg
- /Audio/Effects/radpulse2.ogg
- /Audio/Effects/radpulse3.ogg
- /Audio/Effects/radpulse4.ogg
- /Audio/Effects/radpulse5.ogg
- /Audio/Effects/radpulse6.ogg
- /Audio/Effects/radpulse7.ogg
- /Audio/Effects/radpulse8.ogg
- /Audio/Effects/radpulse9.ogg
- /Audio/Effects/radpulse10.ogg
- /Audio/Effects/radpulse11.ogg
- /Audio/Effects/radpulse12.ogg

View File

@@ -0,0 +1,99 @@
// From https://godotshaders.com/snippet/2d-noise/
uniform sampler2D SCREEN_TEXTURE;
uniform highp vec2 positionInput;
uniform highp vec2 renderScale;
uniform highp float life;
uniform highp float range;
highp vec2 random(highp vec2 uv){
uv = vec2( dot(uv, vec2(127.1,311.7) ),
dot(uv, vec2(269.5,183.3) ) );
return -1.0 + 2.0 * fract(sin(uv) * 43758.5453123);
}
highp float noise(highp vec2 uv) {
highp vec2 uv_index = floor(uv);
highp vec2 uv_fract = fract(uv);
highp vec2 blur = smoothstep(0.0, 1.0, uv_fract);
return mix( mix( dot( random(uv_index + vec2(0.0,0.0) ), uv_fract - vec2(0.0,0.0) ),
dot( random(uv_index + vec2(1.0,0.0) ), uv_fract - vec2(1.0,0.0) ), blur.x),
mix( dot( random(uv_index + vec2(0.0,1.0) ), uv_fract - vec2(0.0,1.0) ),
dot( random(uv_index + vec2(1.0,1.0) ), uv_fract - vec2(1.0,1.0) ), blur.x), blur.y) * 0.5 + 0.5;
}
highp float fbm(highp vec2 uv) {
int octaves = 6;
highp float amplitude = 0.5;
highp float frequency = 3.0;
highp float value = 0.0;
for(int i = 0; i < octaves; i++) {
value += amplitude * noise(frequency * uv);
amplitude *= 0.5;
frequency *= 2.0;
}
return value;
}
void fragment() {
highp vec2 finalCoords = (FRAGCOORD.xy - positionInput) / (renderScale * 32.0);
highp float distanceToCenter = length(finalCoords);
highp float nlife = pow(sin(life * 3.141592), 0.5);
highp float on = ((range - distanceToCenter) / range);
highp float n = on;
highp vec2 fcOffset = vec2(fbm(finalCoords.xy + life / 2.0),fbm(finalCoords.yx + life / 2.0));
n *= fbm((finalCoords + fcOffset) / (nlife / (n * 1.5))) * 1.1;
n *= clamp(nlife, 0.0, 1.0);
highp float a = 0.0; // Alpha
highp float p = 0.0; // Position between L and R stops
lowp vec3 lCol = vec3(0.0); // Left stop color
lowp vec3 rCol = vec3(0.0); // Right stop color
if (n <= 0.05) {
p = 0.0;
a = 0.0;
lCol = vec3(0.0);
rCol = vec3(0.0);
} else if (n < 0.132) {
p = (n - 0.05) / (0.132 - 0.05);
a = p;
lCol = vec3(0.0);
rCol = vec3(0.098, 0.406, 0.112);
} else if (n < 0.186) {
p = (n - 0.132) / (0.186 - 0.132);
a = 1.0;
lCol = vec3(0.098, 0.406, 0.112);
rCol = vec3(0.168, 1.000, 0.288);
} else if (n < 0.388) {
p = (n - 0.186) / (0.388 - 0.186);
a = 1.0;
lCol = vec3(0.168, 1.000, 0.288);
rCol = vec3(0.583, 1.000, 0.640);
} else if (n >= 0.388) {
p = (n - 0.388) / 0.5;
a = 1.0;
lCol = vec3(0.583, 1.000, 0.640);
rCol = vec3(1.000, 1.000, 1.000);
}
p = clamp(p, 0.0, 1.0);
highp vec4 warped = zTextureSpec(SCREEN_TEXTURE, (FRAGCOORD.xy*SCREEN_PIXEL_SIZE)+clamp(on*nlife*(fcOffset/8.0), 0.0, 1.0));
// Extremely hacky way to detect FoV cones
highp float osum = warped.r + warped.g + warped.b;
highp float osr = osum > 0.1 ? 1.0 : 10 * osum;
// Apply overlay
// FYI: If you want a smoother mix, swap lCol and rCol.
warped += mix(
vec4(0.0),
vec4(mix(rCol, lCol, vec3(p)), a),
osr
);
COLOR = warped;
}