View Rotation (#1016)

* Adds a derived class from Nunit's Is test constraint class.
The game camera can now be rotated.

* Added the new `bind` command to the console, used to bind a key to a keybind.
Added two new keybinds for rotating the View camera left and right.
Added more info to the ""Component does not exist for state" exception.
Added Reduced() and FlipPositive() public functions to Angle.

* Fix a copy/paste bug.

* Adds missing code to bind/unbind CameraRotateLeft.
Camera snapping now actually uses the CameraSnapTolerance constant.
This commit is contained in:
Acruid
2020-03-24 18:22:18 -07:00
committed by GitHub
parent c4b9c1cc4e
commit a947a38f3c
31 changed files with 634 additions and 116 deletions

View File

@@ -157,7 +157,6 @@ namespace Robust.Client
// Call Init in game assemblies.
_modLoader.BroadcastRunLevel(ModRunLevel.Init);
_eyeManager.Initialize();
_serializer.Initialize();
_userInterfaceManager.Initialize();
_networkManager.Initialize(false);

View File

@@ -20,6 +20,7 @@ namespace Robust.Client.GameObjects
{
#pragma warning disable 649
[Dependency] private readonly IMapManager _mapManager;
[Dependency] private readonly IComponentFactory _compFactory;
#if EXCEPTION_TOLERANCE
[Dependency] private readonly IRuntimeLog _runtimeLog;
#endif
@@ -247,8 +248,7 @@ namespace Robust.Client.GameObjects
if (compMan.HasComponent(entityUid, compChange.NetID))
continue;
var newComp = (Component) IoCManager.Resolve<IComponentFactory>()
.GetComponent(compChange.ComponentName);
var newComp = (Component) _compFactory.GetComponent(compChange.ComponentName);
newComp.Owner = entity;
compMan.AddComponent(entity, newComp, true);
}
@@ -282,7 +282,10 @@ namespace Robust.Client.GameObjects
{
if (!compMan.TryGetComponent(entityUid, kvStates.Key, out var component))
{
DebugTools.Assert("Component does not exist for state.");
var eUid = entityUid;
var eExpectedNetUid = kvStates.Key;
var eRegisteredNetUidName = _compFactory.GetRegistration(eExpectedNetUid).Name;
DebugTools.Assert($"Component does not exist for state: entUid={eUid}, expectedNetId={eExpectedNetUid}, expectedName={eRegisteredNetUidName}");
continue;
}

View File

@@ -19,6 +19,7 @@ namespace Robust.Client.GameObjects
/// <inheritdoc />
public override string Name => "Eye";
[ViewVariables]
private Eye _eye;
// Horrible hack to get around ordering issues.
@@ -63,6 +64,17 @@ namespace Robust.Client.GameObjects
}
}
[ViewVariables(VVAccess.ReadWrite)]
public Angle Rotation
{
get => _eye.Rotation;
set
{
if (_eye != null)
_eye.Rotation = value;
}
}
[ViewVariables(VVAccess.ReadWrite)]
public Vector2 Offset
{

View File

@@ -1090,7 +1090,7 @@ namespace Robust.Client.GameObjects
drawingHandle.UseShader(layer.Shader);
}
drawingHandle.DrawTexture(texture, -(Vector2) texture.Size / (2f * EyeManager.PIXELSPERMETER),
drawingHandle.DrawTexture(texture, -(Vector2) texture.Size / (2f * EyeManager.PixelsPerMeter),
color * layer.Color);
if (layer.Shader != null)

View File

@@ -357,7 +357,7 @@ namespace Robust.Client.GameObjects
new Angle(-effect.Rotation), effect.Size);
var effectSprite = effect.EffectSprite;
worldHandle.DrawTexture(effectSprite,
-((Vector2) effectSprite.Size / EyeManager.PIXELSPERMETER) / 2, ToColor(effect.Color));
-((Vector2) effectSprite.Size / EyeManager.PixelsPerMeter) / 2, ToColor(effect.Color));
}
}
}

View File

