FrustumTests.cpp 37 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721
  1. /*
  2. * Copyright (c) Contributors to the Open 3D Engine Project.
  3. * For complete copyright and license terms please see the LICENSE at the root of this distribution.
  4. *
  5. * SPDX-License-Identifier: Apache-2.0 OR MIT
  6. *
  7. */
  8. #include <AzCore/Math/Frustum.h>
  9. #include <AzCore/Math/ShapeIntersection.h>
  10. #include <AzCore/UnitTest/TestTypes.h>
  11. #include <AZTestShared/Math/MathTestHelpers.h>
  12. namespace UnitTest
  13. {
  14. // Basic frustum centered on the origin
  15. AZ::Frustum testFrustum1(
  16. AZ::ViewFrustumAttributes(AZ::Transform::CreateIdentity(), 1.0f, AZ::Constants::HalfPi, 1.0f, 100.0f));
  17. // Frustum that has slope 1/2 for the top/bottom/left/right clip planes
  18. AZ::Frustum testFrustum2(
  19. AZ::ViewFrustumAttributes(AZ::Transform::CreateIdentity(), 1.0f, 2.0f * atanf(0.5f), 10.0f, 90.0f));
  20. TEST(MATH_Frustum, TestFrustumSphereDisjointNear)
  21. {
  22. // Test a sphere in front of the near clip with a radius too small to intersect
  23. {
  24. AZ::IntersectResult result = testFrustum1.IntersectSphere(AZ::Vector3::CreateZero(), 0.5f);
  25. EXPECT_EQ(result, AZ::IntersectResult::Exterior);
  26. }
  27. {
  28. AZ::IntersectResult result = testFrustum2.IntersectSphere(AZ::Vector3(0.0f, 9.0f, 0.0f), 0.5f);
  29. EXPECT_EQ(result, AZ::IntersectResult::Exterior);
  30. }
  31. }
  32. TEST(MATH_Frustum, TestFrustumSphereDisjointFar)
  33. {
  34. // Test a sphere beyond the far clip
  35. {
  36. AZ::IntersectResult result = testFrustum1.IntersectSphere(AZ::Vector3(0.0f, 102.0f, 0.0f), 1.0f);
  37. EXPECT_EQ(result, AZ::IntersectResult::Exterior);
  38. }
  39. {
  40. AZ::IntersectResult result = testFrustum2.IntersectSphere(AZ::Vector3(0.0f, 92.0f, 0.0f), 1.0f);
  41. EXPECT_EQ(result, AZ::IntersectResult::Exterior);
  42. }
  43. }
  44. TEST(MATH_Frustum, TestFrustumSphereIntersectNear)
  45. {
  46. // Test a sphere in front of the near clip with a radius large enough to intersect
  47. {
  48. AZ::IntersectResult result = testFrustum1.IntersectSphere(AZ::Vector3::CreateZero(), 1.5f);
  49. EXPECT_EQ(result, AZ::IntersectResult::Overlaps);
  50. }
  51. {
  52. AZ::IntersectResult result = testFrustum2.IntersectSphere(AZ::Vector3(0.0f, 9.0f, 0.0f), 1.5f);
  53. EXPECT_EQ(result, AZ::IntersectResult::Overlaps);
  54. }
  55. }
  56. TEST(MATH_Frustum, TestFrustumSphereIntersectLeft)
  57. {
  58. // Test a sphere on the left clip plane
  59. {
  60. AZ::IntersectResult result = testFrustum1.IntersectSphere(AZ::Vector3(-50.0f, 50.0f, 0.0f), 1.0f);
  61. EXPECT_EQ(result, AZ::IntersectResult::Overlaps);
  62. }
  63. {
  64. AZ::IntersectResult result = testFrustum2.IntersectSphere(AZ::Vector3(-25.0f, 50.0f, 0.0f), 1.0f);
  65. EXPECT_EQ(result, AZ::IntersectResult::Overlaps);
  66. }
  67. }
  68. TEST(MATH_Frustum, TestFrustumSphereIntersectRight)
  69. {
  70. // Test a sphere on the right clip plane
  71. {
  72. AZ::IntersectResult result = testFrustum1.IntersectSphere(AZ::Vector3(50.0f, 50.0f, 0.0f), 1.0f);
  73. EXPECT_EQ(result, AZ::IntersectResult::Overlaps);
  74. }
  75. {
  76. AZ::IntersectResult result = testFrustum2.IntersectSphere(AZ::Vector3(25.0f, 50.0f, 0.0f), 1.0f);
  77. EXPECT_EQ(result, AZ::IntersectResult::Overlaps);
  78. }
  79. }
  80. TEST(MATH_Frustum, TestFrustumSphereIntersectTop)
  81. {
  82. // Test a sphere on the top clip plane
  83. {
  84. AZ::IntersectResult result = testFrustum1.IntersectSphere(AZ::Vector3(0.0f, 50.0f, 50.0f), 1.0f);
  85. EXPECT_EQ(result, AZ::IntersectResult::Overlaps);
  86. }
  87. {
  88. AZ::IntersectResult result = testFrustum2.IntersectSphere(AZ::Vector3(0.0f, 50.0f, 25.0f), 1.0f);
  89. EXPECT_EQ(result, AZ::IntersectResult::Overlaps);
  90. }
  91. }
  92. TEST(MATH_Frustum, TestFrustumSphereIntersectBottom)
  93. {
  94. // Test a sphere on the bottom clip plane
  95. {
  96. AZ::IntersectResult result = testFrustum1.IntersectSphere(AZ::Vector3(0.0f, 50.0f, -50.0f), 1.0f);
  97. EXPECT_EQ(result, AZ::IntersectResult::Overlaps);
  98. }
  99. {
  100. AZ::IntersectResult result = testFrustum2.IntersectSphere(AZ::Vector3(0.0f, 50.0f, -25.0f), 1.0f);
  101. EXPECT_EQ(result, AZ::IntersectResult::Overlaps);
  102. }
  103. }
  104. TEST(MATH_Frustum, TestFrustumSphereContained)
  105. {
  106. // Test a sphere near the middle of the frustum
  107. {
  108. AZ::IntersectResult result = testFrustum1.IntersectSphere(AZ::Vector3(0.0f, 50.0f, 0.0f), 1.0f);
  109. EXPECT_EQ(result, AZ::IntersectResult::Interior);
  110. }
  111. {
  112. AZ::IntersectResult result = testFrustum2.IntersectSphere(AZ::Vector3(0.0f, 50.0f, 0.0f), 1.0f);
  113. EXPECT_EQ(result, AZ::IntersectResult::Interior);
  114. }
  115. }
  116. TEST(MATH_Frustum, TestFrustumAabbDisjointNear)
  117. {
  118. // Test an AABB in front of the near clip with a size too small to intersect
  119. {
  120. AZ::Vector3 center = AZ::Vector3::CreateZero();
  121. AZ::IntersectResult result = testFrustum1.IntersectAabb(center - AZ::Vector3(0.5f), center + AZ::Vector3(0.5f));
  122. EXPECT_EQ(result, AZ::IntersectResult::Exterior);
  123. }
  124. {
  125. AZ::Vector3 center = AZ::Vector3(0.0f, 9.0f, 0.0f);
  126. AZ::IntersectResult result = testFrustum2.IntersectAabb(center - AZ::Vector3(0.5f, 0.5f, 0.5f), center + AZ::Vector3(0.5f, 0.5f, 0.5f));
  127. EXPECT_EQ(result, AZ::IntersectResult::Exterior);
  128. }
  129. }
  130. TEST(MATH_Frustum, TestFrustumAabbDisjointFar)
  131. {
  132. // Test an AABB beyond the far clip
  133. {
  134. AZ::Vector3 center = AZ::Vector3(0.0f, 102.0f, 0.0f);
  135. AZ::IntersectResult result = testFrustum1.IntersectAabb(center - AZ::Vector3(1.0f), center + AZ::Vector3(1.0f));
  136. EXPECT_EQ(result, AZ::IntersectResult::Exterior);
  137. }
  138. {
  139. AZ::Vector3 center = AZ::Vector3(0.0f, 92.0f, 0.0f);
  140. AZ::IntersectResult result = testFrustum2.IntersectAabb(center - AZ::Vector3(1.0f), center + AZ::Vector3(1.0f));
  141. EXPECT_EQ(result, AZ::IntersectResult::Exterior);
  142. }
  143. }
  144. TEST(MATH_Frustum, TestFrustumAabbIntersectNear)
  145. {
  146. // Test an AABB in front of the near clip with a size large enough to intersect
  147. {
  148. AZ::Vector3 center = AZ::Vector3::CreateZero();
  149. AZ::IntersectResult result = testFrustum1.IntersectAabb(center - AZ::Vector3(1.5f), center + AZ::Vector3(1.5f));
  150. EXPECT_EQ(result, AZ::IntersectResult::Overlaps);
  151. }
  152. {
  153. AZ::Vector3 center = AZ::Vector3(0.0f, 9.0f, 0.0f);
  154. AZ::IntersectResult result = testFrustum2.IntersectAabb(center - AZ::Vector3(1.5f, 1.5f, 1.5f), center + AZ::Vector3(1.5f, 1.5f, 1.5f));
  155. EXPECT_EQ(result, AZ::IntersectResult::Overlaps);
  156. }
  157. }
  158. TEST(MATH_Frustum, TestFrustumAabbIntersectLeft)
  159. {
  160. // Test an AABB on the left clip plane
  161. {
  162. AZ::Vector3 center = AZ::Vector3(-50.0f, 50.0f, 0.0f);
  163. AZ::IntersectResult result = testFrustum1.IntersectAabb(center - AZ::Vector3(1.0f), center + AZ::Vector3(1.0f));
  164. EXPECT_EQ(result, AZ::IntersectResult::Overlaps);
  165. }
  166. {
  167. AZ::Vector3 center = AZ::Vector3(-25.0f, 50.0f, 0.0f);
  168. AZ::IntersectResult result = testFrustum2.IntersectAabb(center - AZ::Vector3(1.0f), center + AZ::Vector3(1.0f));
  169. EXPECT_EQ(result, AZ::IntersectResult::Overlaps);
  170. }
  171. }
  172. TEST(MATH_Frustum, TestFrustumAabbIntersectRight)
  173. {
  174. // Test an AABB on the right clip plane
  175. {
  176. AZ::Vector3 center = AZ::Vector3(50.0f, 50.0f, 0.0f);
  177. AZ::IntersectResult result = testFrustum1.IntersectAabb(center - AZ::Vector3(1.0f), center + AZ::Vector3(1.0f));
  178. EXPECT_EQ(result, AZ::IntersectResult::Overlaps);
  179. }
  180. {
  181. AZ::Vector3 center = AZ::Vector3(25.0f, 50.0f, 0.0f);
  182. AZ::IntersectResult result = testFrustum2.IntersectAabb(center - AZ::Vector3(1.0f), center + AZ::Vector3(1.0f));
  183. EXPECT_EQ(result, AZ::IntersectResult::Overlaps);
  184. }
  185. }
  186. TEST(MATH_Frustum, TestFrustumAabbIntersectTop)
  187. {
  188. // Test an AABB on the top clip plane
  189. {
  190. AZ::Vector3 center = AZ::Vector3(0.0f, 50.0f, 50.0f);
  191. AZ::IntersectResult result = testFrustum1.IntersectAabb(center - AZ::Vector3(1.0f), center + AZ::Vector3(1.0f));
  192. EXPECT_EQ(result, AZ::IntersectResult::Overlaps);
  193. }
  194. {
  195. AZ::Vector3 center = AZ::Vector3(0.0f, 50.0f, 25.0f);
  196. AZ::IntersectResult result = testFrustum2.IntersectAabb(center - AZ::Vector3(1.0f), center + AZ::Vector3(1.0f));
  197. EXPECT_EQ(result, AZ::IntersectResult::Overlaps);
  198. }
  199. }
  200. TEST(MATH_Frustum, TestFrustumAabbIntersectBottom)
  201. {
  202. // Test an AABB on the bottom clip plane
  203. {
  204. AZ::Vector3 center = AZ::Vector3(0.0f, 50.0f, -50.0f);
  205. AZ::IntersectResult result = testFrustum1.IntersectAabb(center - AZ::Vector3(1.0f), center + AZ::Vector3(1.0f));
  206. EXPECT_EQ(result, AZ::IntersectResult::Overlaps);
  207. }
  208. {
  209. AZ::Vector3 center = AZ::Vector3(0.0f, 50.0f, -25.0f);
  210. AZ::IntersectResult result = testFrustum2.IntersectAabb(center - AZ::Vector3(1.0f), center + AZ::Vector3(1.0f));
  211. EXPECT_EQ(result, AZ::IntersectResult::Overlaps);
  212. }
  213. }
  214. TEST(MATH_Frustum, TestFrustumAabbContained)
  215. {
  216. // Test an AABB near the middle of the frustum
  217. {
  218. AZ::Vector3 center = AZ::Vector3(0.0f, 50.0f, 0.0f);
  219. AZ::IntersectResult result = testFrustum1.IntersectAabb(center - AZ::Vector3(1.0f), center + AZ::Vector3(1.0f));
  220. EXPECT_EQ(result, AZ::IntersectResult::Interior);
  221. }
  222. {
  223. AZ::Vector3 center = AZ::Vector3(0.0f, 50.0f, 0.0f);
  224. AZ::IntersectResult result = testFrustum2.IntersectAabb(center - AZ::Vector3(1.0f), center + AZ::Vector3(1.0f));
  225. EXPECT_EQ(result, AZ::IntersectResult::Interior);
  226. }
  227. }
  228. TEST(MATH_Frustum, CalculateViewFrustumAttributesExample1)
  229. {
  230. const AZ::Vector3 translation(0.1f, 0.2f, 0.3f);
  231. const AZ::Quaternion quaternion(0.52f, 0.56f, 0.56f, 0.32f);
  232. const AZ::Transform transform = AZ::Transform::CreateFromQuaternionAndTranslation(quaternion, translation);
  233. constexpr float aspectRatio = 1.3f;
  234. constexpr float fovRadians = 0.8f;
  235. constexpr float nearClip = 0.045f;
  236. constexpr float farClip = 10.3f;
  237. const AZ::Frustum frustum(AZ::ViewFrustumAttributes(transform, aspectRatio, fovRadians, nearClip, farClip));
  238. const AZ::ViewFrustumAttributes viewFrustumAttributes = frustum.CalculateViewFrustumAttributes();
  239. EXPECT_THAT(viewFrustumAttributes.m_worldTransform, IsClose(transform));
  240. EXPECT_NEAR(viewFrustumAttributes.m_aspectRatio, aspectRatio, 1e-3f);
  241. EXPECT_NEAR(viewFrustumAttributes.m_verticalFovRadians, fovRadians, 1e-3f);
  242. EXPECT_NEAR(viewFrustumAttributes.m_nearClip, nearClip, 1e-3f);
  243. EXPECT_NEAR(viewFrustumAttributes.m_farClip, farClip, 1e-3f);
  244. }
  245. TEST(MATH_Frustum, CalculateViewFrustumAttributesExample2)
  246. {
  247. const auto transform = AZ::Transform::CreateTranslation(AZ::Vector3::CreateAxisZ(5.0f)) *
  248. AZ::Transform::CreateRotationX(AZ::DegToRad(45.0f)) * AZ::Transform::CreateRotationZ(AZ::DegToRad(90.0f));
  249. constexpr float aspectRatio = 1024.0f/768.0f;
  250. constexpr float fovRadians = AZ::DegToRad(60.0f);
  251. constexpr float nearClip = 0.1f;
  252. constexpr float farClip = 100.0f;
  253. const AZ::Frustum frustum(AZ::ViewFrustumAttributes(transform, aspectRatio, fovRadians, nearClip, farClip));
  254. const AZ::ViewFrustumAttributes viewFrustumAttributes = frustum.CalculateViewFrustumAttributes();
  255. EXPECT_THAT(viewFrustumAttributes.m_worldTransform, IsClose(transform));
  256. EXPECT_NEAR(viewFrustumAttributes.m_aspectRatio, aspectRatio, 1e-3f);
  257. EXPECT_NEAR(viewFrustumAttributes.m_verticalFovRadians, fovRadians, 1e-3f);
  258. EXPECT_NEAR(viewFrustumAttributes.m_nearClip, nearClip, 1e-3f);
  259. EXPECT_NEAR(viewFrustumAttributes.m_farClip, farClip, 1e-3f);
  260. }
  261. TEST(MATH_Frustum, TestGetSetPlane)
  262. {
  263. // Assumes +x runs to the 'right', +y runs 'out' and +z points 'up'
  264. // A frustum is defined by 6 planes. In this case a box shape.
  265. AZ::Plane near_value = AZ::Plane::CreateFromNormalAndPoint(AZ::Vector3(0.f, 1.f, 0.f), AZ::Vector3(0.f, -5.f, 0.f));
  266. AZ::Plane far_value = AZ::Plane::CreateFromNormalAndPoint(AZ::Vector3(0.f, -1.f, 0.f), AZ::Vector3(0.f, 5.f, 0.f));
  267. AZ::Plane left = AZ::Plane::CreateFromNormalAndPoint(AZ::Vector3(1.f, 0.f, 0.f), AZ::Vector3(-5.f, 0.f, 0.f));
  268. AZ::Plane right = AZ::Plane::CreateFromNormalAndPoint(AZ::Vector3(-1.f, 0.f, 0.f), AZ::Vector3(5.f, 0.f, 0.f));
  269. AZ::Plane top = AZ::Plane::CreateFromNormalAndPoint(AZ::Vector3(0.f, 0.f, -1.f), AZ::Vector3(0.f, 0.f, 5.f));
  270. AZ::Plane bottom = AZ::Plane::CreateFromNormalAndPoint(AZ::Vector3(0.f, 0.f, 1.f), AZ::Vector3(0.f, 0.f, -5.f));
  271. AZ::Frustum frustum(near_value, far_value, left, right, top, bottom);
  272. EXPECT_TRUE(frustum.GetPlane(AZ::Frustum::PlaneId::Near) == near_value);
  273. EXPECT_TRUE(frustum.GetPlane(AZ::Frustum::PlaneId::Far) == far_value);
  274. EXPECT_TRUE(frustum.GetPlane(AZ::Frustum::PlaneId::Left) == left);
  275. EXPECT_TRUE(frustum.GetPlane(AZ::Frustum::PlaneId::Right) == right);
  276. EXPECT_TRUE(frustum.GetPlane(AZ::Frustum::PlaneId::Top) == top);
  277. EXPECT_TRUE(frustum.GetPlane(AZ::Frustum::PlaneId::Bottom) == bottom);
  278. AZ::Plane near1 = AZ::Plane::CreateFromNormalAndPoint(AZ::Vector3(0.f, 1.f, 0.f), AZ::Vector3(0.f, -2.f, 0.f));
  279. AZ::Plane far1 = AZ::Plane::CreateFromNormalAndPoint(AZ::Vector3(0.f, -1.f, 0.f), AZ::Vector3(0.f, 2.f, 0.f));
  280. AZ::Plane left1 = AZ::Plane::CreateFromNormalAndPoint(AZ::Vector3(1.f, 0.f, 0.f), AZ::Vector3(-2.f, 0.f, 0.f));
  281. AZ::Plane right1 = AZ::Plane::CreateFromNormalAndPoint(AZ::Vector3(-1.f, 0.f, 0.f), AZ::Vector3(2.f, 0.f, 0.f));
  282. AZ::Plane top1 = AZ::Plane::CreateFromNormalAndPoint(AZ::Vector3(0.f, 0.f, -1.f), AZ::Vector3(0.f, 0.f, 2.f));
  283. AZ::Plane bottom1 = AZ::Plane::CreateFromNormalAndPoint(AZ::Vector3(0.f, 0.f, 1.f), AZ::Vector3(0.f, 0.f, -2.f));
  284. AZ::Frustum frustum1(near1, far1, left1, right1, top1, bottom1);
  285. EXPECT_THAT(frustum, testing::Not(IsClose(frustum1)));
  286. frustum.Set(frustum1);
  287. EXPECT_THAT(frustum, IsClose(frustum1));
  288. frustum.SetPlane(AZ::Frustum::PlaneId::Near, near_value);
  289. frustum.SetPlane(AZ::Frustum::PlaneId::Far, far_value);
  290. frustum.SetPlane(AZ::Frustum::PlaneId::Left, left);
  291. frustum.SetPlane(AZ::Frustum::PlaneId::Right, right);
  292. frustum.SetPlane(AZ::Frustum::PlaneId::Top, top);
  293. frustum.SetPlane(AZ::Frustum::PlaneId::Bottom, bottom);
  294. EXPECT_TRUE(frustum.GetPlane(AZ::Frustum::PlaneId::Near) == near_value);
  295. EXPECT_TRUE(frustum.GetPlane(AZ::Frustum::PlaneId::Far) == far_value);
  296. EXPECT_TRUE(frustum.GetPlane(AZ::Frustum::PlaneId::Left) == left);
  297. EXPECT_TRUE(frustum.GetPlane(AZ::Frustum::PlaneId::Right) == right);
  298. EXPECT_TRUE(frustum.GetPlane(AZ::Frustum::PlaneId::Top) == top);
  299. EXPECT_TRUE(frustum.GetPlane(AZ::Frustum::PlaneId::Bottom) == bottom);
  300. frustum = frustum1;
  301. EXPECT_THAT(frustum, IsClose(frustum1));
  302. }
  303. // TODO: Test frustum creation from View-Projection Matrices
  304. struct FrustumTestCase
  305. {
  306. AZ::Vector3 nearTopLeft;
  307. AZ::Vector3 nearTopRight;
  308. AZ::Vector3 nearBottomLeft;
  309. AZ::Vector3 nearBottomRight;
  310. AZ::Vector3 farTopLeft;
  311. AZ::Vector3 farTopRight;
  312. AZ::Vector3 farBottomLeft;
  313. AZ::Vector3 farBottomRight;
  314. // Used to set which plane is being used
  315. AZ::Frustum::PlaneId plane;
  316. // Used to set a test case name
  317. std::string testCaseName;
  318. };
  319. std::ostream& operator<< (std::ostream& stream, const UnitTest::FrustumTestCase& frustumTestCase)
  320. {
  321. stream << "NearTopLeft: " << frustumTestCase.nearTopLeft
  322. << std::endl << "NearTopRight: " << frustumTestCase.nearTopRight
  323. << std::endl << "NearBottomRight: " << frustumTestCase.nearBottomRight
  324. << std::endl << "NearBottomLeft: " << frustumTestCase.nearBottomLeft
  325. << std::endl << "FarTopLeft: " << frustumTestCase.farTopLeft
  326. << std::endl << "FarTopRight: " << frustumTestCase.farTopRight
  327. << std::endl << "FarBottomLeft: " << frustumTestCase.farBottomLeft
  328. << std::endl << "FarBottomRight: " << frustumTestCase.farBottomRight;
  329. return stream;
  330. }
  331. class Tests
  332. : public ::testing::WithParamInterface <FrustumTestCase>
  333. , public UnitTest::LeakDetectionFixture
  334. {
  335. protected:
  336. void SetUp() override
  337. {
  338. UnitTest::LeakDetectionFixture::SetUp();
  339. // Build a frustum from 8 points
  340. // This allows us to generate test cases in each of the 9 regions in the 3x3 grid divided by the 6 planes,
  341. // as well as test cases that span across those regions
  342. m_testCase = GetParam();
  343. // Planes can be generated from triangles. Points must wind counter-clockwise (right-handed) for the normal to point in the correct direction.
  344. // The Frustum class assumes the plane normals point inwards
  345. m_planes[AZ::Frustum::PlaneId::Near] = AZ::Plane::CreateFromTriangle(m_testCase.nearTopLeft, m_testCase.nearTopRight, m_testCase.nearBottomRight);
  346. m_planes[AZ::Frustum::PlaneId::Far] = AZ::Plane::CreateFromTriangle(m_testCase.farTopRight, m_testCase.farTopLeft, m_testCase.farBottomLeft);
  347. m_planes[AZ::Frustum::PlaneId::Left] = AZ::Plane::CreateFromTriangle(m_testCase.nearTopLeft, m_testCase.nearBottomLeft, m_testCase.farTopLeft);
  348. m_planes[AZ::Frustum::PlaneId::Right] = AZ::Plane::CreateFromTriangle(m_testCase.nearTopRight, m_testCase.farTopRight, m_testCase.farBottomRight);
  349. m_planes[AZ::Frustum::PlaneId::Top] = AZ::Plane::CreateFromTriangle(m_testCase.nearTopLeft, m_testCase.farTopLeft, m_testCase.farTopRight);
  350. m_planes[AZ::Frustum::PlaneId::Bottom] = AZ::Plane::CreateFromTriangle(m_testCase.nearBottomLeft, m_testCase.nearBottomRight, m_testCase.farBottomRight);
  351. // AZ::Plane::CreateFromTriangle uses Vector3::GetNormalized to create a normal, which is not perfectly normalized.
  352. // Since the distance value is set by float dist = -(normal.Dot(v0));, this means that an imperfect normal actually gives you a slightly different distance
  353. // and thus a slightly different plane. Correct that here.
  354. m_planes[AZ::Frustum::PlaneId::Near] = AZ::Plane::CreateFromNormalAndPoint(m_planes[AZ::Frustum::PlaneId::Near].GetNormal().GetNormalized(), m_testCase.nearTopLeft);
  355. m_planes[AZ::Frustum::PlaneId::Far] = AZ::Plane::CreateFromNormalAndPoint(m_planes[AZ::Frustum::PlaneId::Far].GetNormal().GetNormalized(), m_testCase.farTopRight);
  356. m_planes[AZ::Frustum::PlaneId::Left] = AZ::Plane::CreateFromNormalAndPoint(m_planes[AZ::Frustum::PlaneId::Left].GetNormal().GetNormalized(), m_testCase.nearTopLeft);
  357. m_planes[AZ::Frustum::PlaneId::Right] = AZ::Plane::CreateFromNormalAndPoint(m_planes[AZ::Frustum::PlaneId::Right].GetNormal().GetNormalized(), m_testCase.nearTopRight);
  358. m_planes[AZ::Frustum::PlaneId::Top] = AZ::Plane::CreateFromNormalAndPoint(m_planes[AZ::Frustum::PlaneId::Top].GetNormal().GetNormalized(), m_testCase.nearTopLeft);
  359. m_planes[AZ::Frustum::PlaneId::Bottom] = AZ::Plane::CreateFromNormalAndPoint(m_planes[AZ::Frustum::PlaneId::Bottom].GetNormal().GetNormalized(), m_testCase.nearBottomLeft);
  360. // Create the frustum itself
  361. for (AZ::Frustum::PlaneId planeId = AZ::Frustum::PlaneId::Near; planeId < AZ::Frustum::PlaneId::MAX; ++planeId)
  362. {
  363. m_frustum.SetPlane(planeId, m_planes[planeId]);
  364. }
  365. // Get the center points
  366. m_centerPoints[AZ::Frustum::PlaneId::Near] = m_testCase.nearTopLeft + 0.5f * (m_testCase.nearBottomRight - m_testCase.nearTopLeft);
  367. m_centerPoints[AZ::Frustum::PlaneId::Far] = m_testCase.farTopLeft + 0.5f * (m_testCase.farBottomRight - m_testCase.farTopLeft);
  368. m_centerPoints[AZ::Frustum::PlaneId::Left] = m_testCase.nearTopLeft + 0.5f * (m_testCase.farBottomLeft - m_testCase.nearTopLeft);
  369. m_centerPoints[AZ::Frustum::PlaneId::Right] = m_testCase.nearTopRight + 0.5f * (m_testCase.farBottomRight - m_testCase.nearTopRight);
  370. m_centerPoints[AZ::Frustum::PlaneId::Top] = m_testCase.nearTopLeft + 0.5f * (m_testCase.farTopRight - m_testCase.nearTopLeft);
  371. m_centerPoints[AZ::Frustum::PlaneId::Bottom] = m_testCase.nearBottomLeft + 0.5f * (m_testCase.farBottomRight - m_testCase.nearBottomLeft);
  372. // Get the shortest edge of the frustum
  373. AZStd::vector<AZ::Vector3> edges;
  374. // Near plane
  375. edges.push_back(m_testCase.nearTopLeft - m_testCase.nearTopRight);
  376. edges.push_back(m_testCase.nearTopRight - m_testCase.nearBottomRight);
  377. edges.push_back(m_testCase.nearBottomRight - m_testCase.nearBottomLeft);
  378. edges.push_back(m_testCase.nearBottomLeft - m_testCase.nearTopLeft);
  379. // Edges from near plane to far plane
  380. edges.push_back(m_testCase.nearTopLeft - m_testCase.farTopLeft);
  381. edges.push_back(m_testCase.nearTopRight - m_testCase.farTopRight);
  382. edges.push_back(m_testCase.nearBottomRight - m_testCase.farBottomRight);
  383. edges.push_back(m_testCase.nearBottomLeft - m_testCase.farBottomLeft);
  384. // Far plane
  385. edges.push_back(m_testCase.farTopLeft - m_testCase.farTopRight);
  386. edges.push_back(m_testCase.farTopRight - m_testCase.farBottomRight);
  387. edges.push_back(m_testCase.farBottomRight - m_testCase.farBottomLeft);
  388. edges.push_back(m_testCase.farBottomLeft - m_testCase.farTopLeft);
  389. m_minEdgeLength = std::numeric_limits<float>::max();
  390. for (const AZ::Vector3& edge : edges)
  391. {
  392. m_minEdgeLength = AZ::GetMin(m_minEdgeLength, static_cast<float>(edge.GetLength()));
  393. }
  394. }
  395. AZ::Sphere GenerateSphereOutsidePlane(const AZ::Plane& plane, const AZ::Vector3& planeCenter)
  396. {
  397. // Get a radius small enough for the entire sphere to fit outside the plane without extending past the other planes in the frustum
  398. float radius = 0.25f * m_minEdgeLength;
  399. // Get the outward pointing normal of the plane
  400. AZ::Vector3 normal = -1.0f * plane.GetNormal();
  401. // Create a sphere that is outside the plane
  402. AZ::Vector3 center = planeCenter + normal * (radius + m_marginOfErrorOffset);
  403. return AZ::Sphere(center, radius);
  404. }
  405. AZ::Sphere GenerateSphereInsidePlane(const AZ::Plane& plane, const AZ::Vector3& planeCenter)
  406. {
  407. // Get a radius small enough for the entire sphere to fit outside the plane without extending past the other planes in the frustum
  408. float radius = 0.25f * m_minEdgeLength;
  409. // Get the inward pointing normal of the plane
  410. AZ::Vector3 normal = plane.GetNormal();
  411. // Create a sphere that is inside the plane
  412. AZ::Vector3 center = planeCenter + normal * (radius + m_marginOfErrorOffset);
  413. return AZ::Sphere(center, radius);
  414. }
  415. // The points used to generate the test cases
  416. FrustumTestCase m_testCase;
  417. // The planes generated from the points.
  418. // Keep these around for access to the normals, which are used to generate shapes inside/outside of the frustum
  419. AZ::Plane m_planes[AZ::Frustum::PlaneId::MAX];
  420. // The center points on the frustum for each plane
  421. AZ::Vector3 m_centerPoints[AZ::Frustum::PlaneId::MAX];
  422. // The length of the shortest edge in the frustum
  423. float m_minEdgeLength = std::numeric_limits<float>::max();
  424. // Shapes generated inside/outside the frustum will be offset by this much as a margin of error
  425. // Through trial and error determined that, for the test cases below, the sphere needs to be offset from the frustum by at least 0.115f to be guaranteed to pass,
  426. // which gives a reasonable idea of how precise these intersection tests are.
  427. // For the box shaped frustum, the tests passed when offset by FLT_EPSILON, but the frustums further away from the origin were less precise.
  428. float m_marginOfErrorOffset = 0.115f;
  429. // The frustum under test
  430. AZ::Frustum m_frustum;
  431. };
  432. // Tests that a frustum does not contain a sphere that is outside the frustum
  433. TEST_P(Tests, FrustumContainsSphere_SphereOutsidePlane_False)
  434. {
  435. AZ::Sphere testSphere = GenerateSphereOutsidePlane(m_planes[m_testCase.plane], m_centerPoints[m_testCase.plane]);
  436. EXPECT_FALSE(AZ::ShapeIntersection::Contains(m_frustum, testSphere)) << "Frustum contains sphere even though sphere is completely outside the frustum." << std::endl << "Frustum:" << std::endl << m_testCase << std::endl << "Sphere:" << std::endl << testSphere << std::endl;
  437. }
  438. // Tests that a sphere outside the frustum does not overlap the frustum
  439. TEST_P(Tests, SphereOverlapsFrustum_SphereOutsidePlane_False)
  440. {
  441. AZ::Sphere testSphere = GenerateSphereOutsidePlane(m_planes[m_testCase.plane], m_centerPoints[m_testCase.plane]);
  442. EXPECT_FALSE(AZ::ShapeIntersection::Overlaps(testSphere, m_frustum)) << "Sphere overlaps frustum even though sphere is completely outside the frustum." << std::endl << "Frustum:" << std::endl << m_testCase << std::endl << "Sphere:" << std::endl << testSphere << std::endl;
  443. }
  444. // Tests that a frustum contains a sphere that is inside the frustum
  445. TEST_P(Tests, FrustumContainsSphere_SphereInsidePlane_True)
  446. {
  447. AZ::Sphere testSphere = GenerateSphereInsidePlane(m_planes[m_testCase.plane], m_centerPoints[m_testCase.plane]);
  448. EXPECT_TRUE(AZ::ShapeIntersection::Contains(m_frustum, testSphere)) << "Frustum does not contain sphere even though sphere is completely inside the frustum." << std::endl << "Frustum:" << std::endl << m_testCase << std::endl << "Sphere:" << std::endl << testSphere << std::endl;
  449. }
  450. // Tests that a sphere inside the frustum overlaps the frustum
  451. TEST_P(Tests, SphereOverlapsFrustum_SphereInsidePlane_True)
  452. {
  453. AZ::Sphere testSphere = GenerateSphereInsidePlane(m_planes[m_testCase.plane], m_centerPoints[m_testCase.plane]);
  454. EXPECT_TRUE(AZ::ShapeIntersection::Overlaps(testSphere, m_frustum)) << "Sphere does not overlap frustum even though sphere is completely inside the frustum." << std::endl << "Frustum:" << std::endl << m_testCase << std::endl << "Sphere:" << std::endl << testSphere << std::endl;
  455. }
  456. // Tests that a frustum does not contain a sphere that is half inside half outside the frustum
  457. TEST_P(Tests, FrustumContainsSphere_SphereHalfInsideHalfOutsidePlane_False)
  458. {
  459. AZ::Sphere testSphere(m_centerPoints[m_testCase.plane], m_minEdgeLength * .25f);
  460. EXPECT_FALSE(AZ::ShapeIntersection::Contains(m_frustum, testSphere)) << "Frustum contains sphere even though sphere is partially outside the frustum." << std::endl << "Frustum:" << std::endl << m_testCase << std::endl << "Sphere:" << std::endl << testSphere << std::endl;
  461. }
  462. // Tests that a sphere half inside half outside the frustum overlaps the frustum
  463. TEST_P(Tests, SphereOverlapsFrustum_SphereHalfInsideHalfOutsidePlane_True)
  464. {
  465. AZ::Sphere testSphere(m_centerPoints[m_testCase.plane], m_minEdgeLength * .25f);
  466. EXPECT_TRUE(AZ::ShapeIntersection::Overlaps(testSphere, m_frustum)) << "Sphere does not overlap frustum even though sphere is partially inside the frustum." << std::endl << "Frustum:" << std::endl << m_testCase << std::endl << "Sphere:" << std::endl << testSphere << std::endl;
  467. }
  468. std::vector<FrustumTestCase> GenerateFrustumIntersectionTestCases()
  469. {
  470. std::vector<FrustumTestCase> testCases;
  471. std::vector<FrustumTestCase> frustums;
  472. // 2x2x2 box (Z-up coordinate system)
  473. FrustumTestCase box;
  474. box.nearTopLeft = AZ::Vector3(-1.0f, -1.0f, 1.0f);
  475. box.nearTopRight = AZ::Vector3(1.0f, -1.0f, 1.0f);
  476. box.nearBottomLeft = AZ::Vector3(-1.0f, -1.0f, -1.0f);
  477. box.nearBottomRight = AZ::Vector3(1.0f, -1.0f, -1.0f);
  478. box.farTopLeft = AZ::Vector3(-1.0f, 1.0f, 1.0f);
  479. box.farTopRight = AZ::Vector3(1.0f, 1.0f, 1.0f);
  480. box.farBottomLeft = AZ::Vector3(-1.0f, 1.0f, -1.0f);
  481. box.farBottomRight = AZ::Vector3(1.0f, 1.0f, -1.0f);
  482. box.testCaseName = "BoxShaped";
  483. frustums.push_back(box);
  484. FrustumTestCase defaultCameraFrustum;
  485. defaultCameraFrustum.nearTopLeft = AZ::Vector3(-0.204621f, 0.200000f, 0.153465f);
  486. defaultCameraFrustum.nearTopRight = AZ::Vector3(0.204621f, 0.200000f, 0.153465f);
  487. defaultCameraFrustum.nearBottomLeft = AZ::Vector3(-0.204621f, 0.200000f, -0.153465f);
  488. defaultCameraFrustum.nearBottomRight = AZ::Vector3(0.204621f, 0.200000f, -0.153465f);
  489. defaultCameraFrustum.farTopLeft = AZ::Vector3(-1047.656982f, 1024.000000f, 785.742737f);
  490. defaultCameraFrustum.farTopRight = AZ::Vector3(1047.656982f, 1024.000000f, 785.742737f);
  491. defaultCameraFrustum.farBottomLeft = AZ::Vector3(-1047.656982f, 1024.000000f, -785.742737f);
  492. defaultCameraFrustum.farBottomRight = AZ::Vector3(1047.656982f, 1024.000000f, -785.742737f);
  493. defaultCameraFrustum.testCaseName = "DefaultCamera";
  494. frustums.push_back(defaultCameraFrustum);
  495. // These frustums were generated from flying around StarterGame and dumping the frustum values for the viewport camera and shadow cascade frustums to a log file
  496. FrustumTestCase starterGame0;
  497. starterGame0.nearTopLeft = AZ::Vector3(41656.351563f, 794907.750000f, -604483.687500f);
  498. starterGame0.nearTopRight = AZ::Vector3(41662.343750f, 794907.375000f, -604483.687500f);
  499. starterGame0.nearBottomLeft = AZ::Vector3(41656.164063f, 794904.125000f, -604488.437500f);
  500. starterGame0.nearBottomRight = AZ::Vector3(41662.156250f, 794903.750000f, -604488.437500f);
  501. starterGame0.farTopLeft = AZ::Vector3(41677.691406f, 795314.937500f, -604793.375000f);
  502. starterGame0.farTopRight = AZ::Vector3(41683.683594f, 795314.562500f, -604793.375000f);
  503. starterGame0.farBottomLeft = AZ::Vector3(41677.503906f, 795311.312500f, -604798.125000f);
  504. starterGame0.farBottomRight = AZ::Vector3(41683.496094f, 795310.937500f, -604798.125000f);
  505. starterGame0.testCaseName = "StarterGame0";
  506. frustums.push_back(starterGame0);
  507. FrustumTestCase starterGame1;
  508. starterGame1.nearTopLeft = AZ::Vector3(0.166996f, 0.240156f, 0.117468f);
  509. starterGame1.nearTopRight = AZ::Vector3(0.226816f, -0.184736f, 0.117423f);
  510. starterGame1.nearBottomLeft = AZ::Vector3(0.169258f, 0.240499f, -0.113460f);
  511. starterGame1.nearBottomRight = AZ::Vector3(0.229078f, -0.184393f, -0.113506f);
  512. starterGame1.farTopLeft = AZ::Vector3(83.497986f, 120.077904f, 58.734165f);
  513. starterGame1.farTopRight = AZ::Vector3(113.408234f, -92.368172f, 58.711285f);
  514. starterGame1.farBottomLeft = AZ::Vector3(84.628860f, 120.249550f, -56.730221f);
  515. starterGame1.farBottomRight = AZ::Vector3(114.539108f, -92.196526f, -56.753101f);
  516. starterGame1.testCaseName = "StarterGame1";
  517. frustums.push_back(starterGame1);
  518. FrustumTestCase starterGame2;
  519. starterGame2.nearTopLeft = AZ::Vector3(-0.007508f, 0.091724f, -0.043323f);
  520. starterGame2.nearTopRight = AZ::Vector3(0.018772f, 0.090096f, -0.043323f);
  521. starterGame2.nearBottomLeft = AZ::Vector3(-0.008393f, 0.077435f, -0.065421f);
  522. starterGame2.nearBottomRight = AZ::Vector3(0.017887f, 0.075807f, -0.065421f);
  523. starterGame2.farTopLeft = AZ::Vector3(-11.637362f, 142.172897f, -67.151001f);
  524. starterGame2.farTopRight = AZ::Vector3(29.096817f, 139.649338f, -67.151001f);
  525. starterGame2.farBottomLeft = AZ::Vector3(-13.009484f, 120.024765f, -101.403290f);
  526. starterGame2.farBottomRight = AZ::Vector3(27.724697f, 117.501205f, -101.403290f);
  527. starterGame2.testCaseName = "StarterGame2";
  528. frustums.push_back(starterGame2);
  529. FrustumTestCase starterGame3;
  530. starterGame3.nearTopLeft = AZ::Vector3(0.211831f, -0.207308f, 0.107297f);
  531. starterGame3.nearTopRight = AZ::Vector3(-0.217216f, -0.201659f, 0.107297f);
  532. starterGame3.nearBottomLeft = AZ::Vector3(0.211954f, -0.197980f, -0.123455f);
  533. starterGame3.nearBottomRight = AZ::Vector3(-0.217093f, -0.192331f, -0.123455f);
  534. starterGame3.farTopLeft = AZ::Vector3(8473.246094f, -8292.310547f, 4291.892578f);
  535. starterGame3.farTopRight = AZ::Vector3(-8688.623047f, -8066.359863f, 4291.892578f);
  536. starterGame3.farBottomLeft = AZ::Vector3(8478.158203f, -7919.196777f, -4938.199219f);
  537. starterGame3.farBottomRight = AZ::Vector3(-8683.710938f, -7693.245605f, -4938.199219f);
  538. starterGame3.testCaseName = "StarterGame3";
  539. frustums.push_back(starterGame3);
  540. // For each test frustum, create test cases for every plane
  541. for (FrustumTestCase frustum : frustums)
  542. {
  543. for (int i = 0; i < AZ::Frustum::PlaneId::MAX; ++i)
  544. {
  545. frustum.plane = static_cast<AZ::Frustum::PlaneId>(i);
  546. testCases.push_back(frustum);
  547. }
  548. }
  549. return testCases;
  550. }
  551. std::string GenerateFrustumIntersectionTestCaseName(const ::testing::TestParamInfo<FrustumTestCase>& info)
  552. {
  553. std::string testCaseName = info.param.testCaseName;
  554. switch (info.param.plane)
  555. {
  556. case AZ::Frustum::PlaneId::Near:
  557. testCaseName += "_Near";
  558. break;
  559. case AZ::Frustum::PlaneId::Far:
  560. testCaseName += "_Far";
  561. break;
  562. case AZ::Frustum::PlaneId::Left:
  563. testCaseName += "_Left";
  564. break;
  565. case AZ::Frustum::PlaneId::Right:
  566. testCaseName += "_Right";
  567. break;
  568. case AZ::Frustum::PlaneId::Top:
  569. testCaseName += "_Top";
  570. break;
  571. case AZ::Frustum::PlaneId::Bottom:
  572. testCaseName += "_Bottom";
  573. break;
  574. }
  575. return testCaseName;
  576. }
  577. INSTANTIATE_TEST_SUITE_P(
  578. MATH_Frustum, Tests, ::testing::ValuesIn(GenerateFrustumIntersectionTestCases()),
  579. GenerateFrustumIntersectionTestCaseName);
  580. TEST(MATH_Frustum, AabbInsideOrientatedFrustum)
  581. {
  582. // position the frustum slightly along the x-axis looking down the negative x-axis
  583. const AZ::Vector3 frustumOrigin = AZ::Vector3(5.0f, 0.0f, 0.0f);
  584. const AZ::Quaternion frustumOrientation = AZ::Quaternion::CreateRotationZ(AZ::DegToRad(90.0f));
  585. const AZ::Transform frustumTransform =
  586. AZ::Transform::CreateFromQuaternionAndTranslation(frustumOrientation, frustumOrigin);
  587. const AZ::Frustum viewFrustum = AZ::Frustum(
  588. AZ::ViewFrustumAttributes(frustumTransform, 1920.0f / 1080.0f, AZ::DegToRad(60.0f), 0.1f, 100.0f));
  589. // position the aabb at the origin (inside the frustum's view)
  590. const AZ::Aabb aabb = AZ::Aabb::CreateFromMinMax(AZ::Vector3(-0.5f), AZ::Vector3(0.5f));
  591. // the aabb is contained within the frustum
  592. EXPECT_TRUE(AZ::ShapeIntersection::Contains(viewFrustum, aabb));
  593. }
  594. TEST(MATH_Frustum, FrustumCorners)
  595. {
  596. // position the frustum slightly along the x-axis looking down the negative x-axis
  597. const AZ::Vector3 frustumOrigin = AZ::Vector3(5.0f, 0.0f, 0.0f);
  598. const AZ::Quaternion frustumOrientation = AZ::Quaternion::CreateRotationZ(AZ::DegToRad(90.0f));
  599. const AZ::Transform frustumTransform =
  600. AZ::Transform::CreateFromQuaternionAndTranslation(frustumOrientation, frustumOrigin);
  601. const AZ::Frustum viewFrustum = AZ::Frustum(
  602. AZ::ViewFrustumAttributes(frustumTransform, 1920.0f / 1080.0f, AZ::DegToRad(60.0f), 0.1f, 100.0f));
  603. AZ::Frustum::CornerVertexArray corners;
  604. bool valid = viewFrustum.GetCorners(corners);
  605. EXPECT_TRUE(valid);
  606. // 8 unique positions all on 3 planes must be the 8 corners of the frustum.
  607. // The various corners should all be unique
  608. for (uint32_t i = 0; i < corners.size(); ++i)
  609. {
  610. for (uint32_t j = i + 1; j < corners.size(); ++j)
  611. {
  612. EXPECT_THAT(corners.at(i), testing::Not(IsClose(corners.at(j))));
  613. }
  614. }
  615. // The various corners should all lie on exactly 3 planes of the frustum
  616. for (auto corner : corners)
  617. {
  618. uint32_t onPlaneCount = 0;
  619. for (uint32_t planeId = 0; planeId < AZ::Frustum::PlaneId::MAX; ++planeId)
  620. {
  621. const float distanceToPlane = viewFrustum.GetPlane(AZ::Frustum::PlaneId(planeId)).GetPointDist(corner);
  622. if (AZStd::abs(distanceToPlane) < 0.001f)
  623. {
  624. ++onPlaneCount;
  625. }
  626. }
  627. EXPECT_EQ(onPlaneCount, 3);
  628. }
  629. }
  630. } // namespace UnitTest