OrthographicCameraTests.cs 39 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733734735736737738739740741742743744745746747748749750751752753754755756757758759760761762763764765766767768769770771772773774775776777778779780781782783784785786787788789790791792793794795796797798799800801802803804805806807808809810811812813814815816817818819820821822823824825826827828829830831832833834835836837838839840841842843844845846847848849850851852853854855856857858859860861862863864865866867868869870871872873874875876877878879880881882883884885886887888889890891892893894895896897898899900901902903904905906907908909910911912913914915916917918919920921922923924925926927928929930931932933934935936937938939940941942943944945946947948949950951952953954955956957958959960961962963964965966967968969970971972973974975976977978979980981982983984985986987988989990991992993994995996997998999100010011002100310041005100610071008100910101011101210131014101510161017101810191020102110221023102410251026102710281029
  1. using System;
  2. using Microsoft.Xna.Framework;
  3. using Microsoft.Xna.Framework.Graphics;
  4. using MonoGame.Extended.Tests.Fixtures;
  5. using MonoGame.Extended.ViewportAdapters;
  6. namespace MonoGame.Extended.Tests;
  7. [Collection("GraphicsTest")]
  8. public sealed class OrthographicCameraTests
  9. {
  10. private readonly GraphicsTestFixture _graphicsFixture;
  11. public OrthographicCameraTests(GraphicsTestFixture graphicsTestFixture)
  12. {
  13. _graphicsFixture = graphicsTestFixture;
  14. }
  15. [Fact]
  16. public void SetPosition_WorldBoundsDisabled_SetsValueWithoutClamping()
  17. {
  18. OrthographicCamera camera = new OrthographicCamera(_graphicsFixture.GraphicsDevice);
  19. camera.DisableWorldBounds();
  20. Vector2 expectedPosition = new Vector2(100, 100);
  21. camera.Position = expectedPosition;
  22. Assert.Equal(expectedPosition, camera.Position);
  23. }
  24. [Fact]
  25. public void SetPosition_WorldBoundsEnabled_ClampsToMinimumBounds()
  26. {
  27. OrthographicCamera camera = new OrthographicCamera(_graphicsFixture.GraphicsDevice);
  28. Viewport viewport = _graphicsFixture.GraphicsDevice.Viewport;
  29. Rectangle worldBounds = new Rectangle(0, 0, viewport.Width * 2, viewport.Height * 2);
  30. camera.EnableWorldBounds(worldBounds);
  31. camera.Position = new Vector2(-100, -100);
  32. Vector2 expectedPosition = new Vector2(0, 0);
  33. Assert.Equal(expectedPosition, camera.Position);
  34. }
  35. [Fact]
  36. public void SetPosition_WorldBoundsEnabled_ClampsToMaximumBounds()
  37. {
  38. OrthographicCamera camera = new OrthographicCamera(_graphicsFixture.GraphicsDevice);
  39. Viewport viewport = _graphicsFixture.GraphicsDevice.Viewport;
  40. Rectangle worldBounds = new Rectangle(0, 0, viewport.Width * 2, viewport.Height * 2);
  41. camera.EnableWorldBounds(worldBounds);
  42. camera.Position = new Vector2(viewport.Width, viewport.Height) * 3;
  43. Vector2 expectedPosition = new Vector2(worldBounds.Right - viewport.Width, worldBounds.Bottom - viewport.Height);
  44. Assert.Equal(expectedPosition, camera.Position);
  45. }
  46. [Fact]
  47. public void SetPosition_WorldBoundsEnabled_DoesNotClampWhenWithinBounds()
  48. {
  49. OrthographicCamera camera = new OrthographicCamera(_graphicsFixture.GraphicsDevice);
  50. Viewport viewport = _graphicsFixture.GraphicsDevice.Viewport;
  51. Rectangle worldBounds = new Rectangle(0, 0, viewport.Width * 2, viewport.Height * 2);
  52. camera.EnableWorldBounds(worldBounds);
  53. Vector2 expectedPosition = new Vector2(viewport.Width, viewport.Height);
  54. camera.Position = expectedPosition;
  55. Assert.Equal(expectedPosition, camera.Position);
  56. }
  57. [Fact]
  58. public void SetPosition_WorldBoundsSmallerThanCamera_CentersOnWorldBounds()
  59. {
  60. OrthographicCamera camera = new OrthographicCamera(_graphicsFixture.GraphicsDevice);
  61. Rectangle worldBounds = new Rectangle(100, 200, 50, 50);
  62. camera.EnableWorldBounds(worldBounds);
  63. camera.Position = new Vector2(1000, 1000);
  64. Vector2 expectedCenter = worldBounds.Center.ToVector2();
  65. Assert.Equal(expectedCenter, camera.Center);
  66. }
  67. [Fact]
  68. public void SetZoom_DefaultLimits_SetsValueWithoutClamping()
  69. {
  70. OrthographicCamera camera = new OrthographicCamera(_graphicsFixture.GraphicsDevice);
  71. camera.DisableWorldBounds();
  72. float expectedZoom = 2.0f;
  73. camera.Zoom = expectedZoom;
  74. Assert.Equal(expectedZoom, camera.Zoom);
  75. }
  76. [Fact]
  77. public void SetZoom_BelowMinimumZoom_ClampsToMinimum()
  78. {
  79. OrthographicCamera camera = new OrthographicCamera(_graphicsFixture.GraphicsDevice);
  80. camera.DisableWorldBounds();
  81. camera.MinimumZoom = 1.0f;
  82. camera.Zoom = 0.9f;
  83. Assert.Equal(camera.MinimumZoom, camera.Zoom);
  84. }
  85. [Fact]
  86. public void SetZoom_AboveMaximumZoom_ClampsToMaximum()
  87. {
  88. OrthographicCamera camera = new OrthographicCamera(_graphicsFixture.GraphicsDevice);
  89. camera.DisableWorldBounds();
  90. camera.MaximumZoom = 1.0f;
  91. camera.Zoom = 1.1f;
  92. Assert.Equal(camera.MaximumZoom, camera.Zoom);
  93. }
  94. [Fact]
  95. public void SetZoom_WorldBoundsEnabled_BelowMinimumWorldBoundsZoom_ClampsToWorldBounds()
  96. {
  97. OrthographicCamera camera = new OrthographicCamera(_graphicsFixture.GraphicsDevice);
  98. Viewport viewport = _graphicsFixture.GraphicsDevice.Viewport;
  99. Rectangle worldBounds = new Rectangle(0, 0, viewport.Width * 2, viewport.Height * 2);
  100. camera.EnableWorldBounds(worldBounds);
  101. camera.IsZoomClampedToWorldBounds = true;
  102. // Viewport for testing is 800x480, so world bounds are 1600x960
  103. // Minimum zoom to keep view within bounds: max(800/1600, 480/960) = max(0.5, 0.5) = 0.5
  104. // So a zoom at 0.5 is at the world bounds minium, so we set lower than that to check clamping.
  105. camera.Zoom = 0.3f;
  106. Assert.Equal(0.5f, camera.Zoom);
  107. }
  108. [Fact]
  109. public void SetZoom_WorldBoundsEnabled_AboveMaximumWorldBoundsZoom_ClampsToWorldBounds()
  110. {
  111. OrthographicCamera camera = new OrthographicCamera(_graphicsFixture.GraphicsDevice);
  112. Viewport viewport = _graphicsFixture.GraphicsDevice.Viewport;
  113. Rectangle worldBounds = new Rectangle(0, 0, viewport.Width * 2, viewport.Height * 2);
  114. camera.EnableWorldBounds(worldBounds);
  115. camera.IsZoomClampedToWorldBounds = true;
  116. // Viewport for testing is 800x480, so world bounds are 1600x960
  117. // Minimum zoom to keep view within bounds: max(800/1600, 480/960) = max(0.5, 0.5) = 0.5
  118. // So a zoom at 0.5 is at the world bounds minium, so we set lower than that to check clamping.
  119. camera.Zoom = 0.3f;
  120. Assert.Equal(0.5f, camera.Zoom);
  121. }
  122. [Fact]
  123. public void SetZoom_ExplicitMaximumZoom_TakesPrecedenceOverWorldBoundsMinimum()
  124. {
  125. OrthographicCamera camera = new OrthographicCamera(_graphicsFixture.GraphicsDevice);
  126. Viewport viewport = _graphicsFixture.GraphicsDevice.Viewport;
  127. Rectangle worldBounds = new Rectangle(0, 0, viewport.Width * 2, viewport.Height * 2);
  128. camera.EnableWorldBounds(worldBounds);
  129. camera.IsZoomClampedToWorldBounds = true;
  130. // Set explicit maximum BELOW what world bounds minimum requires (0.5)
  131. camera.MaximumZoom = 0.4f;
  132. // Try to set zoom to world bounds minimum
  133. camera.Zoom = 0.5f;
  134. // Explicit MaximumZoom should take precedence
  135. Assert.Equal(0.4f, camera.Zoom);
  136. }
  137. [Fact]
  138. public void SetZoom_WorldBoundsEnabled_ClampsPositionAfterZoomChange()
  139. {
  140. OrthographicCamera camera = new OrthographicCamera(_graphicsFixture.GraphicsDevice);
  141. Viewport viewport = _graphicsFixture.GraphicsDevice.Viewport;
  142. Rectangle worldBounds = new Rectangle(0, 0, viewport.Width, viewport.Height);
  143. // Position camera at edge of world bounds
  144. camera.Position = new Vector2(viewport.Width, viewport.Height);
  145. camera.EnableWorldBounds(worldBounds);
  146. camera.IsZoomClampedToWorldBounds = true;
  147. // Zoom out
  148. // this should force position adjustment to keep view in bounds
  149. camera.Zoom = 0.5f;
  150. // Zoom clamped to 1.0 (camera sees 800×480, same as world bounds)
  151. Assert.Equal(1.0f, camera.Zoom);
  152. // With zoom 1.0 and world bounds = viewport size, only valid position is (0, 0)
  153. Assert.Equal(0f, camera.Position.X, 2);
  154. Assert.Equal(0f, camera.Position.Y, 2);
  155. }
  156. [Fact]
  157. public void SetMinimumZoom_AboveCurrentZoom_ClampsCurrentZoom()
  158. {
  159. OrthographicCamera camera = new OrthographicCamera(_graphicsFixture.GraphicsDevice);
  160. camera.Zoom = 0.5f;
  161. camera.MinimumZoom = 1.0f;
  162. Assert.Equal(1.0f, camera.Zoom);
  163. }
  164. [Fact]
  165. public void SetMaximumZoom_BelowCurrentZoom_ClampsCurrentZoom()
  166. {
  167. OrthographicCamera camera = new OrthographicCamera(_graphicsFixture.GraphicsDevice);
  168. camera.Zoom = 2.0f;
  169. camera.MaximumZoom = 1.0f;
  170. Assert.Equal(1.0f, camera.Zoom);
  171. }
  172. [Fact]
  173. public void SetMinimumZoom_Negative_ThrowsArgumentOutOfRangeException()
  174. {
  175. OrthographicCamera camera = new OrthographicCamera(_graphicsFixture.GraphicsDevice);
  176. Assert.Throws<ArgumentOutOfRangeException>(() => camera.MinimumZoom = -1.0f);
  177. }
  178. [Fact]
  179. public void SetMaximumZoom_Negative_ThrowsArgumentOutOfRangeException()
  180. {
  181. OrthographicCamera camera = new OrthographicCamera(_graphicsFixture.GraphicsDevice);
  182. Assert.Throws<ArgumentOutOfRangeException>(() => camera.MaximumZoom = -1.0f);
  183. }
  184. [Fact]
  185. public void SetPitch_BelowMinimum_ClampsToMinimum()
  186. {
  187. OrthographicCamera camera = new OrthographicCamera(_graphicsFixture.GraphicsDevice);
  188. camera.MinimumPitch = 1.0f;
  189. camera.Pitch = 0.9f;
  190. Assert.Equal(camera.MinimumPitch, camera.Pitch);
  191. }
  192. [Fact]
  193. public void SetPitch_AboveMaximum_ClampsToMaximum()
  194. {
  195. OrthographicCamera camera = new OrthographicCamera(_graphicsFixture.GraphicsDevice);
  196. camera.MaximumPitch = 1.0f;
  197. camera.Pitch = 1.1f;
  198. Assert.Equal(camera.MaximumPitch, camera.Pitch);
  199. }
  200. [Fact]
  201. public void SetMinimumPitch_Negative_ThrowsArgumentOutOfRangeException()
  202. {
  203. OrthographicCamera camera = new OrthographicCamera(_graphicsFixture.GraphicsDevice);
  204. Assert.Throws<ArgumentOutOfRangeException>(() => camera.MinimumPitch = -0.01f);
  205. }
  206. [Fact]
  207. public void SetMaximumPitch_Negative_ThrowsArgumentOutOfRangeException()
  208. {
  209. OrthographicCamera camera = new OrthographicCamera(_graphicsFixture.GraphicsDevice);
  210. Assert.Throws<ArgumentOutOfRangeException>(() => camera.MaximumPitch = -0.01f);
  211. }
  212. [Fact]
  213. public void BoundingRectangle_WithMovement_ReturnsCorrectBounds()
  214. {
  215. DefaultViewportAdapter viewportAdapter = new DefaultViewportAdapter(_graphicsFixture.GraphicsDevice);
  216. OrthographicCamera camera = new OrthographicCamera(viewportAdapter);
  217. // Move right 2, then down 3
  218. Vector2 movement = new Vector2(2, 3);
  219. camera.Move(new Vector2(movement.X, 0));
  220. camera.Move(new Vector2(0, movement.Y));
  221. RectangleF boundingRectangle = camera.BoundingRectangle;
  222. Assert.Equal(movement.X, boundingRectangle.Left, 2);
  223. Assert.Equal(movement.Y, boundingRectangle.Top, 2);
  224. Assert.Equal(movement.X + viewportAdapter.VirtualWidth, boundingRectangle.Right, 2);
  225. Assert.Equal(movement.Y + viewportAdapter.VirtualHeight, boundingRectangle.Bottom, 2);
  226. }
  227. [Fact]
  228. public void BoundingRectangle_WithZoom_ReturnsCorrectBounds()
  229. {
  230. OrthographicCamera camera = new OrthographicCamera(_graphicsFixture.GraphicsDevice);
  231. camera.Zoom = 2f;
  232. RectangleF boundingRectangle = camera.BoundingRectangle;
  233. // With 2x zoom on 800x480 viewport, camera sees 400x240 area
  234. Assert.Equal(400f, boundingRectangle.Width, 2);
  235. Assert.Equal(240f, boundingRectangle.Height, 2);
  236. }
  237. [Fact]
  238. public void ContainsPoint_WithDefaultCamera_ReturnsCorrectContainment()
  239. {
  240. OrthographicCamera camera = new OrthographicCamera(_graphicsFixture.GraphicsDevice);
  241. Viewport viewport = _graphicsFixture.GraphicsDevice.Viewport;
  242. Assert.Equal(ContainmentType.Contains, camera.Contains(new Point(1, 1)));
  243. Assert.Equal(ContainmentType.Contains, camera.Contains(new Point(viewport.Width - 1, viewport.Height - 1)));
  244. Assert.Equal(ContainmentType.Disjoint, camera.Contains(new Point(-1, -1)));
  245. Assert.Equal(ContainmentType.Disjoint, camera.Contains(new Point(viewport.Width + 1, viewport.Height + 1)));
  246. }
  247. [Fact]
  248. public void ContainsVector2_WithDefaultCamera_ReturnsCorrectContainment()
  249. {
  250. OrthographicCamera camera = new OrthographicCamera(_graphicsFixture.GraphicsDevice);
  251. Viewport viewport = _graphicsFixture.GraphicsDevice.Viewport;
  252. Assert.Equal(ContainmentType.Contains, camera.Contains(new Vector2(viewport.Width - 0.5f, viewport.Height - 0.5f)));
  253. Assert.Equal(ContainmentType.Contains, camera.Contains(new Vector2(0.5f, 0.5f)));
  254. Assert.Equal(ContainmentType.Disjoint, camera.Contains(new Vector2(-0.5f, -0.5f)));
  255. Assert.Equal(ContainmentType.Disjoint, camera.Contains(new Vector2(viewport.Width + 0.5f, viewport.Height + 0.5f)));
  256. Assert.Equal(ContainmentType.Disjoint, camera.Contains(new Vector2(-0.5f, viewport.Height / 2f)));
  257. Assert.Equal(ContainmentType.Contains, camera.Contains(new Vector2(0.5f, viewport.Height / 2f)));
  258. Assert.Equal(ContainmentType.Contains, camera.Contains(new Vector2(viewport.Width - 0.5f, viewport.Height / 2f)));
  259. Assert.Equal(ContainmentType.Disjoint, camera.Contains(new Vector2(viewport.Width + 0.5f, viewport.Height / 2f)));
  260. }
  261. [Fact]
  262. public void ContainsRectangle_WithDefaultCamera_ReturnsCorrectContainment()
  263. {
  264. OrthographicCamera camera = new OrthographicCamera(_graphicsFixture.GraphicsDevice);
  265. Assert.Equal(ContainmentType.Intersects, camera.Contains(new Rectangle(-50, -50, 100, 100)));
  266. Assert.Equal(ContainmentType.Contains, camera.Contains(new Rectangle(50, 50, 100, 100)));
  267. Assert.Equal(ContainmentType.Disjoint, camera.Contains(new Rectangle(850, 500, 100, 100)));
  268. }
  269. [Fact]
  270. public void ContainsRectangle_FullyContained_ReturnsContains()
  271. {
  272. OrthographicCamera camera = new OrthographicCamera(_graphicsFixture.GraphicsDevice);
  273. Rectangle fullyContainedRect = new Rectangle(100, 100, 200, 200);
  274. ContainmentType result = camera.Contains(fullyContainedRect);
  275. Assert.Equal(ContainmentType.Contains, result);
  276. }
  277. [Fact]
  278. public void ContainsRectangle_PartiallyContained_ReturnsIntersects()
  279. {
  280. OrthographicCamera camera = new OrthographicCamera(_graphicsFixture.GraphicsDevice);
  281. Rectangle partiallyContainedRect = new Rectangle(-50, -50, 100, 100);
  282. ContainmentType result = camera.Contains(partiallyContainedRect);
  283. Assert.Equal(ContainmentType.Intersects, result);
  284. }
  285. [Fact]
  286. public void EnableWorldBounds_SetsWorldBoundsAndFlag()
  287. {
  288. OrthographicCamera camera = new OrthographicCamera(_graphicsFixture.GraphicsDevice);
  289. Rectangle worldBounds = new Rectangle(0, 0, 800, 600);
  290. camera.EnableWorldBounds(worldBounds);
  291. Assert.Equal(worldBounds, camera.WorldBounds);
  292. Assert.True(camera.IsClampedToWorldBounds);
  293. }
  294. [Fact]
  295. public void DisableWorldBounds_ClearsWorldBoundsAndFlag()
  296. {
  297. OrthographicCamera camera = new OrthographicCamera(_graphicsFixture.GraphicsDevice);
  298. camera.EnableWorldBounds(new Rectangle(0, 0, 800, 600));
  299. camera.DisableWorldBounds();
  300. Assert.Equal(Rectangle.Empty, camera.WorldBounds);
  301. Assert.False(camera.IsClampedToWorldBounds);
  302. }
  303. [Fact]
  304. public void Move_WithoutRotation_TranslatesPosition()
  305. {
  306. OrthographicCamera camera = new OrthographicCamera(_graphicsFixture.GraphicsDevice);
  307. Vector2 originalPosition = camera.Position;
  308. Vector2 movement = new Vector2(10, 20);
  309. camera.Move(movement);
  310. Assert.Equal(originalPosition + movement, camera.Position);
  311. }
  312. [Fact]
  313. public void Move_WithRotation_TranslatesPositionRelativeToRotation()
  314. {
  315. OrthographicCamera camera = new OrthographicCamera(_graphicsFixture.GraphicsDevice);
  316. // 90 degrees
  317. camera.Rotation = MathHelper.PiOver2;
  318. // Move right in world space
  319. Vector2 movement = new Vector2(10, 0);
  320. camera.Move(movement);
  321. // With 90 degree rotation, moving "right" should actually move "up" in screen space
  322. // The movement is transformed by the inverse rotation
  323. Vector2 expectedMovement = Vector2.Transform(movement, Matrix.CreateRotationZ(-camera.Rotation));
  324. Assert.Equal(expectedMovement.X, camera.Position.X, 3);
  325. Assert.Equal(expectedMovement.Y, camera.Position.Y, 3);
  326. }
  327. [Fact]
  328. public void Rotate_IncreasesRotation()
  329. {
  330. OrthographicCamera camera = new OrthographicCamera(_graphicsFixture.GraphicsDevice);
  331. var deltaRotation = MathHelper.PiOver4;
  332. camera.Rotate(deltaRotation);
  333. Assert.Equal(deltaRotation, camera.Rotation, 5);
  334. }
  335. [Fact]
  336. public void ZoomIn_IncreasesZoom()
  337. {
  338. OrthographicCamera camera = new OrthographicCamera(_graphicsFixture.GraphicsDevice);
  339. float originalZoom = camera.Zoom;
  340. camera.ZoomIn(1);
  341. Assert.Equal(originalZoom + 1, camera.Zoom);
  342. }
  343. [Fact]
  344. public void ZoomOut_DecreasesZoom()
  345. {
  346. OrthographicCamera camera = new OrthographicCamera(_graphicsFixture.GraphicsDevice);
  347. float originalZoom = camera.Zoom;
  348. camera.ZoomOut(1);
  349. Assert.Equal(originalZoom - 1, camera.Zoom);
  350. }
  351. [Fact]
  352. public void PitchUp_IncreasesPitch()
  353. {
  354. OrthographicCamera camera = new OrthographicCamera(_graphicsFixture.GraphicsDevice);
  355. float originalPitch = camera.Pitch;
  356. camera.PitchUp(1);
  357. Assert.Equal(originalPitch + 1, camera.Pitch);
  358. }
  359. [Fact]
  360. public void PitchDown_DecreasesPitch()
  361. {
  362. OrthographicCamera camera = new OrthographicCamera(_graphicsFixture.GraphicsDevice);
  363. float originalPitch = camera.Pitch;
  364. camera.PitchDown(1);
  365. Assert.Equal(originalPitch - 1, camera.Pitch);
  366. }
  367. [Fact]
  368. public void LookAt_SetsPositionCorrectly()
  369. {
  370. DefaultViewportAdapter viewportAdapter = new DefaultViewportAdapter(_graphicsFixture.GraphicsDevice);
  371. OrthographicCamera camera = new OrthographicCamera(viewportAdapter);
  372. Vector2 targetPosition = new Vector2(100, 200);
  373. camera.LookAt(targetPosition);
  374. Vector2 expectedPosition = targetPosition - new Vector2(viewportAdapter.VirtualWidth, viewportAdapter.VirtualHeight) * 0.5f;
  375. Assert.Equal(expectedPosition, camera.Position);
  376. }
  377. [Fact]
  378. public void ScreenToWorld_WithCameraMovement_TransformsCorrectly()
  379. {
  380. OrthographicCamera camera = new OrthographicCamera(_graphicsFixture.GraphicsDevice);
  381. camera.Position = new Vector2(100, 200);
  382. // Screen position at origin
  383. Vector2 screenPosition = Vector2.Zero;
  384. Vector2 worldPosition = camera.ScreenToWorld(screenPosition);
  385. // World position should account for camera offset
  386. Assert.Equal(100, worldPosition.X, 2);
  387. Assert.Equal(200, worldPosition.Y, 2);
  388. }
  389. [Fact]
  390. public void WorldToScreen_WithCameraMovement_TransformsCorrectly()
  391. {
  392. OrthographicCamera camera = new OrthographicCamera(_graphicsFixture.GraphicsDevice);
  393. camera.Position = new Vector2(100, 200);
  394. // World position at camera position
  395. Vector2 worldPosition = new Vector2(100, 200);
  396. Vector2 screenPosition = camera.WorldToScreen(worldPosition);
  397. // Should appear at screen origin
  398. Assert.Equal(0, screenPosition.X, 2);
  399. Assert.Equal(0, screenPosition.Y, 2);
  400. }
  401. [Fact]
  402. public void ScreenToWorld_WithZoom_TransformsCorrectly()
  403. {
  404. OrthographicCamera camera = new OrthographicCamera(_graphicsFixture.GraphicsDevice);
  405. camera.Zoom = 2.0f;
  406. Vector2 screenPosition = new Vector2(100, 100);
  407. Vector2 worldPosition = camera.ScreenToWorld(screenPosition);
  408. // With default camera:
  409. // - Origin is at (400, 240) - the viewport center
  410. // - Position is at (0, 0)
  411. // - Screen (100, 100) is 300px left and 140px up from Origin
  412. // - With 2x zoom: world offset is (300/2, 140/2) = (150, 70) from Origin
  413. // - World position: Origin - offset = (400 - 150, 240 - 70) = (250, 170)
  414. Assert.Equal(250, worldPosition.X, 2);
  415. Assert.Equal(170, worldPosition.Y, 2);
  416. }
  417. [Fact]
  418. public void WorldToScreen_WithZoom_TransformsCorrectly()
  419. {
  420. OrthographicCamera camera = new OrthographicCamera(_graphicsFixture.GraphicsDevice);
  421. camera.Zoom = 2.0f;
  422. Vector2 worldPosition = new Vector2(100, 100);
  423. Vector2 screenPosition = camera.WorldToScreen(worldPosition);
  424. // With default camera:
  425. // - Origin is at (400, 240) - the viewport center
  426. // - Position is at (0, 0)
  427. // - Camera.Center is at (400, 240)
  428. // - World (100, 100) is 300px left and 140px up from Camera.Center
  429. // - With 2x zoom: screen offset is (300 * 2, 140 * 2) = (600, 280) from Origin
  430. // - Screen position: Origin - offset = (400 - 600, 240 - 280) = (-200, -40)
  431. Assert.Equal(-200, screenPosition.X, 2);
  432. Assert.Equal(-40, screenPosition.Y, 2);
  433. }
  434. [Fact]
  435. public void WorldToScreen_RoundTrip_ReturnsOriginalPosition()
  436. {
  437. OrthographicCamera camera = new OrthographicCamera(_graphicsFixture.GraphicsDevice);
  438. camera.Position = new Vector2(100, 200);
  439. camera.Zoom = 1.5f;
  440. Vector2 originalWorld = new Vector2(250, 350);
  441. Vector2 screen = camera.WorldToScreen(originalWorld);
  442. Vector2 backToWorld = camera.ScreenToWorld(screen);
  443. Assert.Equal(originalWorld.X, backToWorld.X, 2);
  444. Assert.Equal(originalWorld.Y, backToWorld.Y, 2);
  445. }
  446. [Fact]
  447. public void GetViewMatrix_ReturnsValidMatrix()
  448. {
  449. OrthographicCamera camera = new OrthographicCamera(_graphicsFixture.GraphicsDevice);
  450. Matrix viewMatrix = camera.GetViewMatrix();
  451. Assert.Equal(Matrix.Identity, viewMatrix);
  452. }
  453. [Fact]
  454. public void GetInverseViewMatrix_IsInverseOfViewMatrix()
  455. {
  456. OrthographicCamera camera = new OrthographicCamera(_graphicsFixture.GraphicsDevice);
  457. camera.Position = new Vector2(10, 20);
  458. camera.Rotation = MathHelper.PiOver4;
  459. camera.Zoom = 2f;
  460. Matrix viewMatrix = camera.GetViewMatrix();
  461. Matrix inverseViewMatrix = camera.GetInverseViewMatrix();
  462. Matrix shouldBeIdentity = Matrix.Multiply(viewMatrix, inverseViewMatrix);
  463. // Check if the result is close to identity matrix
  464. AssertExtensions.Equal(Matrix.Identity, shouldBeIdentity, 3);
  465. }
  466. [Fact]
  467. public void GetViewMatrix_WithParallaxFactor_ReturnsValidMatrix()
  468. {
  469. OrthographicCamera camera = new OrthographicCamera(_graphicsFixture.GraphicsDevice);
  470. // Set non-zero position to make parallax effect visible
  471. camera.Position = new Vector2(100, 50);
  472. Vector2 parallaxFactor = new Vector2(0.5f, 0.5f);
  473. Matrix viewMatrix = camera.GetViewMatrix(parallaxFactor);
  474. Assert.NotEqual(Matrix.Identity, viewMatrix);
  475. }
  476. [Fact]
  477. public void GetViewMatrix_WithParallaxFactor_AppliesCorrectTransformation()
  478. {
  479. OrthographicCamera camera = new OrthographicCamera(_graphicsFixture.GraphicsDevice);
  480. camera.Position = new Vector2(100, 60);
  481. Vector2 parallaxFactor = new Vector2(0.5f, 0.25f);
  482. Matrix parallaxMatrix = camera.GetViewMatrix(parallaxFactor);
  483. // Default parallax factor of (1,1)
  484. Matrix normalMatrix = camera.GetViewMatrix();
  485. // The matrices should be different when parallax factor is not (1,1) and position is not zero
  486. Assert.NotEqual(normalMatrix, parallaxMatrix);
  487. Assert.NotEqual(Matrix.Identity, parallaxMatrix);
  488. Assert.NotEqual(Matrix.Identity, normalMatrix);
  489. // With position (100, 60) and parallax (0.5, 0.25):
  490. // Expected translation = -(100 * 0.5, 60 * 0.25) = (-50, -15)
  491. // This should be reflected in the view matrix M41, M42 values
  492. Assert.Equal(-50f, parallaxMatrix.M41, 1);
  493. Assert.Equal(-15f, parallaxMatrix.M42, 1);
  494. }
  495. [Fact]
  496. public void GetBoundingFrustum_ReturnsValidFrustum()
  497. {
  498. var camera = new OrthographicCamera(_graphicsFixture.GraphicsDevice);
  499. Viewport viewport = _graphicsFixture.GraphicsDevice.Viewport;
  500. BoundingFrustum boundingFrustum = camera.GetBoundingFrustum();
  501. Vector3[] corners = boundingFrustum.GetCorners();
  502. // Verify we have 8 corners (standard frustum)
  503. Assert.Equal(8, corners.Length);
  504. // Check near plane corners (Z = 1)
  505. Assert.Equal(0, corners[0].X, 2);
  506. Assert.Equal(0, corners[0].Y, 2);
  507. Assert.Equal(1, corners[0].Z, 2);
  508. Assert.Equal(viewport.Width, corners[1].X, 2);
  509. Assert.Equal(0, corners[1].Y, 2);
  510. Assert.Equal(1, corners[1].Z, 2);
  511. Assert.Equal(viewport.Width, corners[2].X, 2);
  512. Assert.Equal(viewport.Height, corners[2].Y, 2);
  513. Assert.Equal(1, corners[2].Z, 2);
  514. Assert.Equal(0, corners[3].X, 2);
  515. Assert.Equal(viewport.Height, corners[3].Y, 2);
  516. Assert.Equal(1, corners[3].Z, 2);
  517. // Check far plane corners (Z = 0)
  518. Assert.Equal(0, corners[4].X, 2);
  519. Assert.Equal(0, corners[4].Y, 2);
  520. Assert.Equal(0, corners[4].Z, 2);
  521. Assert.Equal(viewport.Width, corners[5].X, 2);
  522. Assert.Equal(0, corners[5].Y, 2);
  523. Assert.Equal(0, corners[5].Z, 2);
  524. Assert.Equal(viewport.Width, corners[6].X, 2);
  525. Assert.Equal(viewport.Height, corners[6].Y, 2);
  526. Assert.Equal(0, corners[6].Z, 2);
  527. Assert.Equal(0, corners[7].X, 2);
  528. Assert.Equal(viewport.Height, corners[7].Y, 2);
  529. Assert.Equal(0, corners[7].Z, 2);
  530. }
  531. [Fact]
  532. public void ZoomIn_WithZoomCenter_KeepsZoomCenterFixedOnScreen()
  533. {
  534. OrthographicCamera camera = new OrthographicCamera(_graphicsFixture.GraphicsDevice);
  535. camera.Position = Vector2.Zero;
  536. camera.Zoom = 1.0f;
  537. Vector2 zoomCenter = new Vector2(100, 100);
  538. Vector2 screenBefore = camera.WorldToScreen(zoomCenter);
  539. camera.ZoomIn(0.5f, zoomCenter);
  540. Vector2 screenAfter = camera.WorldToScreen(zoomCenter);
  541. Assert.Equal(screenBefore.X, screenAfter.X, 1);
  542. Assert.Equal(screenBefore.Y, screenAfter.Y, 1);
  543. }
  544. [Fact]
  545. public void ZoomOut_WithZoomCenter_KeepsZoomCenterFixedOnScreen()
  546. {
  547. OrthographicCamera camera = new OrthographicCamera(_graphicsFixture.GraphicsDevice);
  548. camera.Position = Vector2.Zero;
  549. camera.Zoom = 2.0f;
  550. Vector2 zoomCenter = new Vector2(100, 100);
  551. Vector2 screenBefore = camera.WorldToScreen(zoomCenter);
  552. camera.ZoomOut(0.5f, zoomCenter);
  553. Vector2 screenAfter = camera.WorldToScreen(zoomCenter);
  554. Assert.Equal(screenBefore.X, screenAfter.X, 1);
  555. Assert.Equal(screenBefore.Y, screenAfter.Y, 1);
  556. }
  557. [Fact]
  558. public void ZoomIn_WithZoomCenter_ClampedByMinimumZoom_DoesNotAdjustPosition()
  559. {
  560. OrthographicCamera camera = new OrthographicCamera(_graphicsFixture.GraphicsDevice);
  561. camera.MaximumZoom = 2.0f;
  562. camera.Zoom = camera.MaximumZoom;
  563. camera.Position = new Vector2(50, 50);
  564. Vector2 zoomCenter = new Vector2(100, 100);
  565. Vector2 positionBefore = camera.Position;
  566. camera.ZoomIn(1.0f, zoomCenter);
  567. Assert.Equal(2.0f, camera.Zoom);
  568. Assert.Equal(positionBefore, camera.Position);
  569. }
  570. [Fact]
  571. public void ZoomOut_WithZoomCenter_ClampedByMinimumZoom_DoesNotAdjustPosition()
  572. {
  573. OrthographicCamera camera = new OrthographicCamera(_graphicsFixture.GraphicsDevice);
  574. camera.MinimumZoom = 0.5f;
  575. camera.Zoom = camera.MinimumZoom;
  576. camera.Position = new Vector2(50, 50);
  577. Vector2 zoomCenter = new Vector2(100, 100);
  578. Vector2 positionBefore = camera.Position;
  579. camera.ZoomOut(1.0f, zoomCenter);
  580. Assert.Equal(0.5f, camera.Zoom);
  581. Assert.Equal(positionBefore, camera.Position);
  582. }
  583. [Fact]
  584. public void ZoomIn_WithZoomCenter_AtOrigin_AdjustsPositionCorrectly()
  585. {
  586. OrthographicCamera camera = new OrthographicCamera(_graphicsFixture.GraphicsDevice);
  587. camera.Position = new Vector2(100, 100);
  588. camera.Zoom = 1.0f;
  589. Vector2 zoomCenter = camera.Origin;
  590. Vector2 screenBefore = camera.WorldToScreen(zoomCenter);
  591. camera.ZoomIn(0.5f, zoomCenter);
  592. Vector2 screenAfter = camera.WorldToScreen(zoomCenter);
  593. Assert.Equal(screenBefore.X, screenAfter.X, 1);
  594. Assert.Equal(screenBefore.Y, screenAfter.Y, 1);
  595. }
  596. [Fact]
  597. public void ZoomOut_WithZoomCenter_AtOrigin_AdjustsPositionCorrectly()
  598. {
  599. OrthographicCamera camera = new OrthographicCamera(_graphicsFixture.GraphicsDevice);
  600. camera.Position = new Vector2(100, 100);
  601. camera.Zoom = 2.0f;
  602. Vector2 zoomCenter = camera.Origin;
  603. Vector2 screenBefore = camera.WorldToScreen(zoomCenter);
  604. camera.ZoomOut(0.5f, zoomCenter);
  605. Vector2 screenAfter = camera.WorldToScreen(zoomCenter);
  606. Assert.Equal(screenBefore.X, screenAfter.X, 1);
  607. Assert.Equal(screenBefore.Y, screenAfter.Y, 1);
  608. }
  609. [Fact]
  610. public void ZoomIn_WithZoomCenter_WorldBoundsEnabled_RespectsBounds()
  611. {
  612. OrthographicCamera camera = new OrthographicCamera(_graphicsFixture.GraphicsDevice);
  613. Viewport viewport = _graphicsFixture.GraphicsDevice.Viewport;
  614. Rectangle worldBounds = new Rectangle(0, 0, viewport.Width * 2, viewport.Height * 2);
  615. camera.EnableWorldBounds(worldBounds);
  616. camera.Position = new Vector2(viewport.Width / 2, viewport.Height / 2);
  617. camera.Zoom = 1.0f;
  618. Vector2 zoomCenter = new Vector2(200, 200);
  619. camera.ZoomIn(0.5f, zoomCenter);
  620. Assert.True(camera.Position.X >= 0);
  621. Assert.True(camera.Position.Y >= 0);
  622. Assert.True(camera.Position.X <= worldBounds.Right - viewport.Width / camera.Zoom);
  623. Assert.True(camera.Position.Y <= worldBounds.Bottom - viewport.Height / camera.Zoom);
  624. }
  625. [Fact]
  626. public void ZoomOut_WithZoomCenter_WorldBoundsEnabled_RespectsBounds()
  627. {
  628. OrthographicCamera camera = new OrthographicCamera(_graphicsFixture.GraphicsDevice);
  629. Viewport viewport = _graphicsFixture.GraphicsDevice.Viewport;
  630. Rectangle worldBounds = new Rectangle(0, 0, viewport.Width * 2, viewport.Height * 2);
  631. camera.EnableWorldBounds(worldBounds);
  632. camera.Position = new Vector2(viewport.Width / 2, viewport.Height / 2);
  633. camera.Zoom = 2.0f;
  634. Vector2 zoomCenter = new Vector2(200, 200);
  635. camera.ZoomOut(0.5f, zoomCenter);
  636. Assert.True(camera.Position.X >= 0);
  637. Assert.True(camera.Position.Y >= 0);
  638. Assert.True(camera.Position.X <= worldBounds.Right - viewport.Width / camera.Zoom);
  639. Assert.True(camera.Position.Y <= worldBounds.Bottom - viewport.Height / camera.Zoom);
  640. }
  641. // -----------------------------------------------------------------------------
  642. // Tests for issue #793
  643. // OrthographicCamera.ScreenToWorld and similar methods interact poorly with
  644. // window not at (0,0)
  645. // https://github.com/MonoGame-Extended/Monogame-Extended/issues/793
  646. // -----------------------------------------------------------------------------
  647. //
  648. // The goal of these tests is to verify that coordinate transformations between
  649. // world-space and screen-space behave correctly when the viewport has a non-zero
  650. // origin (e.g. the window is offset or letterboxed).
  651. //
  652. // Specifically:
  653. // - DefaultViewportAdapter: viewport offset should NOT affect transformations.
  654. // - ScalingViewportAdapter/BoxingViewportAdapter: offset IS part of transformation.
  655. //
  656. // Each test ensures that ScreenToWorld() and WorldToScreen() remain consistent
  657. // inverses of one another under these different scenarios.
  658. //
  659. [Trait("Issue", "#793")]
  660. [Collection("GraphicsTest")]
  661. public class OrthographicCameraIssue793Tests
  662. {
  663. private readonly GraphicsTestFixture _graphicsFixture;
  664. public OrthographicCameraIssue793Tests(GraphicsTestFixture graphicsTestFixture)
  665. {
  666. _graphicsFixture = graphicsTestFixture;
  667. }
  668. // -------------------------------------------------------------------------
  669. // Test 1: DefaultViewportAdapter (non-scaling)
  670. // -------------------------------------------------------------------------
  671. // Verifies that for a simple viewport offset (e.g., window not at (0,0)),
  672. // the ScreenToWorld transformation ignores viewport origin and maps directly.
  673. // Mouse/touch input coordinates (from MouseState) are already window-relative,
  674. // so no offset adjustment should occur.
  675. [Fact]
  676. public void ScreenToWorld_WithNonZeroViewportOrigin_TransformsCorrectly()
  677. {
  678. Viewport originalViewport = _graphicsFixture.GraphicsDevice.Viewport;
  679. try
  680. {
  681. _graphicsFixture.GraphicsDevice.Viewport = new Viewport(100, 50, 800, 480);
  682. OrthographicCamera camera = new OrthographicCamera(_graphicsFixture.GraphicsDevice);
  683. Vector2 screenPosition = new Vector2(200, 150);
  684. Vector2 worldPosition = camera.ScreenToWorld(screenPosition);
  685. // Expectation:
  686. // With viewport origin offset, but no scaling or zoom,
  687. // the mapping should remain 1:1 with window coordinates.
  688. Assert.Equal(200, worldPosition.X, 2);
  689. Assert.Equal(150, worldPosition.Y, 2);
  690. }
  691. finally
  692. {
  693. _graphicsFixture.GraphicsDevice.Viewport = originalViewport;
  694. }
  695. }
  696. // -------------------------------------------------------------------------
  697. // Test 2: Round-trip transformation with DefaultViewportAdapter
  698. // -------------------------------------------------------------------------
  699. // Ensures that WorldToScreen() and ScreenToWorld() are true inverses even when
  700. // the viewport has a non-zero origin. The result after round-tripping should
  701. // return to the original world position (within floating-point tolerance).
  702. [Fact]
  703. public void WorldToScreen_RoundTrip_WithNonZeroViewportOrigin_ReturnsOriginalPosition()
  704. {
  705. Viewport originalViewport = _graphicsFixture.GraphicsDevice.Viewport;
  706. try
  707. {
  708. _graphicsFixture.GraphicsDevice.Viewport = new Viewport(100, 50, 800, 480);
  709. OrthographicCamera camera = new OrthographicCamera(_graphicsFixture.GraphicsDevice);
  710. camera.Position = new Vector2(100, 200);
  711. camera.Zoom = 1.5f;
  712. Vector2 originalWorld = new Vector2(250, 350);
  713. Vector2 screen = camera.WorldToScreen(originalWorld);
  714. Vector2 backToWorld = camera.ScreenToWorld(screen);
  715. Assert.Equal(originalWorld.X, backToWorld.X, 2);
  716. Assert.Equal(originalWorld.Y, backToWorld.Y, 2);
  717. }
  718. finally
  719. {
  720. _graphicsFixture.GraphicsDevice.Viewport = originalViewport;
  721. }
  722. }
  723. // -------------------------------------------------------------------------
  724. // Test 3: BoxingViewportAdapter (scaling)
  725. // -------------------------------------------------------------------------
  726. // Verifies that when using a scaling viewport adapter (BoxingViewportAdapter),
  727. // ScreenToWorld() correctly accounts for the viewport offset, which defines
  728. // where the virtual coordinate system is drawn inside the window.
  729. //
  730. // In this case, the viewport offset IS part of the coordinate transformation.
  731. [Fact]
  732. public void ScreenToWorld_WithBoxingViewportAdapter_TransformsCorrectly()
  733. {
  734. Viewport originalViewport = _graphicsFixture.GraphicsDevice.Viewport;
  735. try
  736. {
  737. int virtualWidth = 400;
  738. int virtualHeight = 240;
  739. // Create a boxing viewport adapter with a virtual resolution smaller
  740. // than the actual window. This will introduce a viewport offset
  741. BoxingViewportAdapter viewportAdapter = new BoxingViewportAdapter(
  742. _graphicsFixture.Game.Window,
  743. _graphicsFixture.GraphicsDevice,
  744. virtualWidth,
  745. virtualHeight);
  746. // Forces recalculation of the viewport region inside the window.
  747. viewportAdapter.Reset();
  748. OrthographicCamera camera = new OrthographicCamera(viewportAdapter);
  749. // Retrieve the actual viewport dimensions and offset
  750. Viewport viewport = _graphicsFixture.GraphicsDevice.Viewport;
  751. float scaleX = (float)viewport.Width / virtualWidth;
  752. float scaleY = (float)viewport.Height / virtualHeight;
  753. // The center of the viewport in window space
  754. Vector2 windowPosition = new Vector2(viewport.X + viewport.Width * 0.5f,
  755. viewport.Y + viewport.Height * 0.5f);
  756. Vector2 worldPosition = camera.ScreenToWorld(windowPosition);
  757. // Should map to center of virtual space (200, 120)
  758. Assert.Equal(virtualWidth * 0.5f, worldPosition.X, 2);
  759. Assert.Equal(virtualHeight * 0.5f, worldPosition.Y, 2);
  760. }
  761. finally
  762. {
  763. _graphicsFixture.GraphicsDevice.Viewport = originalViewport;
  764. }
  765. }
  766. // -------------------------------------------------------------------------
  767. // Test 4: Round-trip transformation with BoxingViewportAdapter
  768. // -------------------------------------------------------------------------
  769. // Ensures that WorldToScreen() and ScreenToWorld() remain perfect inverses
  770. // when scaling and offset are both involved.
  771. //
  772. // This confirms that the viewport offset and scaling transformations
  773. // are correctly applied and undone in opposite order.
  774. [Fact]
  775. public void WorldToScreen_RoundTrip_WithBoxingViewportAdapter_ReturnsOriginalPosition()
  776. {
  777. Viewport originalViewport = _graphicsFixture.GraphicsDevice.Viewport;
  778. try
  779. {
  780. int virtualWidth = 400;
  781. int virtualHeight = 240;
  782. BoxingViewportAdapter viewportAdapter = new BoxingViewportAdapter(_graphicsFixture.Game.Window,
  783. _graphicsFixture.GraphicsDevice,
  784. virtualWidth,
  785. virtualHeight);
  786. viewportAdapter.Reset();
  787. OrthographicCamera camera = new OrthographicCamera(viewportAdapter);
  788. camera.Position = new Vector2(50, 100);
  789. camera.Zoom = 1.5f;
  790. Vector2 originalWorld = new Vector2(125, 175);
  791. // Round trip world -> screen -> world
  792. Vector2 screen = camera.WorldToScreen(originalWorld);
  793. Vector2 backToWorld = camera.ScreenToWorld(screen);
  794. // Expect round-trip consistency
  795. Assert.Equal(originalWorld.X, backToWorld.X, 2);
  796. Assert.Equal(originalWorld.Y, backToWorld.Y, 2);
  797. }
  798. finally
  799. {
  800. _graphicsFixture.GraphicsDevice.Viewport = originalViewport;
  801. }
  802. }
  803. }
  804. }