@@ -1,6 +1,15 @@
using JetBrains.Annotations;
using System;
using JetBrains.Annotations;
using Robust.Client.Interfaces.Graphics.ClientEye;
using Robust.Client.ViewVariables;
using Robust.Shared.GameObjects;
using Robust.Shared.GameObjects.Systems;
using Robust.Shared.Input;
using Robust.Shared.Interfaces.GameObjects.Systems;
using Robust.Shared.IoC;
using Robust.Shared.Maths;
#nullable enable
namespace Robust.Client.GameObjects.EntitySystems
{
@@ -10,17 +19,75 @@ namespace Robust.Client.GameObjects.EntitySystems
[UsedImplicitly]
internal class EyeUpdateSystem : EntitySystem
{
// How fast the camera rotates in radians
private const float CameraRotateSpeed = MathF.PI;
private const float CameraSnapTolerance = 0.01f;
#pragma warning disable 649, CS8618
// ReSharper disable once NotNullMemberIsNotInitialized
[Dependency] private readonly IEyeManager _eyeManager;
#pragma warning restore 649, CS8618
/// <inheritdoc />
public override void Initialize()
{
base.Initialize();
EntityQuery = new TypeEntityQuery(typeof(EyeComponent));
//WARN: Tightly couples this system with InputSystem, and assumes InputSystem exists and is initialized
var inputSystem = EntitySystemManager.GetEntitySystem<InputSystem>();
inputSystem.BindMap.BindFunction(EngineKeyFunctions.CameraRotateRight, new NullInputCmdHandler());
inputSystem.BindMap.BindFunction(EngineKeyFunctions.CameraRotateLeft, new NullInputCmdHandler());
}
/// <inheritdoc />
public override void Shutdown()
{
//WARN: Tightly couples this system with InputSystem, and assumes InputSystem exists and is initialized
var inputSystem = EntitySystemManager.GetEntitySystem<InputSystem>();
inputSystem.BindMap.UnbindFunction(EngineKeyFunctions.CameraRotateRight);
inputSystem.BindMap.UnbindFunction(EngineKeyFunctions.CameraRotateLeft);
base.Shutdown();
}
/// <inheritdoc />
public override void FrameUpdate(float frameTime)
{
var currentEye = _eyeManager.CurrentEye;
var inputSystem = EntitySystemManager.GetEntitySystem<InputSystem>();
var direction = 0;
if (inputSystem.CmdStates[EngineKeyFunctions.CameraRotateRight] == BoundKeyState.Down)
{
direction += 1;
}
if (inputSystem.CmdStates[EngineKeyFunctions.CameraRotateLeft] == BoundKeyState.Down)
{
direction -= 1;
}
// apply camera rotation
if(direction != 0)
{
currentEye.Rotation += CameraRotateSpeed * frameTime * direction;
currentEye.Rotation = currentEye.Rotation.Reduced();
}
else
{
// snap to cardinal directions
var closestDir = currentEye.Rotation.GetCardinalDir().ToVec();
var currentDir = currentEye.Rotation.ToVec();
var dot = Vector2.Dot(closestDir, currentDir);
if (FloatMath.CloseTo(dot, 1, CameraSnapTolerance))
{
currentEye.Rotation = closestDir.ToAngle();
}
}
foreach (var entity in RelevantEntities)
{
var eyeComp = entity.GetComponent<EyeComponent>();

View File

@@ -1,31 +1,72 @@
using Robust.Client.Interfaces.Graphics.ClientEye;
using Robust.Shared.Map;
using Robust.Shared.Maths;
using Robust.Shared.ViewVariables;
#nullable enable
namespace Robust.Client.Graphics.ClientEye
{
/// <inheritdoc />
public class Eye : IEye
{
//TODO: This is literally a Transformation matrix, might as well add Rotation to it as well.
private Vector2 _scale = Vector2.One;
private Angle _rotation = Angle.Zero;
private MapCoordinates _coords;
/// <inheritdoc />
public Vector2 Zoom { get; set; } = Vector2.One;
[ViewVariables(VVAccess.ReadWrite)]
public bool DrawFov { get; set; } = true;
/// <inheritdoc />
public virtual MapCoordinates Position { get; internal set; }
/// <inheritdoc />
public Matrix3 GetViewMatrix()
[ViewVariables(VVAccess.ReadWrite)]
public virtual MapCoordinates Position
{
var matrix = Matrix3.Identity;
matrix.R0C0 = 1 / Zoom.X;
matrix.R1C1 = 1 / Zoom.Y;
matrix.R0C2 = -Position.X / Zoom.X;
matrix.R1C2 = -Position.Y / Zoom.Y;
return matrix;
get => _coords;
internal set => _coords = value;
}
public bool DrawFov { get; set; } = true;
/// <inheritdoc />
[ViewVariables(VVAccess.ReadWrite)]
public Angle Rotation
{
get => _rotation;
set => _rotation = value;
}
/// <inheritdoc />
[ViewVariables(VVAccess.ReadWrite)]
public Vector2 Zoom
{
get => new Vector2(1 / _scale.X, 1 / _scale.Y);
set => _scale = new Vector2(1 / value.X, 1 / value.Y);
}
/// <inheritdoc />
[ViewVariables(VVAccess.ReadWrite)]
public Vector2 Scale
{
get => _scale;
set => _scale = value;
}
/// <inheritdoc />
public void GetViewMatrix(out Matrix3 viewMatrix)
{
//TODO: Something is funky with this, matrices are being multiplied backwards, Position is negative
//Should this be a normal transform that is inverted?
var scaleMat = Matrix3.CreateScale(_scale.X, _scale.Y);
var rotMat = Matrix3.CreateRotation(_rotation);
var transMat = Matrix3.CreateTranslation(-_coords.Position);
viewMatrix = transMat * rotMat * scaleMat;
}
/// <inheritdoc />
public void GetViewMatrixInv(out Matrix3 viewMatrixInv)
{
GetViewMatrix(out var viewMatrix);
viewMatrixInv = Matrix3.Invert(viewMatrix);
}
}
}

View File

@@ -5,41 +5,42 @@ using Robust.Shared.IoC;
using Robust.Shared.Map;
using Robust.Shared.Maths;
#nullable enable
namespace Robust.Client.Graphics.ClientEye
{
/// <inheritdoc />
public sealed class EyeManager : IEyeManager
{
// If you modify this make sure to edit the value in the Robust.Shared.Audio.AudioParams struct default too!
// No I can't be bothered to make this a shared constant.
public const int PIXELSPERMETER = 32;
/// <summary>
/// Default scaling for the projection matrix.
/// </summary>
public const int PixelsPerMeter = 32;
#pragma warning disable 649
#pragma warning disable 649, CS8618
// ReSharper disable twice NotNullMemberIsNotInitialized
[Dependency] private readonly IMapManager _mapManager;
[Dependency] private readonly IClyde _displayManager;
#pragma warning restore 649
#pragma warning restore 649, CS8618
// We default to this when we get set to a null eye.
private FixedEye defaultEye;
private readonly FixedEye _defaultEye = new FixedEye();
private IEye currentEye;
private IEye? _currentEye;
/// <inheritdoc />
public IEye CurrentEye
{
get => currentEye;
set
{
if (currentEye == value)
{
return;
}
currentEye = value ?? defaultEye;
}
get => _currentEye ?? _defaultEye;
set => _currentEye = value;
}
public MapId CurrentMap => currentEye.Position.MapId;
/// <inheritdoc />
public MapId CurrentMap => CurrentEye.Position.MapId;
/// <inheritdoc />
public Box2 GetWorldViewport()
{
var vpSize = _displayManager.ScreenSize;
@@ -57,32 +58,57 @@ namespace Robust.Client.Graphics.ClientEye
return new Box2(left, bottom, right, top);
}
public void Initialize()
{
defaultEye = new FixedEye();
currentEye = defaultEye;
}
/// <inheritdoc />
public Vector2 WorldToScreen(Vector2 point)
{
var matrix = CurrentEye.GetViewMatrix();
point = matrix.Transform(point);
point *= new Vector2(1, -1) * PIXELSPERMETER;
point += _displayManager.ScreenSize/2f;
return point;
var newPoint = point;
CurrentEye.GetViewMatrix(out var viewMatrix);
newPoint = viewMatrix * newPoint;
// (inlined version of UiProjMatrix)
newPoint *= new Vector2(1, -1) * PixelsPerMeter;
newPoint += _displayManager.ScreenSize / 2f;
return newPoint;
}
/// <inheritdoc />
public void GetScreenProjectionMatrix(out Matrix3 projMatrix)
{
Matrix3 result = default;
result.R0C0 = PixelsPerMeter;
result.R1C1 = -PixelsPerMeter;
var screenSize = _displayManager.ScreenSize;
result.R0C2 = screenSize.X / 2f;
result.R1C2 = screenSize.Y / 2f;
result.R2C2 = 1;
/* column major
Sx 0 Tx
0 Sy Ty
0 0 1
*/
projMatrix = result;
}
/// <inheritdoc />
public ScreenCoordinates WorldToScreen(GridCoordinates point)
{
var worldCoords = _mapManager.GetGrid(point.GridID).LocalToWorld(point);
return new ScreenCoordinates(WorldToScreen(worldCoords.Position));
}
/// <inheritdoc />
public GridCoordinates ScreenToWorld(ScreenCoordinates point)
{
return ScreenToWorld(point.Position);
}
/// <inheritdoc />
public GridCoordinates ScreenToWorld(Vector2 point)
{
var mapCoords = ScreenToMap(point);
@@ -95,12 +121,20 @@ namespace Robust.Client.Graphics.ClientEye
return new GridCoordinates(grid.WorldToLocal(mapCoords.Position), grid.Index);
}
/// <inheritdoc />
public MapCoordinates ScreenToMap(Vector2 point)
{
var matrix = Matrix3.Invert(CurrentEye.GetViewMatrix());
point -= _displayManager.ScreenSize / 2f;
var worldPos = matrix.Transform(point / PIXELSPERMETER * new Vector2(1, -1));
return new MapCoordinates(worldPos, CurrentMap);
var newPoint = point;
// (inlined version of UiProjMatrix^-1)
newPoint -= _displayManager.ScreenSize / 2f;
newPoint *= new Vector2(1, -1) / PixelsPerMeter;
// view matrix
CurrentEye.GetViewMatrixInv(out var viewMatrixInv);
newPoint = viewMatrixInv * newPoint;
return new MapCoordinates(newPoint, CurrentMap);
}
}
}

View File

@@ -80,7 +80,7 @@ namespace Robust.Client.Graphics.Clyde
// Calculate world-space AABB for camera, to cull off-screen things.
var eye = _eyeManager.CurrentEye;
var worldBounds = Box2.CenteredAround(eye.Position.Position,
_framebufferSize / (float) EyeManager.PIXELSPERMETER * eye.Zoom);
_framebufferSize / (float) EyeManager.PixelsPerMeter * eye.Zoom);
using (DebugGroup("Lights"))
{

View File

@@ -202,7 +202,7 @@ namespace Robust.Client.Graphics.Clyde
if (eye.DrawFov)
{
// Calculate maximum distance for the projection based on screen size.
var screenSizeCut = ScreenSize / EyeManager.PIXELSPERMETER;
var screenSizeCut = ScreenSize / EyeManager.PixelsPerMeter;
var maxDist = (float) Math.Max(screenSizeCut.X, screenSizeCut.Y);
// FOV is rendered twice.
@@ -519,7 +519,7 @@ namespace Robust.Client.Graphics.Clyde
// Have to scale the blurring radius based on viewport size and camera zoom.
const float refCameraHeight = 14;
var cameraSize = eye.Zoom.Y * ScreenSize.Y / EyeManager.PIXELSPERMETER;
var cameraSize = eye.Zoom.Y * ScreenSize.Y / EyeManager.PixelsPerMeter;
// 7e-3f is just a magic factor that makes it look ok.
var factor = 7e-3f * (refCameraHeight / cameraSize);
@@ -786,7 +786,7 @@ namespace Robust.Client.Graphics.Clyde
// On low-res lights we bias the occlusion mask inwards.
// This avoids wall lighting going onto the tile next to them at certain tile alignments.
// It's inwards to avoid seeing disconnected shadows on wall edges.
const float bias = 0.5f / EyeManager.PIXELSPERMETER;
const float bias = 0.5f / EyeManager.PixelsPerMeter;
if (!no)
{

View File

@@ -94,8 +94,8 @@ namespace Robust.Client.Graphics.Clyde
var viewMatrix = Matrix3.Identity;
viewMatrix.R0C0 = scale.X;
viewMatrix.R1C1 = scale.Y;
viewMatrix.R0C2 = ofsX / EyeManager.PIXELSPERMETER;
viewMatrix.R1C2 = -ofsY / EyeManager.PIXELSPERMETER;
viewMatrix.R0C2 = ofsX / EyeManager.PixelsPerMeter;
viewMatrix.R1C2 = -ofsY / EyeManager.PixelsPerMeter;
SetViewTransform(viewMatrix);
}

View File

@@ -1,4 +1,4 @@
using System;
using System;
using System.Collections.Generic;
using System.Diagnostics.CodeAnalysis;
using System.Runtime.CompilerServices;
@@ -85,24 +85,22 @@ namespace Robust.Client.Graphics.Clyde
{
var eye = _eyeManager.CurrentEye;
var toScreen = _eyeManager.WorldToScreen(eye.Position.Position);
// Round camera position to a screen pixel to avoid weird issues on odd screen sizes.
toScreen = ((float) Math.Floor(toScreen.X), (float) Math.Floor(toScreen.Y));
var cameraWorldAdjusted = _eyeManager.ScreenToMap(toScreen);
eye.GetViewMatrix(out var viewMatrixWorld);
var viewMatrixWorld = Matrix3.Identity;
viewMatrixWorld.R0C0 = 1 / eye.Zoom.X;
viewMatrixWorld.R1C1 = 1 / eye.Zoom.Y;
viewMatrixWorld.R0C2 = -cameraWorldAdjusted.X / eye.Zoom.X;
viewMatrixWorld.R1C2 = -cameraWorldAdjusted.Y / eye.Zoom.Y;
var projMatrixWorld = Matrix3.Identity;
projMatrixWorld.R0C0 = EyeManager.PIXELSPERMETER * 2f / ScreenSize.X;
projMatrixWorld.R1C1 = EyeManager.PIXELSPERMETER * 2f / ScreenSize.Y;
CalcWorldProjectionMatrix(out var projMatrixWorld);
return new ProjViewMatrices(projMatrixWorld, viewMatrixWorld);
}
/// <inheritdoc />
public void CalcWorldProjectionMatrix(out Matrix3 projMatrix)
{
var projMatrixWorld = Matrix3.Identity;
projMatrixWorld.R0C0 = EyeManager.PixelsPerMeter * 2f / ScreenSize.X;
projMatrixWorld.R1C1 = EyeManager.PixelsPerMeter * 2f / ScreenSize.Y;
projMatrix = projMatrixWorld;
}
private void _setProjViewMatrices(in ProjViewMatrices matrices)
{
_currentMatrices = matrices;

View File

@@ -220,7 +220,7 @@ namespace Robust.Client.Graphics.Clyde
GL.BindBufferBase(BufferRangeTarget.UniformBuffer, UniformConstantsBindingIndex,
UniformConstantsUBO.ObjectHandle);
EntityPostRenderTarget = CreateRenderTarget(Vector2i.One * 4 * EyeManager.PIXELSPERMETER,
EntityPostRenderTarget = CreateRenderTarget(Vector2i.One * 4 * EyeManager.PixelsPerMeter,
new RenderTargetFormatParameters(RenderTargetColorFormat.Rgba8Srgb, true),
name: nameof(EntityPostRenderTarget));
}

View File

@@ -1,4 +1,4 @@
using System;
using System;
using System.IO;
using Robust.Client.Audio;
using Robust.Client.Graphics.Shaders;
@@ -89,6 +89,11 @@ namespace Robust.Client.Graphics.Clyde
return new DummyRenderTarget(size, new DummyTexture(size));
}
public void CalcWorldProjectionMatrix(out Matrix3 projMatrix)
{
projMatrix = Matrix3.Identity;
}
public ClydeHandle LoadShader(ParsedShader shader, string name = null)
{
return default;

View File

@@ -5,7 +5,7 @@ namespace Robust.Client.Graphics.Drawing
{
public abstract class DrawingHandleWorld : DrawingHandleBase
{
private const int Ppm = EyeManager.PIXELSPERMETER;
private const int Ppm = EyeManager.PixelsPerMeter;
public abstract void DrawRect(Box2 rect, Color color, bool filled = true);
public abstract void DrawRect(in Box2Rotated rect, Color color, bool filled = true);

View File

@@ -25,6 +25,8 @@ namespace Robust.Client.Input
common.AddFunction(EngineKeyFunctions.MoveLeft);
common.AddFunction(EngineKeyFunctions.MoveRight);
common.AddFunction(EngineKeyFunctions.Run);
common.AddFunction(EngineKeyFunctions.CameraRotateRight);
common.AddFunction(EngineKeyFunctions.CameraRotateLeft);
common.AddFunction(EngineKeyFunctions.TextCursorLeft);
common.AddFunction(EngineKeyFunctions.TextCursorRight);

View File

@@ -4,6 +4,8 @@ using System.IO;
using System.Linq;
using System.Runtime.InteropServices;
using System.Text;
using JetBrains.Annotations;
using Robust.Client.Interfaces.Console;
using Robust.Client.Interfaces.Input;
using Robust.Shared.Input;
using Robust.Shared.Interfaces.Reflection;
@@ -328,9 +330,18 @@ namespace Robust.Client.Input
}
}
/// <inheritdoc />
public void RegisterBinding(BoundKeyFunction function, KeyBindingType bindingType,
Key baseKey, Key? mod1, Key? mod2, Key? mod3)
{
var binding = new KeyBinding(this, function, bindingType, baseKey, false, false,
mod1 ?? Key.Unknown, mod2 ?? Key.Unknown, mod3 ?? Key.Unknown);
RegisterBinding(binding);
}
private void RegisterBinding(KeyBinding binding)
{
//TODO: Assert there are no duplicate binding combos
_bindings.Add(binding);
// reversed a,b for descending order
@@ -519,4 +530,51 @@ namespace Robust.Client.Input
Enabled,
Disabled,
}
[UsedImplicitly]
internal class BindCommand : IConsoleCommand
{
public string Command => "bind";
public string Description => "Binds an input key to an input command.";
public string Help => "bind <KeyName> <BindMode> <InputCommand>";
public bool Execute(IDebugConsole console, params string[] args)
{
if (args.Length < 3)
{
console.AddLine("Too few arguments.");
return false;
}
if (args.Length > 3)
{
console.AddLine("Too many arguments.");
return false;
}
var keyName = args[0];
if (!Enum.TryParse(typeof(Keyboard.Key), keyName, true, out var keyIdObj))
{
console.AddLine($"Key '{keyName}' is unrecognized.");
return false;
}
var keyId = (Keyboard.Key) keyIdObj;
if (!Enum.TryParse(typeof(KeyBindingType), args[1], true, out var keyModeObj))
{
console.AddLine($"BindMode '{args[1]}' is unrecognized.");
return false;
}
var keyMode = (KeyBindingType)keyModeObj;
var inputCommand = args[2];
var inputMan = IoCManager.Resolve<IInputManager>();
inputMan.RegisterBinding(new BoundKeyFunction(inputCommand), keyMode, keyId, null, null, null);
return false;
}
}
}

View File

@@ -1,18 +1,20 @@
using Robust.Shared.Map;
using JetBrains.Annotations;
using Robust.Shared.Map;
using Robust.Shared.Maths;
namespace Robust.Client.Interfaces.Graphics.ClientEye
{
/// <summary>
/// An Eye is a point through which the player can view the world.
/// It's a 2D camera in other game dev lingo basically.
/// An Eye is a point through which the player can view the world.
/// It's a 2D camera in other game dev lingo basically.
/// </summary>
[PublicAPI]
public interface IEye
{
/// <summary>
/// Current zoom level of this eye. Zoom is the inverse of Scale (Zoom = 1 / Scale).
/// Should the black FoV effect be drawn for this eye?
/// </summary>
Vector2 Zoom { get; set; }
bool DrawFov { get; }
/// <summary>
/// Current position of the center of the eye in the game world.
@@ -20,10 +22,32 @@ namespace Robust.Client.Interfaces.Graphics.ClientEye
MapCoordinates Position { get; }
/// <summary>
/// Returns the view matrix for this eye.
/// Rotation of the camera around the Z axis.
/// </summary>
Matrix3 GetViewMatrix();
Angle Rotation { get; set; }
bool DrawFov { get; }
/// <summary>
/// Current zoom level of this eye. Zoom is the inverse of Scale (Zoom = 1 / Scale).
/// </summary>
Vector2 Zoom { get; set; }
/// <summary>
/// Current view scale of this eye. Scale is the inverse of Zoom (Scale = 1 / Zoom).
/// </summary>
Vector2 Scale { get; set; }
/// <summary>
/// Returns the view matrix for this eye, used to convert a point from
/// world space to camera space.
/// </summary>
/// <param name="viewMatrix">View matrix for this camera.</param>
void GetViewMatrix(out Matrix3 viewMatrix);
/// <summary>
/// Returns the inverted view matrix for this eye, used to convert a point from
/// camera space to world space.
/// </summary>
/// <param name="viewMatrixInv">Inverted view matrix for this camera.</param>
void GetViewMatrixInv(out Matrix3 viewMatrixInv);
}
}

View File

@@ -1,8 +1,13 @@
using Robust.Shared.Map;
using System;
using Robust.Shared.Map;
using Robust.Shared.Maths;
namespace Robust.Client.Interfaces.Graphics.ClientEye
{
/// <summary>
/// Keeps a reference to the current eye (camera) that the client is seeing though, and provides
/// utility functions for the current eye.
/// </summary>
public interface IEyeManager
{
/// <summary>
@@ -14,21 +19,76 @@ namespace Robust.Client.Interfaces.Graphics.ClientEye
IEye CurrentEye { get; set; }
/// <summary>
/// The ID of the map on which the current eye is "placed".
/// The ID of the map on which the current eye is "placed".
/// </summary>
MapId CurrentMap { get; }
/// <summary>
/// A world-space box that is at LEAST the area covered by the viewport.
/// May be larger due to say rotation.
/// A world-space box that is at LEAST the area covered by the viewport.
/// May be larger due to say rotation.
/// </summary>
Box2 GetWorldViewport();
void Initialize();
/// <summary>
/// Calculates the projection matrix to transform a point from camera space
/// to UI screen space.
/// </summary>
/// <param name="projMatrix"></param>
void GetScreenProjectionMatrix(out Matrix3 projMatrix);
/// <summary>
/// Projects a point from world space to UI screen space using the current camera.
/// </summary>
/// <param name="point">Point in world to transform.</param>
/// <returns>Corresponding point in UI screen space.</returns>
Vector2 WorldToScreen(Vector2 point);
/// <summary>
/// Projects a point from world space to UI screen space using the current camera.
/// </summary>
/// <param name="point">Point in world to transform.</param>
/// <returns>Corresponding point in UI screen space.</returns>
ScreenCoordinates WorldToScreen(GridCoordinates point);
/// <summary>
/// Unprojects a point from UI screen space to world space using the current camera.
/// </summary>
/// <remarks>
/// The game exists on the 2D X/Y plane, so this function returns a point
/// instead of a line segment.
/// </remarks>
/// <param name="point">Point on screen to transform.</param>
/// <returns>
/// Corresponding grid-local point in the world. If there is no grid at the
/// point, the GridID will be Invalid and the Position will be in world coordinates.
/// </returns>
[Obsolete("The call site should use ScreenToMap(), and then call IMapManager.TryFindGridAt().")]
GridCoordinates ScreenToWorld(ScreenCoordinates point);
/// <summary>
/// Unprojects a point from UI screen space to world space using the current camera.
/// </summary>
/// <remarks>
/// The game exists on the 2D X/Y plane, so this function returns a point on the plane
/// instead of a line segment.
/// </remarks>
/// <param name="point">Point on screen to transform.</param>
/// <returns>
/// Corresponding grid-local point in the world. If there is no grid at the
/// point, the GridID will be Invalid and the Position will be in world coordinates.
/// </returns>
[Obsolete("The call site should use ScreenToMap(), and then call IMapManager.TryFindGridAt().")]
GridCoordinates ScreenToWorld(Vector2 point);
/// <summary>
/// Unprojects a point from UI screen space to world space using the current camera.
/// </summary>
/// <remarks>
/// The game exists on the 2D X/Y plane, so this function returns a point o the plane
/// instead of a line segment.
/// </remarks>
/// <param name="point">Point on screen to transform.</param>
/// <returns>Corresponding point in the world.</returns>
MapCoordinates ScreenToMap(Vector2 point);
}
}

View File

@@ -21,6 +21,8 @@ namespace Robust.Client.Interfaces.Graphics
IRenderTarget CreateRenderTarget(Vector2i size, RenderTargetFormatParameters format,
TextureSampleParameters? sampleParameters = null, string name = null);
void CalcWorldProjectionMatrix(out Matrix3 projMatrix);
}
// TODO: Maybe implement IDisposable for render targets. I got lazy and didn't.

View File

@@ -26,6 +26,18 @@ namespace Robust.Client.Interfaces.Input
void KeyDown(KeyEventArgs e);
void KeyUp(KeyEventArgs e);
/// <summary>
/// Registers a new key binding in the input manager.
/// </summary>
/// <param name="function">The function the key binding is bound to.</param>
/// <param name="bindingType"></param>
/// <param name="baseKey"></param>
/// <param name="mod1"></param>
/// <param name="mod2"></param>
/// <param name="mod3"></param>
void RegisterBinding(BoundKeyFunction function, KeyBindingType bindingType,
Keyboard.Key baseKey, Keyboard.Key? mod1, Keyboard.Key? mod2, Keyboard.Key? mod3);
/// <summary>
/// Gets a key binding according to the function it is bound to.
/// </summary>

View File

@@ -44,7 +44,7 @@ namespace Robust.Client.Map
private void _genTextureAtlas()
{
var defList = TileDefs.Where(t => !string.IsNullOrEmpty(t.SpriteName)).ToList();
const int tileSize = EyeManager.PIXELSPERMETER;
const int tileSize = EyeManager.PixelsPerMeter;
var dimensionX = (int) Math.Ceiling(Math.Sqrt(defList.Count));
var dimensionY = (int) Math.Ceiling((float) defList.Count / dimensionX);
@@ -77,7 +77,7 @@ namespace Robust.Client.Map
_tileRegions.Add(def.TileId,
Box2.FromDimensions(
point.X / w, (h - point.Y - EyeManager.PIXELSPERMETER) / h,
point.X / w, (h - point.Y - EyeManager.PixelsPerMeter) / h,
tileSize / w, tileSize / h));
}

View File

@@ -22,7 +22,7 @@ namespace Robust.Client.Placement.Modes
{
if (onGrid)
{
const int ppm = EyeManager.PIXELSPERMETER;
const int ppm = EyeManager.PixelsPerMeter;
var viewportSize = (Vector2)pManager._clyde.ScreenSize;
var position = pManager.eyeManager.ScreenToWorld(Vector2.Zero);
var gridstartx = (float) Math.Round(position.X / snapSize, MidpointRounding.AwayFromZero) * snapSize;

View File

@@ -108,7 +108,7 @@ namespace Robust.Client.Placement
foreach (var coordinate in locationcollection)
{
var worldPos = pManager.MapManager.GetGrid(coordinate.GridID).LocalToWorld(coordinate).Position;
var pos = worldPos - (size/(float)EyeManager.PIXELSPERMETER) / 2f;
var pos = worldPos - (size/(float)EyeManager.PixelsPerMeter) / 2f;
var color = IsValidPosition(coordinate) ? ValidPlaceColor : InvalidPlaceColor;
handle.DrawTexture(SpriteToDraw, pos, color);
}

View File

@@ -135,6 +135,14 @@ namespace Robust.Shared.Maths
|| FloatMath.CloseTo(aPositive, bPositive + MathHelper.TwoPi, tolerance);
}
/// <summary>
/// Removes revolutions from a positive or negative angle to make it as small as possible.
/// </summary>
public Angle Reduced()
{
return new Angle(Reduce(Theta));
}
/// <summary>
/// Removes revolutions from a positive or negative angle to make it as small as possible.
/// </summary>
@@ -176,6 +184,11 @@ namespace Robust.Shared.Maths
return !(a == b);
}
public Angle FlipPositive()
{
return new Angle(FlipPositive(Theta));
}
/// <summary>
/// Calculates the congruent positive angle of a negative angle. Does nothing to a positive angle.
/// </summary>

View File

@@ -330,6 +330,12 @@ namespace Robust.Shared.Maths
public static Matrix3 CreateTranslation(float x, float y)
{
var result = Identity;
/* column major
0 0 x
0 0 y
0 0 1
*/
result.R0C2 = x;
result.R1C2 = y;
return result;
@@ -352,14 +358,37 @@ namespace Robust.Shared.Maths
var result = Identity;
/* column major
cos -sin 0
sin cos 0
0 0 1
*/
result.R0C0 = cos;
result.R1C0 = sin;
result.R0C1 = -sin;
result.R1C1 = cos;
return result;
}
public static Matrix3 CreateScale(float x, float y)
{
var result = Identity;
/* column major
x 0 0
0 y 0
0 0 1
*/
result.R0C0 = x;
result.R1C1 = y;
return result;
}
public static Matrix3 CreateScale(in Vector2 scale)
{
return CreateScale(scale.X, scale.Y);
}
#endregion Constructors
#region Equality
@@ -604,7 +633,7 @@ namespace Robust.Shared.Maths
result.R2C2 = matrix.R2C0 * R0C2 + matrix.R2C1 * R1C2 + matrix.R2C2 * R2C2;
}
/// <summary>Multiply left matrix times left matrix.</summary>
/// <summary>Multiply left matrix times right matrix.</summary>
/// <param name="left">The matrix on the matrix side of the equation.</param>
/// <param name="right">The matrix on the right side of the equation</param>
/// <param name="result">The resulting matrix of the multiplication.</param>
@@ -798,11 +827,60 @@ namespace Robust.Shared.Maths
result.Z = R2C0 * vector.X + R2C1 * vector.Y + R2C2 * vector.Z;
}
public static void Transform(ref Matrix3 matrix, ref Vector3 vector, out Vector3 result)
/// <summary>
/// Post-multiplies a 3x3 matrix with a 3x1 vector.
/// </summary>
/// <param name="matrix">Matrix containing the transformation.</param>
/// <param name="vector">Input vector to transform.</param>
/// <param name="result">Transformed vector.</param>
public static void Transform(in Matrix3 matrix, in Vector3 vector, out Vector3 result)
{
result.X = matrix.R0C0 * vector.X + matrix.R0C1 * vector.Y + matrix.R0C2 * vector.Z;
result.Y = matrix.R1C0 * vector.X + matrix.R1C1 * vector.Y + matrix.R1C2 * vector.Z;
result.Z = matrix.R2C0 * vector.X + matrix.R2C1 * vector.Y + matrix.R2C2 * vector.Z;
var x = matrix.R0C0 * vector.X + matrix.R0C1 * vector.Y + matrix.R0C2 * vector.Z;
var y = matrix.R1C0 * vector.X + matrix.R1C1 * vector.Y + matrix.R1C2 * vector.Z;
var z = matrix.R2C0 * vector.X + matrix.R2C1 * vector.Y + matrix.R2C2 * vector.Z;
result = new Vector3(x, y, z);
}
/// <summary>
/// Post-multiplies a 3x3 matrix with a 2x1 vector. The column-major 3x3 matrix is treated as
/// a 3x2 matrix for this calculation.
/// </summary>
/// <param name="matrix">Matrix containing the transformation.</param>
/// <param name="vector">Input vector to transform.</param>
/// <param name="result">Transformed vector.</param>
public static void Transform(in Matrix3 matrix, in Vector2 vector, out Vector2 result)
{
var x = matrix.R0C0 * vector.X + matrix.R0C1 * vector.Y + matrix.R0C2;
var y = matrix.R1C0 * vector.X + matrix.R1C1 * vector.Y + matrix.R1C2;
result = new Vector2(x, y);
}
/// <summary>
/// Pre-multiples a 1x3 vector with a 3x3 matrix.
/// </summary>
/// <param name="matrix">Matrix containing the transformation.</param>
/// <param name="vector">Input vector to transform.</param>
/// <param name="result">Transformed vector.</param>
public static void Transform(in Vector3 vector, in Matrix3 matrix, out Vector3 result)
{
var x = (vector.X * matrix.R0C0) + (vector.Y * matrix.R1C0) + (vector.Z * matrix.R2C0);
var y = (vector.X * matrix.R0C1) + (vector.Y * matrix.R1C1) + (vector.Z * matrix.R2C1);
var z = (vector.X * matrix.R0C2) + (vector.Y * matrix.R1C2) + (vector.Z * matrix.R2C2);
result = new Vector3(x, y, z);
}
/// <summary>
/// Pre-multiples a 1x2 vector with a 3x3 matrix. The row-major 3x3 matrix is treated as
/// a 2x3 matrix for this calculation.
/// </summary>
/// <param name="matrix">Matrix containing the transformation.</param>
/// <param name="vector">Input vector to transform.</param>
/// <param name="result">Transformed vector.</param>
public static void Transform(in Vector2 vector, in Matrix3 matrix, out Vector2 result)
{
var x = (vector.X * matrix.R0C0) + (vector.Y * matrix.R1C0) + (matrix.R2C0);
var y = (vector.X * matrix.R0C1) + (vector.Y * matrix.R1C1) + (matrix.R2C1);
result = new Vector2(x, y);
}
public void Rotate(float angle)
@@ -895,6 +973,57 @@ namespace Robust.Shared.Maths
#endregion Transformation Functions
#region Operator Overloads
/// <summary>
/// Post-multiplies a 3x3 matrix with a 3x1 vector.
/// </summary>
/// <param name="matrix">Matrix containing the transformation.</param>
/// <param name="vector">Input vector to transform.</param>
/// <returns>Transformed vector.</returns>
public static Vector3 operator *(in Matrix3 matrix, in Vector3 vector)
{
Transform(in matrix, in vector, out var result);
return result;
}
/// <summary>
/// Post-multiplies a 3x3 matrix with a 2x1 vector. The 3x3 matrix is treated as
/// a 3x2 matrix for this calculation.
/// </summary>
/// <param name="matrix">Matrix containing the transformation.</param>
/// <param name="vector">Input vector to transform.</param>
/// <returns>Transformed vector.</returns>
public static Vector2 operator *(in Matrix3 matrix, in Vector2 vector)
{
Transform(in matrix, in vector, out var result);
return result;
}
public static Vector3 operator *(in Vector3 vector, in Matrix3 matrix)
{
Transform(in vector, in matrix, out var result);
return result;
}
public static Vector2 operator *(in Vector2 vector, in Matrix3 matrix)
{
Transform(in vector, in matrix, out var result);
return result;
}
/// <summary>Multiply left matrix times right matrix.</summary>
/// <param name="left">The matrix on the matrix side of the equation.</param>
/// <param name="right">The matrix on the right side of the equation</param>
/// <returns>The resulting matrix of the multiplication.</returns>
public static Matrix3 operator *(Matrix3 left, Matrix3 right)
{
Multiply(ref left, ref right, out var result);
return result;
}
#endregion
#region Constants
/// <summary>The identity matrix.</summary>

View File

@@ -1,5 +1,4 @@
using System;
using Robust.Shared.GameObjects;
using Robust.Shared.GameObjects;
using Robust.Shared.Map;
using Robust.Shared.Players;
@@ -146,4 +145,17 @@ namespace Robust.Shared.Input
return false;
}
}
/// <summary>
/// Consumes both up and down states without calling any handler delegates. Primarily used on the client to
/// prevent an input message from being sent to the server.
/// </summary>
public class NullInputCmdHandler : InputCmdHandler
{
/// <inheritdoc />
public override bool HandleCmdMessage(ICommonSession session, InputCmdMessage message)
{
return true;
}
}
}

