1
0

ScalingViewport.cs 12 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405
  1. using System;
  2. using System.Collections.Generic;
  3. using System.Numerics;
  4. using Robust.Client.Graphics;
  5. using Robust.Client.Input;
  6. using Robust.Client.UserInterface;
  7. using Robust.Client.UserInterface.CustomControls;
  8. using Robust.Shared.Graphics;
  9. using Robust.Shared.IoC;
  10. using Robust.Shared.Map;
  11. using Robust.Shared.Maths;
  12. using Robust.Shared.Timing;
  13. using Robust.Shared.Utility;
  14. using Robust.Shared.ViewVariables;
  15. using SixLabors.ImageSharp.PixelFormats;
  16. namespace Content.Client.Viewport
  17. {
  18. /// <summary>
  19. /// Viewport control that has a fixed viewport size and scales it appropriately.
  20. /// </summary>
  21. public sealed class ScalingViewport : Control, IViewportControl
  22. {
  23. [Dependency] private readonly IClyde _clyde = default!;
  24. [Dependency] private readonly IEntityManager _entityManager = default!;
  25. [Dependency] private readonly IInputManager _inputManager = default!;
  26. // Internal viewport creation is deferred.
  27. private IClydeViewport? _viewport;
  28. private IEye? _eye;
  29. private Vector2i _viewportSize;
  30. private int _curRenderScale;
  31. private ScalingViewportStretchMode _stretchMode = ScalingViewportStretchMode.Bilinear;
  32. private ScalingViewportRenderScaleMode _renderScaleMode = ScalingViewportRenderScaleMode.Fixed;
  33. private ScalingViewportIgnoreDimension _ignoreDimension = ScalingViewportIgnoreDimension.None;
  34. private int _fixedRenderScale = 1;
  35. private readonly List<CopyPixelsDelegate<Rgba32>> _queuedScreenshots = new();
  36. public int CurrentRenderScale => _curRenderScale;
  37. /// <summary>
  38. /// The eye to render.
  39. /// </summary>
  40. public IEye? Eye
  41. {
  42. get => _eye;
  43. set
  44. {
  45. _eye = value;
  46. if (_viewport != null)
  47. _viewport.Eye = value;
  48. }
  49. }
  50. /// <summary>
  51. /// The size, in unscaled pixels, of the internal viewport.
  52. /// </summary>
  53. /// <remarks>
  54. /// The actual viewport may have render scaling applied based on parameters.
  55. /// </remarks>
  56. public Vector2i ViewportSize
  57. {
  58. get => _viewportSize;
  59. set
  60. {
  61. _viewportSize = value;
  62. InvalidateViewport();
  63. }
  64. }
  65. // Do not need to InvalidateViewport() since it doesn't affect viewport creation.
  66. [ViewVariables(VVAccess.ReadWrite)] public Vector2i? FixedStretchSize { get; set; }
  67. [ViewVariables(VVAccess.ReadWrite)]
  68. public ScalingViewportStretchMode StretchMode
  69. {
  70. get => _stretchMode;
  71. set
  72. {
  73. _stretchMode = value;
  74. InvalidateViewport();
  75. }
  76. }
  77. [ViewVariables(VVAccess.ReadWrite)]
  78. public ScalingViewportRenderScaleMode RenderScaleMode
  79. {
  80. get => _renderScaleMode;
  81. set
  82. {
  83. _renderScaleMode = value;
  84. InvalidateViewport();
  85. }
  86. }
  87. [ViewVariables(VVAccess.ReadWrite)]
  88. public int FixedRenderScale
  89. {
  90. get => _fixedRenderScale;
  91. set
  92. {
  93. _fixedRenderScale = value;
  94. InvalidateViewport();
  95. }
  96. }
  97. [ViewVariables(VVAccess.ReadWrite)]
  98. public ScalingViewportIgnoreDimension IgnoreDimension
  99. {
  100. get => _ignoreDimension;
  101. set
  102. {
  103. _ignoreDimension = value;
  104. InvalidateViewport();
  105. }
  106. }
  107. public ScalingViewport()
  108. {
  109. IoCManager.InjectDependencies(this);
  110. RectClipContent = true;
  111. }
  112. protected override void KeyBindDown(GUIBoundKeyEventArgs args)
  113. {
  114. base.KeyBindDown(args);
  115. if (args.Handled)
  116. return;
  117. _inputManager.ViewportKeyEvent(this, args);
  118. }
  119. protected override void KeyBindUp(GUIBoundKeyEventArgs args)
  120. {
  121. base.KeyBindUp(args);
  122. if (args.Handled)
  123. return;
  124. _inputManager.ViewportKeyEvent(this, args);
  125. }
  126. protected override void Draw(IRenderHandle handle)
  127. {
  128. EnsureViewportCreated();
  129. DebugTools.AssertNotNull(_viewport);
  130. _viewport!.Render();
  131. if (_queuedScreenshots.Count != 0)
  132. {
  133. var callbacks = _queuedScreenshots.ToArray();
  134. _viewport.RenderTarget.CopyPixelsToMemory<Rgba32>(image =>
  135. {
  136. foreach (var callback in callbacks)
  137. {
  138. callback(image);
  139. }
  140. });
  141. _queuedScreenshots.Clear();
  142. }
  143. var drawBox = GetDrawBox();
  144. var drawBoxGlobal = drawBox.Translated(GlobalPixelPosition);
  145. _viewport.RenderScreenOverlaysBelow(handle, this, drawBoxGlobal);
  146. handle.DrawingHandleScreen.DrawTextureRect(_viewport.RenderTarget.Texture, drawBox);
  147. _viewport.RenderScreenOverlaysAbove(handle, this, drawBoxGlobal);
  148. }
  149. public void Screenshot(CopyPixelsDelegate<Rgba32> callback)
  150. {
  151. _queuedScreenshots.Add(callback);
  152. }
  153. // Draw box in pixel coords to draw the viewport at.
  154. private UIBox2i GetDrawBox()
  155. {
  156. DebugTools.AssertNotNull(_viewport);
  157. var vpSize = _viewport!.Size;
  158. var ourSize = (Vector2) PixelSize;
  159. if (FixedStretchSize == null)
  160. {
  161. var (ratioX, ratioY) = ourSize / vpSize;
  162. var ratio = 1f;
  163. switch (_ignoreDimension)
  164. {
  165. case ScalingViewportIgnoreDimension.None:
  166. ratio = Math.Min(ratioX, ratioY);
  167. break;
  168. case ScalingViewportIgnoreDimension.Vertical:
  169. ratio = ratioX;
  170. break;
  171. case ScalingViewportIgnoreDimension.Horizontal:
  172. ratio = ratioY;
  173. break;
  174. }
  175. var size = vpSize * ratio;
  176. // Size
  177. var pos = (ourSize - size) / 2;
  178. return (UIBox2i) UIBox2.FromDimensions(pos, size);
  179. }
  180. else
  181. {
  182. // Center only, no scaling.
  183. var pos = (ourSize - FixedStretchSize.Value) / 2;
  184. return (UIBox2i) UIBox2.FromDimensions(pos, FixedStretchSize.Value);
  185. }
  186. }
  187. private void RegenerateViewport()
  188. {
  189. DebugTools.AssertNull(_viewport);
  190. var vpSizeBase = ViewportSize;
  191. var ourSize = PixelSize;
  192. var (ratioX, ratioY) = ourSize / (Vector2) vpSizeBase;
  193. var ratio = Math.Min(ratioX, ratioY);
  194. var renderScale = 1;
  195. switch (_renderScaleMode)
  196. {
  197. case ScalingViewportRenderScaleMode.CeilInt:
  198. renderScale = (int) Math.Ceiling(ratio);
  199. break;
  200. case ScalingViewportRenderScaleMode.FloorInt:
  201. renderScale = (int) Math.Floor(ratio);
  202. break;
  203. case ScalingViewportRenderScaleMode.Fixed:
  204. renderScale = _fixedRenderScale;
  205. break;
  206. }
  207. // Always has to be at least one to avoid passing 0,0 to the viewport constructor
  208. renderScale = Math.Max(1, renderScale);
  209. _curRenderScale = renderScale;
  210. _viewport = _clyde.CreateViewport(
  211. ViewportSize * renderScale,
  212. new TextureSampleParameters
  213. {
  214. Filter = StretchMode == ScalingViewportStretchMode.Bilinear,
  215. });
  216. _viewport.RenderScale = new Vector2(renderScale, renderScale);
  217. _viewport.Eye = _eye;
  218. }
  219. protected override void Resized()
  220. {
  221. base.Resized();
  222. InvalidateViewport();
  223. }
  224. private void InvalidateViewport()
  225. {
  226. _viewport?.Dispose();
  227. _viewport = null;
  228. }
  229. public MapCoordinates ScreenToMap(Vector2 coords)
  230. {
  231. if (_eye == null)
  232. return default;
  233. EnsureViewportCreated();
  234. Matrix3x2.Invert(GetLocalToScreenMatrix(), out var matrix);
  235. coords = Vector2.Transform(coords, matrix);
  236. return _viewport!.LocalToWorld(coords);
  237. }
  238. /// <inheritdoc/>
  239. public MapCoordinates PixelToMap(Vector2 coords)
  240. {
  241. if (_eye == null)
  242. return default;
  243. EnsureViewportCreated();
  244. Matrix3x2.Invert(GetLocalToScreenMatrix(), out var matrix);
  245. coords = Vector2.Transform(coords, matrix);
  246. var ev = new PixelToMapEvent(coords, this, _viewport!);
  247. _entityManager.EventBus.RaiseEvent(EventSource.Local, ref ev);
  248. return _viewport!.LocalToWorld(ev.VisiblePosition);
  249. }
  250. public Vector2 WorldToScreen(Vector2 map)
  251. {
  252. if (_eye == null)
  253. return default;
  254. EnsureViewportCreated();
  255. var vpLocal = _viewport!.WorldToLocal(map);
  256. var matrix = GetLocalToScreenMatrix();
  257. return Vector2.Transform(vpLocal, matrix);
  258. }
  259. public Matrix3x2 GetWorldToScreenMatrix()
  260. {
  261. EnsureViewportCreated();
  262. return _viewport!.GetWorldToLocalMatrix() * GetLocalToScreenMatrix();
  263. }
  264. public Matrix3x2 GetLocalToScreenMatrix()
  265. {
  266. EnsureViewportCreated();
  267. var drawBox = GetDrawBox();
  268. var scaleFactor = drawBox.Size / (Vector2) _viewport!.Size;
  269. if (scaleFactor.X == 0 || scaleFactor.Y == 0)
  270. // Basically a nonsense scenario, at least make sure to return something that can be inverted.
  271. return Matrix3x2.Identity;
  272. return Matrix3Helpers.CreateTransform(GlobalPixelPosition + drawBox.TopLeft, 0, scaleFactor);
  273. }
  274. private void EnsureViewportCreated()
  275. {
  276. if (_viewport == null)
  277. {
  278. RegenerateViewport();
  279. }
  280. DebugTools.AssertNotNull(_viewport);
  281. }
  282. }
  283. /// <summary>
  284. /// Defines how the viewport is stretched if it does not match the size of the control perfectly.
  285. /// </summary>
  286. public enum ScalingViewportStretchMode
  287. {
  288. /// <summary>
  289. /// Bilinear sampling is used.
  290. /// </summary>
  291. Bilinear = 0,
  292. /// <summary>
  293. /// Nearest neighbor sampling is used.
  294. /// </summary>
  295. Nearest,
  296. }
  297. /// <summary>
  298. /// Defines how the base render scale of the viewport is selected.
  299. /// </summary>
  300. public enum ScalingViewportRenderScaleMode
  301. {
  302. /// <summary>
  303. /// <see cref="ScalingViewport.FixedRenderScale"/> is used.
  304. /// </summary>
  305. Fixed = 0,
  306. /// <summary>
  307. /// Floor to the closest integer scale possible.
  308. /// </summary>
  309. FloorInt,
  310. /// <summary>
  311. /// Ceiling to the closest integer scale possible.
  312. /// </summary>
  313. CeilInt
  314. }
  315. /// <summary>
  316. /// If the viewport is allowed to freely scale, this determines which dimensions should be ignored while fitting the viewport
  317. /// </summary>
  318. public enum ScalingViewportIgnoreDimension
  319. {
  320. /// <summary>
  321. /// The viewport won't ignore any dimension.
  322. /// </summary>
  323. None = 0,
  324. /// <summary>
  325. /// The viewport will ignore the horizontal dimension, and will exclusively consider the vertical dimension for scaling.
  326. /// </summary>
  327. Horizontal,
  328. /// <summary>
  329. /// The viewport will ignore the vertical dimension, and will exclusively consider the horizontal dimension for scaling.
  330. /// </summary>
  331. Vertical
  332. }
  333. }