SingularityOverlay.cs 6.9 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146
  1. using Content.Shared.Singularity.Components;
  2. using Robust.Client.Graphics;
  3. using Robust.Client.UserInterface.CustomControls;
  4. using Robust.Shared.Enums;
  5. using Robust.Shared.Prototypes;
  6. using System.Numerics;
  7. namespace Content.Client.Singularity
  8. {
  9. public sealed class SingularityOverlay : Overlay, IEntityEventSubscriber
  10. {
  11. [Dependency] private readonly IEntityManager _entMan = default!;
  12. [Dependency] private readonly IPrototypeManager _prototypeManager = default!;
  13. private SharedTransformSystem? _xformSystem = null;
  14. /// <summary>
  15. /// Maximum number of distortions that can be shown on screen at a time.
  16. /// If this value is changed, the shader itself also needs to be updated.
  17. /// </summary>
  18. public const int MaxCount = 5;
  19. private const float MaxDistance = 20f;
  20. public override OverlaySpace Space => OverlaySpace.WorldSpace;
  21. public override bool RequestScreenTexture => true;
  22. private readonly ShaderInstance _shader;
  23. public SingularityOverlay()
  24. {
  25. IoCManager.InjectDependencies(this);
  26. _shader = _prototypeManager.Index<ShaderPrototype>("Singularity").Instance().Duplicate();
  27. _shader.SetParameter("maxDistance", MaxDistance * EyeManager.PixelsPerMeter);
  28. _entMan.EventBus.SubscribeEvent<PixelToMapEvent>(EventSource.Local, this, OnProjectFromScreenToMap);
  29. ZIndex = 101; // Should be drawn after the placement overlay so admins placing items near the singularity can tell where they're going.
  30. }
  31. private readonly Vector2[] _positions = new Vector2[MaxCount];
  32. private readonly float[] _intensities = new float[MaxCount];
  33. private readonly float[] _falloffPowers = new float[MaxCount];
  34. private int _count = 0;
  35. protected override bool BeforeDraw(in OverlayDrawArgs args)
  36. {
  37. if (args.Viewport.Eye == null)
  38. return false;
  39. if (_xformSystem is null && !_entMan.TrySystem(out _xformSystem))
  40. return false;
  41. _count = 0;
  42. var query = _entMan.EntityQueryEnumerator<SingularityDistortionComponent, TransformComponent>();
  43. while (query.MoveNext(out var uid, out var distortion, out var xform))
  44. {
  45. if (xform.MapID != args.MapId)
  46. continue;
  47. var mapPos = _xformSystem.GetWorldPosition(uid);
  48. // is the distortion in range?
  49. if ((mapPos - args.WorldAABB.ClosestPoint(mapPos)).LengthSquared() > MaxDistance * MaxDistance)
  50. continue;
  51. // To be clear, this needs to use "inside-viewport" pixels.
  52. // In other words, specifically NOT IViewportControl.WorldToScreen (which uses outer coordinates).
  53. var tempCoords = args.Viewport.WorldToLocal(mapPos);
  54. tempCoords.Y = args.Viewport.Size.Y - tempCoords.Y; // Local space to fragment space.
  55. _positions[_count] = tempCoords;
  56. _intensities[_count] = distortion.Intensity;
  57. _falloffPowers[_count] = distortion.FalloffPower;
  58. _count++;
  59. if (_count == MaxCount)
  60. break;
  61. }
  62. return (_count > 0);
  63. }
  64. protected override void Draw(in OverlayDrawArgs args)
  65. {
  66. if (ScreenTexture == null || args.Viewport.Eye == null)
  67. return;
  68. _shader?.SetParameter("renderScale", args.Viewport.RenderScale * args.Viewport.Eye.Scale);
  69. _shader?.SetParameter("count", _count);
  70. _shader?.SetParameter("position", _positions);
  71. _shader?.SetParameter("intensity", _intensities);
  72. _shader?.SetParameter("falloffPower", _falloffPowers);
  73. _shader?.SetParameter("SCREEN_TEXTURE", ScreenTexture);
  74. var worldHandle = args.WorldHandle;
  75. worldHandle.UseShader(_shader);
  76. worldHandle.DrawRect(args.WorldAABB, Color.White);
  77. worldHandle.UseShader(null);
  78. }
  79. /// <summary>
  80. /// Repeats the transformation applied by the shader in <see cref="Resources/Textures/Shaders/singularity.swsl"/>
  81. /// </summary>
  82. private void OnProjectFromScreenToMap(ref PixelToMapEvent args)
  83. { // Mostly copypasta from the singularity shader.
  84. if (args.Viewport.Eye == null)
  85. return;
  86. var maxDistance = MaxDistance * EyeManager.PixelsPerMeter;
  87. var finalCoords = args.VisiblePosition;
  88. for (var i = 0; i < MaxCount && i < _count; i++)
  89. {
  90. // An explanation of pain:
  91. // The shader used by the singularity to create the neat distortion effect occurs in _fragment space_
  92. // All of these calculations are done in _local space_.
  93. // The only difference between the two is that in fragment space 'Y' is measured in pixels from the bottom of the viewport...
  94. // and in local space 'Y' is measured in pixels from the top of the viewport.
  95. // As a minor optimization the locations of the singularities are transformed into fragment space in BeforeDraw so the shader doesn't need to.
  96. // We need to undo that here or this will transform the cursor position as if the singularities were mirrored vertically relative to the center of the viewport.
  97. var localPosition = _positions[i];
  98. localPosition.Y = args.Viewport.Size.Y - localPosition.Y;
  99. var delta = args.VisiblePosition - localPosition;
  100. var distance = (delta / (args.Viewport.RenderScale * args.Viewport.Eye.Scale)).Length();
  101. var deformation = _intensities[i] / MathF.Pow(distance, _falloffPowers[i]);
  102. // ensure deformation goes to zero at max distance
  103. // avoids long-range single-pixel shifts that are noticeable when leaving PVS.
  104. if (distance >= maxDistance)
  105. deformation = 0.0f;
  106. else
  107. deformation *= 1.0f - MathF.Pow(distance / maxDistance, 4.0f);
  108. if (deformation > 0.8)
  109. deformation = MathF.Pow(deformation, 0.3f);
  110. finalCoords -= delta * deformation;
  111. }
  112. finalCoords.X -= MathF.Floor(finalCoords.X / (args.Viewport.Size.X * 2)) * args.Viewport.Size.X * 2; // Manually handle the wrapping reflection behaviour used by the viewport texture.
  113. finalCoords.Y -= MathF.Floor(finalCoords.Y / (args.Viewport.Size.Y * 2)) * args.Viewport.Size.Y * 2;
  114. finalCoords.X = (finalCoords.X >= args.Viewport.Size.X) ? ((args.Viewport.Size.X * 2) - finalCoords.X) : finalCoords.X;
  115. finalCoords.Y = (finalCoords.Y >= args.Viewport.Size.Y) ? ((args.Viewport.Size.Y * 2) - finalCoords.Y) : finalCoords.Y;
  116. args.VisiblePosition = finalCoords;
  117. }
  118. }
  119. }