View File

@@ -18,6 +18,9 @@ namespace Robust.Shared.Input
public static readonly BoundKeyFunction MoveRight = "MoveRight";
public static readonly BoundKeyFunction Run = "Run";
public static readonly BoundKeyFunction CameraRotateRight = "CameraRotateRight";
public static readonly BoundKeyFunction CameraRotateLeft = "CameraRotateLeft";
public static readonly BoundKeyFunction Use = "Use";
public static readonly BoundKeyFunction ShowDebugConsole = "ShowDebugConsole";

View File

@@ -0,0 +1,36 @@
using NUnit.Framework;
using Robust.Client.Graphics.ClientEye;
using Robust.Shared.Map;
using Robust.Shared.Maths;
// ReSharper disable AccessToStaticMemberViaDerivedType
namespace Robust.UnitTesting.Client.Graphics
{
[TestFixture, Parallelizable, TestOf(typeof(Eye))]
class EyeTest
{
[Test]
public void Constructor_DefaultZoom_isOne()
{
var eye = new Eye();
Assert.That(eye.Zoom, Is.Approximately(Vector2.One));
}
[Test]
public void Constructor_DefaultPosition_isZero()
{
var eye = new Eye();
Assert.That(eye.Position, Is.EqualTo(MapCoordinates.Nullspace));
}
[Test]
public void Constructor_DefaultDrawFov_isTrue()
{
var eye = new Eye();
Assert.That(eye.DrawFov, Is.True);
}
}
}

