This repository has been archived by the owner on Nov 1, 2024. It is now read-only.
forked from new-frontiers-14/frontier-station-14
-
Notifications
You must be signed in to change notification settings - Fork 38
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
Add EMP Blast effect shader (new-frontiers-14#2262)
- Loading branch information
Showing
8 changed files
with
342 additions
and
2 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,145 @@ | ||
using System.Numerics; | ||
using Content.Shared._NF.Emp.Components; | ||
using Robust.Client.GameObjects; | ||
using Robust.Client.Graphics; | ||
using Robust.Shared.Enums; | ||
using Robust.Shared.Graphics; | ||
using Robust.Shared.Map; | ||
using Robust.Shared.Prototypes; | ||
using Robust.Shared.Timing; | ||
|
||
namespace Content.Client._NF.Emp.Overlays | ||
{ | ||
public sealed class EmpBlastOverlay : Overlay | ||
{ | ||
[Dependency] private readonly IEntityManager _entityManager = default!; | ||
[Dependency] private readonly IPrototypeManager _prototypeManager = default!; | ||
[Dependency] private readonly IGameTiming _gameTiming = default!; | ||
private TransformSystem? _transform; | ||
|
||
private const float PvsDist = 25.0f; | ||
|
||
public override OverlaySpace Space => OverlaySpace.WorldSpace; | ||
public override bool RequestScreenTexture => true; | ||
|
||
private readonly ShaderInstance _baseShader; | ||
private readonly Dictionary<EntityUid, (ShaderInstance shd, EmpShaderInstance instance)> _blasts = new(); | ||
|
||
public EmpBlastOverlay() | ||
{ | ||
IoCManager.InjectDependencies(this); | ||
_baseShader = _prototypeManager.Index<ShaderPrototype>("Emp").Instance().Duplicate(); | ||
} | ||
|
||
protected override bool BeforeDraw(in OverlayDrawArgs args) | ||
{ | ||
EmpQuery(args.Viewport.Eye); | ||
return _blasts.Count > 0; | ||
} | ||
|
||
protected override void Draw(in OverlayDrawArgs args) | ||
{ | ||
if (ScreenTexture == null) | ||
return; | ||
|
||
var worldHandle = args.WorldHandle; | ||
var viewport = args.Viewport; | ||
|
||
foreach ((var shd, var instance) in _blasts.Values) | ||
{ | ||
if (instance.CurrentMapCoords.MapId != args.MapId) | ||
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.Position); | ||
tempCoords.Y = viewport.Size.Y - tempCoords.Y; | ||
shd?.SetParameter("renderScale", viewport.RenderScale); | ||
shd?.SetParameter("positionInput", tempCoords); | ||
shd?.SetParameter("range", instance.Range); | ||
var life = (_gameTiming.RealTime - instance.Start).TotalSeconds / instance.Duration; | ||
shd?.SetParameter("life", (float)life); | ||
|
||
// There's probably a very good reason not to do this. | ||
// Oh well! | ||
shd?.SetParameter("SCREEN_TEXTURE", viewport.RenderTarget.Texture); | ||
|
||
worldHandle.UseShader(shd); | ||
worldHandle.DrawRect(Box2.CenteredAround(instance.CurrentMapCoords.Position, new Vector2(instance.Range, instance.Range) * 2f), Color.White); | ||
} | ||
|
||
worldHandle.UseShader(null); | ||
} | ||
|
||
//Queries all blasts on the map and either adds or removes them from the list of rendered blasts based on whether they should be drawn (in range? on the same z-level/map? blast entity still exists?) | ||
private void EmpQuery(IEye? currentEye) | ||
{ | ||
_transform ??= _entityManager.System<TransformSystem>(); | ||
|
||
if (currentEye == null) | ||
{ | ||
_blasts.Clear(); | ||
return; | ||
} | ||
|
||
var currentEyeLoc = currentEye.Position; | ||
|
||
var blasts = _entityManager.EntityQueryEnumerator<EmpBlastComponent>(); | ||
//Add all blasts that are not added yet but qualify | ||
while (blasts.MoveNext(out var blastEntity, out var blast)) | ||
{ | ||
if (!_blasts.ContainsKey(blastEntity) && BlastQualifies(blastEntity, currentEyeLoc, blast)) | ||
{ | ||
_blasts.Add( | ||
blastEntity, | ||
( | ||
_baseShader.Duplicate(), | ||
new EmpShaderInstance( | ||
_transform.GetMapCoordinates(blastEntity), | ||
blast.VisualRange, | ||
blast.StartTime, | ||
blast.VisualDuration | ||
) | ||
) | ||
); | ||
} | ||
} | ||
|
||
var activeShaderIds = _blasts.Keys; | ||
foreach (var blastEntity in activeShaderIds) //Remove all blasts that are added and no longer qualify | ||
{ | ||
if (_entityManager.EntityExists(blastEntity) && | ||
_entityManager.TryGetComponent(blastEntity, out EmpBlastComponent? blast) && | ||
BlastQualifies(blastEntity, currentEyeLoc, blast)) | ||
{ | ||
var shaderInstance = _blasts[blastEntity]; | ||
shaderInstance.instance.CurrentMapCoords = _transform.GetMapCoordinates(blastEntity); | ||
shaderInstance.instance.Range = blast.VisualRange; | ||
} | ||
else | ||
{ | ||
_blasts[blastEntity].shd.Dispose(); | ||
_blasts.Remove(blastEntity); | ||
} | ||
} | ||
|
||
} | ||
|
||
private bool BlastQualifies(EntityUid blastEntity, MapCoordinates currentEyeLoc, EmpBlastComponent blast) | ||
{ | ||
var transformComponent = _entityManager.GetComponent<TransformComponent>(blastEntity); | ||
var transformSystem = _entityManager.System<SharedTransformSystem>(); | ||
return transformComponent.MapID == currentEyeLoc.MapId | ||
&& transformSystem.InRange(transformComponent.Coordinates, transformSystem.ToCoordinates(transformComponent.ParentUid, currentEyeLoc), PvsDist + blast.VisualRange); | ||
} | ||
|
||
private sealed record EmpShaderInstance(MapCoordinates CurrentMapCoords, float Range, TimeSpan Start, float Duration) | ||
{ | ||
public MapCoordinates CurrentMapCoords = CurrentMapCoords; | ||
public float Range = Range; | ||
public TimeSpan Start = Start; | ||
public float Duration = Duration; | ||
}; | ||
} | ||
} | ||
|
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,30 @@ | ||
using Robust.Shared.GameStates; | ||
|
||
namespace Content.Shared._NF.Emp.Components; | ||
|
||
/// <summary> | ||
/// Create circle pulse animation of emp around object. | ||
/// Drawn on client after creation only once per component lifetime. | ||
/// </summary> | ||
[RegisterComponent, NetworkedComponent, AutoGenerateComponentState] | ||
public sealed partial class EmpBlastComponent : Component | ||
{ | ||
/// <summary> | ||
/// Timestamp when component was assigned to this entity. | ||
/// </summary> | ||
[AutoNetworkedField] | ||
public TimeSpan StartTime; | ||
|
||
/// <summary> | ||
/// How long will animation play in seconds. | ||
/// Can be overridden by <see cref="Robust.Shared.Spawners.TimedDespawnComponent"/>. | ||
/// </summary> | ||
[DataField, AutoNetworkedField] | ||
public float VisualDuration = 1f; | ||
|
||
/// <summary> | ||
/// The range of animation. | ||
/// </summary> | ||
[DataField, AutoNetworkedField] | ||
public float VisualRange = 5f; | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,27 @@ | ||
using Content.Shared._NF.Emp.Components; | ||
using Robust.Shared.Spawners; | ||
using Robust.Shared.Timing; | ||
|
||
namespace Content.Shared._NF.Emp.Systems; | ||
|
||
public sealed class EmpBlastSystem : EntitySystem | ||
{ | ||
[Dependency] private readonly IGameTiming _timing = default!; | ||
|
||
public override void Initialize() | ||
{ | ||
base.Initialize(); | ||
SubscribeLocalEvent<EmpBlastComponent, ComponentStartup>(OnStartup); | ||
} | ||
|
||
private void OnStartup(EntityUid uid, EmpBlastComponent component, ComponentStartup args) | ||
{ | ||
component.StartTime = _timing.RealTime; | ||
|
||
// try to get despawn time or keep default duration time | ||
if (TryComp<TimedDespawnComponent>(uid, out var despawn)) | ||
{ | ||
component.VisualDuration = despawn.Lifetime; | ||
} | ||
} | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,16 @@ | ||
- type: entity | ||
name: emp blast | ||
id: EffectEmpBlast | ||
description: Looking at this anomaly makes you feel an electric tingling all over your body. | ||
categories: [ HideSpawnMenu ] | ||
components: | ||
- type: EmpBlast | ||
- type: TimedDespawn | ||
lifetime: 1 | ||
- type: Tag | ||
tags: | ||
- HideContextMenu | ||
- type: EmitSoundOnSpawn | ||
sound: | ||
path: /Audio/Effects/Lightning/lightningbolt.ogg | ||
- type: AnimationPlayer |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,7 @@ | ||
- type: shader | ||
id: Emp | ||
kind: source | ||
path: "/Textures/_NF/Shaders/emp.swsl" | ||
params: | ||
positionInput: 0,0 | ||
life: 0 |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -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) { | ||
const 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(clamp(life, 0.0, 1.0) * 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.112, 0.406); | ||
} else if (n < 0.186) { | ||
p = (n - 0.132) / (0.186 - 0.132); | ||
a = 1.0; | ||
lCol = vec3(0.098, 0.112, 0.406); | ||
rCol = vec3(0.168, 0.288, 1.000); | ||
} else if (n < 0.388) { | ||
p = (n - 0.186) / (0.388 - 0.186); | ||
a = 1.0; | ||
lCol = vec3(0.168, 0.288, 1.000); | ||
rCol = vec3(0.583, 0.640, 1.000); | ||
} else if (n >= 0.388) { | ||
p = (n - 0.388) / 0.5; | ||
a = 1.0; | ||
lCol = vec3(0.583, 0.640, 1.000); | ||
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.0 * 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; | ||
} |