From 3b02d461a2210077d4c59c6b66627f9c451aa837 Mon Sep 17 00:00:00 2001 From: TemporalOroboros Date: Sat, 12 Aug 2023 16:43:07 -0700 Subject: [PATCH] Corrects for singularity distortion. (#13925) --- .../CombatMode/CombatModeIndicatorsOverlay.cs | 2 +- .../Decals/Overlays/DecalPlacementOverlay.cs | 2 +- Content.Client/DragDrop/DragDropSystem.cs | 6 +- Content.Client/Gameplay/GameplayStateBase.cs | 4 +- Content.Client/Maps/GridDraggingSystem.cs | 2 +- Content.Client/NPC/PathfindingSystem.cs | 4 +- .../NodeContainer/NodeVisualizationOverlay.cs | 2 +- .../Outline/InteractionOutlineSystem.cs | 2 +- Content.Client/Outline/TargetOutlineSystem.cs | 2 +- .../Singularity/SingularityOverlay.cs | 63 +++++++++++++++++-- Content.Client/Tabletop/TabletopSystem.cs | 2 +- Content.Client/Viewport/ScalingViewport.cs | 21 ++++++- .../Weapons/Melee/MeleeArcOverlay.cs | 2 +- .../Weapons/Melee/MeleeWeaponSystem.cs | 2 +- .../Weapons/Misc/TetherGunSystem.cs | 2 +- .../Weapons/Ranged/GunSpreadOverlay.cs | 2 +- .../Weapons/Ranged/Systems/GunSystem.cs | 2 +- 17 files changed, 96 insertions(+), 26 deletions(-) diff --git a/Content.Client/CombatMode/CombatModeIndicatorsOverlay.cs b/Content.Client/CombatMode/CombatModeIndicatorsOverlay.cs index fc3de45dc9..e3a61fe3be 100644 --- a/Content.Client/CombatMode/CombatModeIndicatorsOverlay.cs +++ b/Content.Client/CombatMode/CombatModeIndicatorsOverlay.cs @@ -60,7 +60,7 @@ public sealed class CombatModeIndicatorsOverlay : Overlay protected override void Draw(in OverlayDrawArgs args) { var mouseScreenPosition = _inputManager.MouseScreenPosition; - var mousePosMap = _eye.ScreenToMap(mouseScreenPosition); + var mousePosMap = _eye.PixelToMap(mouseScreenPosition); if (mousePosMap.MapId != args.MapId) return; diff --git a/Content.Client/Decals/Overlays/DecalPlacementOverlay.cs b/Content.Client/Decals/Overlays/DecalPlacementOverlay.cs index 016a71bfc8..be277448ed 100644 --- a/Content.Client/Decals/Overlays/DecalPlacementOverlay.cs +++ b/Content.Client/Decals/Overlays/DecalPlacementOverlay.cs @@ -34,7 +34,7 @@ public sealed class DecalPlacementOverlay : Overlay return; var mouseScreenPos = _inputManager.MouseScreenPosition; - var mousePos = _eyeManager.ScreenToMap(mouseScreenPos); + var mousePos = _eyeManager.PixelToMap(mouseScreenPos); if (mousePos.MapId != args.MapId) return; diff --git a/Content.Client/DragDrop/DragDropSystem.cs b/Content.Client/DragDrop/DragDropSystem.cs index 8696f78355..7abb5bbe6b 100644 --- a/Content.Client/DragDrop/DragDropSystem.cs +++ b/Content.Client/DragDrop/DragDropSystem.cs @@ -242,7 +242,7 @@ public sealed class DragDropSystem : SharedDragDropSystem if (TryComp(_draggedEntity, out var draggedSprite)) { // pop up drag shadow under mouse - var mousePos = _eyeManager.ScreenToMap(_inputManager.MouseScreenPosition); + var mousePos = _eyeManager.PixelToMap(_inputManager.MouseScreenPosition); _dragShadow = EntityManager.SpawnEntity("dragshadow", mousePos); var dragSprite = Comp(_dragShadow.Value); dragSprite.CopyFrom(draggedSprite); @@ -405,7 +405,7 @@ public sealed class DragDropSystem : SharedDragDropSystem // find possible targets on screen even if not reachable // TODO: Duplicated in SpriteSystem and TargetOutlineSystem. Should probably be cached somewhere for a frame? - var mousePos = _eyeManager.ScreenToMap(_inputManager.MouseScreenPosition); + var mousePos = _eyeManager.PixelToMap(_inputManager.MouseScreenPosition); var expansion = new Vector2(1.5f, 1.5f); var bounds = new Box2(mousePos.Position - expansion, mousePos.Position + expansion); @@ -533,7 +533,7 @@ public sealed class DragDropSystem : SharedDragDropSystem // Update position every frame to make it smooth. if (Exists(_dragShadow)) { - var mousePos = _eyeManager.ScreenToMap(_inputManager.MouseScreenPosition); + var mousePos = _eyeManager.PixelToMap(_inputManager.MouseScreenPosition); Transform(_dragShadow.Value).WorldPosition = mousePos.Position; } } diff --git a/Content.Client/Gameplay/GameplayStateBase.cs b/Content.Client/Gameplay/GameplayStateBase.cs index f0594fe379..bd9d7b4499 100644 --- a/Content.Client/Gameplay/GameplayStateBase.cs +++ b/Content.Client/Gameplay/GameplayStateBase.cs @@ -51,7 +51,7 @@ namespace Content.Client.Gameplay EntityUid? uid = null; if (UserInterfaceManager.CurrentlyHovered is IViewportControl vp && _inputManager.MouseScreenPosition.IsValid) - uid = GetClickedEntity(vp.ScreenToMap(_inputManager.MouseScreenPosition.Position)); + uid = GetClickedEntity(vp.PixelToMap(_inputManager.MouseScreenPosition.Position)); else if (UserInterfaceManager.CurrentlyHovered is EntityMenuElement element) uid = element.Entity; @@ -166,7 +166,7 @@ namespace Content.Client.Gameplay EntityUid? entityToClick = null; if (args.Viewport is IViewportControl vp) { - var mousePosWorld = vp.ScreenToMap(kArgs.PointerLocation.Position); + var mousePosWorld = vp.PixelToMap(kArgs.PointerLocation.Position); entityToClick = GetClickedEntity(mousePosWorld); coordinates = _mapManager.TryFindGridAt(mousePosWorld, out _, out var grid) ? diff --git a/Content.Client/Maps/GridDraggingSystem.cs b/Content.Client/Maps/GridDraggingSystem.cs index 6bfd5d3e97..dcf74557ee 100644 --- a/Content.Client/Maps/GridDraggingSystem.cs +++ b/Content.Client/Maps/GridDraggingSystem.cs @@ -94,7 +94,7 @@ public sealed class GridDraggingSystem : SharedGridDraggingSystem } var mouseScreenPos = _inputManager.MouseScreenPosition; - var mousePos = _eyeManager.ScreenToMap(mouseScreenPos); + var mousePos = _eyeManager.PixelToMap(mouseScreenPos); if (_dragging == null) { diff --git a/Content.Client/NPC/PathfindingSystem.cs b/Content.Client/NPC/PathfindingSystem.cs index 9cd82fcb3b..ea50725b4f 100644 --- a/Content.Client/NPC/PathfindingSystem.cs +++ b/Content.Client/NPC/PathfindingSystem.cs @@ -173,7 +173,7 @@ namespace Content.Client.NPC private void DrawScreen(OverlayDrawArgs args, DrawingHandleScreen screenHandle) { var mousePos = _inputManager.MouseScreenPosition; - var mouseWorldPos = _eyeManager.ScreenToMap(mousePos); + var mouseWorldPos = _eyeManager.PixelToMap(mousePos); var aabb = new Box2(mouseWorldPos.Position - SharedPathfindingSystem.ChunkSizeVec, mouseWorldPos.Position + SharedPathfindingSystem.ChunkSizeVec); var xformQuery = _entManager.GetEntityQuery(); @@ -324,7 +324,7 @@ namespace Content.Client.NPC private void DrawWorld(OverlayDrawArgs args, DrawingHandleWorld worldHandle) { var mousePos = _inputManager.MouseScreenPosition; - var mouseWorldPos = _eyeManager.ScreenToMap(mousePos); + var mouseWorldPos = _eyeManager.PixelToMap(mousePos); var aabb = new Box2(mouseWorldPos.Position - Vector2.One / 4f, mouseWorldPos.Position + Vector2.One / 4f); var xformQuery = _entManager.GetEntityQuery(); diff --git a/Content.Client/NodeContainer/NodeVisualizationOverlay.cs b/Content.Client/NodeContainer/NodeVisualizationOverlay.cs index 820ba14b01..43e3c12a72 100644 --- a/Content.Client/NodeContainer/NodeVisualizationOverlay.cs +++ b/Content.Client/NodeContainer/NodeVisualizationOverlay.cs @@ -65,7 +65,7 @@ namespace Content.Client.NodeContainer var mousePos = _inputManager.MouseScreenPosition.Position; _mouseWorldPos = args .ViewportControl! - .ScreenToMap(new Vector2(mousePos.X, mousePos.Y)) + .PixelToMap(mousePos) .Position; if (_hovered == null) diff --git a/Content.Client/Outline/InteractionOutlineSystem.cs b/Content.Client/Outline/InteractionOutlineSystem.cs index 26128fef50..d48b8763f4 100644 --- a/Content.Client/Outline/InteractionOutlineSystem.cs +++ b/Content.Client/Outline/InteractionOutlineSystem.cs @@ -117,7 +117,7 @@ public sealed class InteractionOutlineSystem : EntitySystem if (_uiManager.CurrentlyHovered is IViewportControl vp && _inputManager.MouseScreenPosition.IsValid) { - var mousePosWorld = vp.ScreenToMap(_inputManager.MouseScreenPosition.Position); + var mousePosWorld = vp.PixelToMap(_inputManager.MouseScreenPosition.Position); entityToClick = screen.GetClickedEntity(mousePosWorld); if (vp is ScalingViewport svp) diff --git a/Content.Client/Outline/TargetOutlineSystem.cs b/Content.Client/Outline/TargetOutlineSystem.cs index 44171b754e..67507c3070 100644 --- a/Content.Client/Outline/TargetOutlineSystem.cs +++ b/Content.Client/Outline/TargetOutlineSystem.cs @@ -118,7 +118,7 @@ public sealed class TargetOutlineSystem : EntitySystem // find possible targets on screen // TODO: Duplicated in SpriteSystem and DragDropSystem. Should probably be cached somewhere for a frame? - var mousePos = _eyeManager.ScreenToMap(_inputManager.MouseScreenPosition).Position; + var mousePos = _eyeManager.PixelToMap(_inputManager.MouseScreenPosition).Position; var bounds = new Box2(mousePos - LookupVector, mousePos + LookupVector); var pvsEntities = _lookup.GetEntitiesIntersecting(_eyeManager.CurrentMap, bounds, LookupFlags.Approximate | LookupFlags.Static); var spriteQuery = GetEntityQuery(); diff --git a/Content.Client/Singularity/SingularityOverlay.cs b/Content.Client/Singularity/SingularityOverlay.cs index f163b9570b..c7c6aa2b5b 100644 --- a/Content.Client/Singularity/SingularityOverlay.cs +++ b/Content.Client/Singularity/SingularityOverlay.cs @@ -1,15 +1,17 @@ -using System.Numerics; using Content.Shared.Singularity.Components; using Robust.Client.Graphics; +using Robust.Client.UserInterface.CustomControls; using Robust.Shared.Enums; using Robust.Shared.Prototypes; +using System.Numerics; namespace Content.Client.Singularity { - public sealed class SingularityOverlay : Overlay + public sealed class SingularityOverlay : Overlay, IEntityEventSubscriber { [Dependency] private readonly IEntityManager _entMan = default!; [Dependency] private readonly IPrototypeManager _prototypeManager = default!; + private SharedTransformSystem? _xformSystem = null; /// /// Maximum number of distortions that can be shown on screen at a time. @@ -29,6 +31,8 @@ namespace Content.Client.Singularity IoCManager.InjectDependencies(this); _shader = _prototypeManager.Index("Singularity").Instance().Duplicate(); _shader.SetParameter("maxDistance", MaxDistance * EyeManager.PixelsPerMeter); + _entMan.EventBus.SubscribeEvent(EventSource.Local, this, OnProjectFromScreenToMap); + ZIndex = 101; // Should be drawn after the placement overlay so admins placing items near the singularity can tell where they're going. } private readonly Vector2[] _positions = new Vector2[MaxCount]; @@ -40,14 +44,17 @@ namespace Content.Client.Singularity { if (args.Viewport.Eye == null) return false; + if (_xformSystem is null && !_entMan.TrySystem(out _xformSystem)) + return false; _count = 0; - foreach (var (distortion, xform) in _entMan.EntityQuery()) + var query = _entMan.EntityQueryEnumerator(); + while (query.MoveNext(out var uid, out var distortion, out var xform)) { if (xform.MapID != args.MapId) continue; - var mapPos = xform.WorldPosition; + var mapPos = _xformSystem.GetWorldPosition(uid); // is the distortion in range? if ((mapPos - args.WorldAABB.ClosestPoint(mapPos)).LengthSquared() > MaxDistance * MaxDistance) @@ -56,7 +63,7 @@ namespace Content.Client.Singularity // To be clear, this needs to use "inside-viewport" pixels. // In other words, specifically NOT IViewportControl.WorldToScreen (which uses outer coordinates). var tempCoords = args.Viewport.WorldToLocal(mapPos); - tempCoords.Y = args.Viewport.Size.Y - tempCoords.Y; + tempCoords.Y = args.Viewport.Size.Y - tempCoords.Y; // Local space to fragment space. _positions[_count] = tempCoords; _intensities[_count] = distortion.Intensity; @@ -87,6 +94,50 @@ namespace Content.Client.Singularity worldHandle.DrawRect(args.WorldAABB, Color.White); worldHandle.UseShader(null); } + + /// + /// Repeats the transformation applied by the shader in + /// + private void OnProjectFromScreenToMap(ref PixelToMapEvent args) + { // Mostly copypasta from the singularity shader. + var maxDistance = MaxDistance * EyeManager.PixelsPerMeter; + var finalCoords = args.VisiblePosition; + + for (var i = 0; i < MaxCount && i < _count; i++) + { + // An explanation of pain: + // The shader used by the singularity to create the neat distortion effect occurs in _fragment space_ + // All of these calculations are done in _local space_. + // The only difference between the two is that in fragment space 'Y' is measured in pixels from the bottom of the viewport... + // and in local space 'Y' is measured in pixels from the top of the viewport. + // As a minor optimization the locations of the singularities are transformed into fragment space in BeforeDraw so the shader doesn't need to. + // 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. + var localPosition = _positions[i]; + localPosition.Y = args.Viewport.Size.Y - localPosition.Y; + var delta = args.VisiblePosition - localPosition; + var distance = (delta / args.Viewport.RenderScale).Length(); + + var deformation = _intensities[i] / MathF.Pow(distance, _falloffPowers[i]); + + // ensure deformation goes to zero at max distance + // avoids long-range single-pixel shifts that are noticeable when leaving PVS. + + if (distance >= maxDistance) + deformation = 0.0f; + else + deformation *= 1.0f - MathF.Pow(distance / maxDistance, 4.0f); + + if (deformation > 0.8) + deformation = MathF.Pow(deformation, 0.3f); + + finalCoords -= delta * deformation; + } + + 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. + finalCoords.Y -= MathF.Floor(finalCoords.Y / (args.Viewport.Size.Y * 2)) * args.Viewport.Size.Y * 2; + finalCoords.X = (finalCoords.X >= args.Viewport.Size.X) ? ((args.Viewport.Size.X * 2) - finalCoords.X) : finalCoords.X; + finalCoords.Y = (finalCoords.Y >= args.Viewport.Size.Y) ? ((args.Viewport.Size.Y * 2) - finalCoords.Y) : finalCoords.Y; + args.VisiblePosition = finalCoords; + } } } - diff --git a/Content.Client/Tabletop/TabletopSystem.cs b/Content.Client/Tabletop/TabletopSystem.cs index 5aafb5ffe3..ee0f9646c5 100644 --- a/Content.Client/Tabletop/TabletopSystem.cs +++ b/Content.Client/Tabletop/TabletopSystem.cs @@ -94,7 +94,7 @@ namespace Content.Client.Tabletop } // Map mouse position to EntityCoordinates - var coords = _viewport.ScreenToMap(_inputManager.MouseScreenPosition.Position); + var coords = _viewport.PixelToMap(_inputManager.MouseScreenPosition.Position); // Clamp coordinates to viewport var clampedCoords = ClampPositionToViewport(coords, _viewport); diff --git a/Content.Client/Viewport/ScalingViewport.cs b/Content.Client/Viewport/ScalingViewport.cs index 69b5d51567..4237679958 100644 --- a/Content.Client/Viewport/ScalingViewport.cs +++ b/Content.Client/Viewport/ScalingViewport.cs @@ -21,6 +21,7 @@ namespace Content.Client.Viewport public sealed class ScalingViewport : Control, IViewportControl { [Dependency] private readonly IClyde _clyde = default!; + [Dependency] private readonly IEntityManager _entityManager = default!; [Dependency] private readonly IInputManager _inputManager = default!; // Internal viewport creation is deferred. @@ -252,8 +253,26 @@ namespace Content.Client.Viewport EnsureViewportCreated(); var matrix = Matrix3.Invert(GetLocalToScreenMatrix()); + coords = matrix.Transform(coords); - return _viewport!.LocalToWorld(matrix.Transform(coords)); + return _viewport!.LocalToWorld(coords); + } + + /// + public MapCoordinates PixelToMap(Vector2 coords) + { + if (_eye == null) + return default; + + EnsureViewportCreated(); + + var matrix = Matrix3.Invert(GetLocalToScreenMatrix()); + coords = matrix.Transform(coords); + + var ev = new PixelToMapEvent(coords, this, _viewport!); + _entityManager.EventBus.RaiseEvent(EventSource.Local, ref ev); + + return _viewport!.LocalToWorld(ev.VisiblePosition); } public Vector2 WorldToScreen(Vector2 map) diff --git a/Content.Client/Weapons/Melee/MeleeArcOverlay.cs b/Content.Client/Weapons/Melee/MeleeArcOverlay.cs index dcbbb3219b..29bfa09565 100644 --- a/Content.Client/Weapons/Melee/MeleeArcOverlay.cs +++ b/Content.Client/Weapons/Melee/MeleeArcOverlay.cs @@ -47,7 +47,7 @@ public sealed class MeleeArcOverlay : Overlay return; var mousePos = _inputManager.MouseScreenPosition; - var mapPos = _eyeManager.ScreenToMap(mousePos); + var mapPos = _eyeManager.PixelToMap(mousePos); if (mapPos.MapId != args.MapId) return; diff --git a/Content.Client/Weapons/Melee/MeleeWeaponSystem.cs b/Content.Client/Weapons/Melee/MeleeWeaponSystem.cs index fba9655791..a232105642 100644 --- a/Content.Client/Weapons/Melee/MeleeWeaponSystem.cs +++ b/Content.Client/Weapons/Melee/MeleeWeaponSystem.cs @@ -96,7 +96,7 @@ public sealed partial class MeleeWeaponSystem : SharedMeleeWeaponSystem return; } - var mousePos = _eyeManager.ScreenToMap(_inputManager.MouseScreenPosition); + var mousePos = _eyeManager.PixelToMap(_inputManager.MouseScreenPosition); if (mousePos.MapId == MapId.Nullspace) { diff --git a/Content.Client/Weapons/Misc/TetherGunSystem.cs b/Content.Client/Weapons/Misc/TetherGunSystem.cs index 0ee1cc4b3f..2063fa6cf1 100644 --- a/Content.Client/Weapons/Misc/TetherGunSystem.cs +++ b/Content.Client/Weapons/Misc/TetherGunSystem.cs @@ -64,7 +64,7 @@ public sealed class TetherGunSystem : SharedTetherGunSystem } var mousePos = _input.MouseScreenPosition; - var mouseWorldPos = _eyeManager.ScreenToMap(mousePos); + var mouseWorldPos = _eyeManager.PixelToMap(mousePos); if (mouseWorldPos.MapId == MapId.Nullspace) return; diff --git a/Content.Client/Weapons/Ranged/GunSpreadOverlay.cs b/Content.Client/Weapons/Ranged/GunSpreadOverlay.cs index d179f3f63f..559c465e6d 100644 --- a/Content.Client/Weapons/Ranged/GunSpreadOverlay.cs +++ b/Content.Client/Weapons/Ranged/GunSpreadOverlay.cs @@ -50,7 +50,7 @@ public sealed class GunSpreadOverlay : Overlay return; var mouseScreenPos = _input.MouseScreenPosition; - var mousePos = _eye.ScreenToMap(mouseScreenPos); + var mousePos = _eye.PixelToMap(mouseScreenPos); if (mapPos.MapId != mousePos.MapId) return; diff --git a/Content.Client/Weapons/Ranged/Systems/GunSystem.cs b/Content.Client/Weapons/Ranged/Systems/GunSystem.cs index 1cfcd2a69a..2d8f5fdbfa 100644 --- a/Content.Client/Weapons/Ranged/Systems/GunSystem.cs +++ b/Content.Client/Weapons/Ranged/Systems/GunSystem.cs @@ -151,7 +151,7 @@ public sealed partial class GunSystem : SharedGunSystem if (gun.NextFire > Timing.CurTime) return; - var mousePos = _eyeManager.ScreenToMap(_inputManager.MouseScreenPosition); + var mousePos = _eyeManager.PixelToMap(_inputManager.MouseScreenPosition); if (mousePos.MapId == MapId.Nullspace) {