10
Robust.UnitTesting/Is.cs Normal file
View File

@@ -0,0 +1,10 @@
namespace Robust.UnitTesting
{
class Is : NUnit.Framework.Is
{
public static ApproxEqualityConstraint Approximately(object expected, double? tolerance = null)
{
return new ApproxEqualityConstraint(expected, tolerance);
}
}
}

View File

@@ -53,23 +53,21 @@ namespace Robust.UnitTesting.Shared.Maths
}
[Test]
public void MultiplyTest()
public void MultiplyTransformOrder()
{
var startPoint = new Vector3(2, 0, 1);
var rotateMatrix = Matrix3.CreateRotation((float)(System.Math.PI / 2.0)); // 1. rotate 90 degrees upward
var translateMatrix = Matrix3.CreateTranslation(new Vector2(0, -2)); // 2. translate 0, -2 downwards.
var startPoint = new Vector3(1, 0, 1);
var scaleMatrix = Matrix3.CreateScale(new Vector2(2, 2));
var rotateMatrix = Matrix3.CreateRotation((float)(System.Math.PI / 2.0));
var translateMatrix = Matrix3.CreateTranslation(new Vector2(-5, -3));
// NOTE: Matrix Product is NOT commutative. OpenTK (and this) uses pre-multiplication, OpenGL and all the tutorials
// you will read about it use post-multiplication. So in OpenTK MVP = M*V*P; in OpenGL it is MVP = P*V*M.
// OpenGL: TransformedVector = TranslationMatrix * RotationMatrix * ScaleMatrix * OriginalVector;
Matrix3.Multiply(ref rotateMatrix, ref translateMatrix, out var transformMatrix);
// 1. Take the start point -> ( 1, 0)
// 2. Scale it by 2 -> ( 2, 0)
// 3. Rotate by +90 degrees -> ( 0, 2)
// 4. Translate by (-5, -3) -> (-5,-1)
var result = (scaleMatrix * rotateMatrix * translateMatrix) * startPoint;
var result = startPoint;
Matrix3.Transform(transformMatrix, ref result);
// (2,0) -> (0,2) -> (0,0)
Assert.That(FloatMath.CloseTo(result.X, 0), Is.True, result.ToString);
Assert.That(FloatMath.CloseTo(result.Y, 0), Is.True, result.ToString);
Assert.That(result.X, Is.Approximately(-5f));
Assert.That(result.Y, Is.Approximately(-1f));
}
[Test]
@@ -133,10 +131,10 @@ namespace Robust.UnitTesting.Shared.Maths
Matrix3.Multiply(ref rotateMatrix, ref translateMatrix, out var transformMatrix);
// Act
Matrix3.Transform(ref transformMatrix, ref startPoint, out var localPoint);
Matrix3.Transform(in transformMatrix, in startPoint, out var localPoint);
var invMatrix = Matrix3.Invert(transformMatrix);
Matrix3.Transform(ref invMatrix, ref localPoint, out var result);
Matrix3.Transform(in invMatrix, in localPoint, out var result);
// Assert
Assert.That(FloatMath.CloseTo(startPoint.X, result.X), Is.True, result.ToString);