ClipmapBoundsTests.cpp 16 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362
  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/UnitTest/TestTypes.h>
  9. #include <gmock/gmock.h>
  10. #include <TerrainRenderer/ClipmapBounds.h>
  11. #include <TerrainRenderer/Aabb2i.h>
  12. #include <AzCore/std/containers/span.h>
  13. #include <AzCore/std/containers/vector.h>
  14. namespace UnitTest
  15. {
  16. class ClipmapBoundsTests
  17. : public testing::Test
  18. {
  19. public:
  20. void CheckTransformRegionFullBounds(const Terrain::ClipmapBoundsDescriptor& desc);
  21. };
  22. void ClipmapBoundsTests::CheckTransformRegionFullBounds(const Terrain::ClipmapBoundsDescriptor& desc)
  23. {
  24. Terrain::ClipmapBounds bounds(desc);
  25. AZ::Aabb worldBounds = bounds.GetWorldBounds();
  26. float worldBoundsSize = worldBounds.GetXExtent();
  27. auto output = bounds.TransformRegion(worldBounds);
  28. ASSERT_EQ(output.size(), 4);
  29. AZ::Vector2 boundary = AZ::Vector2(
  30. floorf(worldBounds.GetMax().GetX() / worldBoundsSize),
  31. floorf(worldBounds.GetMax().GetY() / worldBoundsSize)
  32. ) * worldBoundsSize;
  33. Terrain::Vector2i localMax = {
  34. aznumeric_cast<int32_t>(AZStd::lround(desc.m_worldSpaceCenter.GetX() / desc.m_clipmapToWorldScale)),
  35. aznumeric_cast<int32_t>(AZStd::lround(desc.m_worldSpaceCenter.GetY() / desc.m_clipmapToWorldScale))
  36. };
  37. localMax += aznumeric_cast<int32_t>(desc.m_size / 2ul);
  38. int32_t intSize = int32_t(desc.m_size);
  39. Terrain::Vector2i localBoundary = {
  40. ((localMax.m_x % intSize) + intSize) % intSize,
  41. ((localMax.m_y % intSize) + intSize) % intSize
  42. };
  43. // Check each quadrant returned
  44. Terrain::ClipmapBoundsRegionList expected;
  45. expected.resize(4);
  46. expected.at(0).m_localAabb = Terrain::Aabb2i({localBoundary.m_x, localBoundary.m_y}, {intSize, intSize});
  47. expected.at(0).m_worldAabb = AZ::Aabb::CreateFromMinMaxValues(
  48. worldBounds.GetMin().GetX(), worldBounds.GetMin().GetY(), 0.0f,
  49. boundary.GetX(), boundary.GetY(), 0.0f);
  50. expected.at(1).m_localAabb = Terrain::Aabb2i({0, localBoundary.m_y}, {localBoundary.m_x, intSize});
  51. expected.at(1).m_worldAabb = AZ::Aabb::CreateFromMinMaxValues(
  52. boundary.GetX(), worldBounds.GetMin().GetY(), 0.0f,
  53. worldBounds.GetMax().GetX(), boundary.GetY(), 0.0f);
  54. expected.at(2).m_localAabb = Terrain::Aabb2i({localBoundary.m_x, 0}, {intSize, localBoundary.m_y});
  55. expected.at(2).m_worldAabb = AZ::Aabb::CreateFromMinMaxValues(
  56. worldBounds.GetMin().GetX(), boundary.GetY(), 0.0f,
  57. boundary.GetX(), worldBounds.GetMax().GetY(), 0.0f);
  58. expected.at(3).m_localAabb = Terrain::Aabb2i({ 0, 0 }, { localBoundary.m_x, localBoundary.m_y });
  59. expected.at(3).m_worldAabb = AZ::Aabb::CreateFromMinMaxValues(
  60. boundary.GetX(), boundary.GetY(), 0.0f,
  61. worldBounds.GetMax().GetX(), worldBounds.GetMax().GetY(), 0.0f);
  62. EXPECT_THAT(output, ::testing::UnorderedElementsAreArray(expected));
  63. }
  64. TEST_F(ClipmapBoundsTests, Construction)
  65. {
  66. Terrain::ClipmapBoundsDescriptor desc;
  67. Terrain::ClipmapBounds bounds(desc);
  68. }
  69. TEST_F(ClipmapBoundsTests, BasicTransform)
  70. {
  71. // Create clipmap around 0.0, so it's perfectly divided into 4 quadrants
  72. Terrain::ClipmapBoundsDescriptor desc;
  73. desc.m_worldSpaceCenter = AZ::Vector2(0.0f, 0.0f);
  74. desc.m_clipmapUpdateMultiple = 0;
  75. desc.m_clipmapToWorldScale = 1.0f;
  76. desc.m_size = 1024;
  77. Terrain::ClipmapBounds bounds(desc);
  78. auto output = bounds.TransformRegion(AZ::Aabb::CreateFromMinMaxValues(-512.0f, -512.0f, 0.0f, 512.0f, 512.0f, 0.0f));
  79. ASSERT_EQ(output.size(), 4);
  80. // Check each quadrant returned
  81. EXPECT_EQ(output.at(0).m_localAabb, Terrain::Aabb2i({512, 512}, {1024, 1024}));
  82. EXPECT_TRUE(output.at(0).m_worldAabb.IsClose(AZ::Aabb::CreateFromMinMaxValues(-512.0f, -512.0f, 0.0f, 0.0f, 0.0f, 0.0f)));
  83. EXPECT_EQ(output.at(1).m_localAabb, Terrain::Aabb2i({0, 512}, {512, 1024}));
  84. EXPECT_TRUE(output.at(1).m_worldAabb.IsClose(AZ::Aabb::CreateFromMinMaxValues(0.0f, -512.0f, 0.0f, 512.0f, 0.0f, 0.0f)));
  85. EXPECT_EQ(output.at(2).m_localAabb, Terrain::Aabb2i({512, 0}, {1024, 512}));
  86. EXPECT_TRUE(output.at(2).m_worldAabb.IsClose(AZ::Aabb::CreateFromMinMaxValues(-512.0f, 0.0f, 0.0f, 0.0f, 512.0f, 0.0f)));
  87. EXPECT_EQ(output.at(3).m_localAabb, Terrain::Aabb2i({0, 0}, {512, 512}));
  88. EXPECT_TRUE(output.at(3).m_worldAabb.IsClose(AZ::Aabb::CreateFromMinMaxValues(0.0f, 0.0f, 0.0f, 512.0f, 512.0f, 0.0f)));
  89. }
  90. TEST_F(ClipmapBoundsTests, ScaledTransform)
  91. {
  92. // Create clipmap around 0.0, so it's perfectly divided into 4 quadrants, but half-scale
  93. Terrain::ClipmapBoundsDescriptor desc;
  94. desc.m_worldSpaceCenter = AZ::Vector2(0.0f, 0.0f);
  95. desc.m_clipmapUpdateMultiple = 0;
  96. desc.m_clipmapToWorldScale = 0.5f;
  97. desc.m_size = 1024;
  98. Terrain::ClipmapBounds bounds(desc);
  99. auto output = bounds.TransformRegion(AZ::Aabb::CreateFromMinMaxValues(-256.0f, -256.0f, 0.0f, 256.0f, 256.0f, 0.0f));
  100. ASSERT_EQ(output.size(), 4);
  101. // Check each quadrant returned
  102. EXPECT_EQ(output.at(0).m_localAabb, Terrain::Aabb2i({512, 512}, {1024, 1024}));
  103. EXPECT_TRUE(output.at(0).m_worldAabb.IsClose(AZ::Aabb::CreateFromMinMaxValues(-256.0f, -256.0f, 0.0f, 0.0f, 0.0f, 0.0f)));
  104. EXPECT_EQ(output.at(1).m_localAabb, Terrain::Aabb2i({0, 512}, {512, 1024}));
  105. EXPECT_TRUE(output.at(1).m_worldAabb.IsClose(AZ::Aabb::CreateFromMinMaxValues(0.0f, -256.0f, 0.0f, 256.0f, 0.0f, 0.0f)));
  106. EXPECT_EQ(output.at(2).m_localAabb, Terrain::Aabb2i({512, 0}, {1024, 512}));
  107. EXPECT_TRUE(output.at(2).m_worldAabb.IsClose(AZ::Aabb::CreateFromMinMaxValues(-256.0f, 0.0f, 0.0f, 0.0f, 256.0f, 0.0f)));
  108. EXPECT_EQ(output.at(3).m_localAabb, Terrain::Aabb2i({0, 0}, {512, 512}));
  109. EXPECT_TRUE(output.at(3).m_worldAabb.IsClose(AZ::Aabb::CreateFromMinMaxValues(0.0f, 0.0f, 0.0f, 256.0f, 256.0f, 0.0f)));
  110. }
  111. TEST_F(ClipmapBoundsTests, ComplexTransformsFullBounds)
  112. {
  113. // Check 4 different clipmaps - one in completely positive space, one in negative space, and two straddling the axis
  114. // Clipmap in negative space
  115. {
  116. Terrain::ClipmapBoundsDescriptor desc;
  117. desc.m_worldSpaceCenter = AZ::Vector2(-1234.0f, -5432.0f);
  118. desc.m_clipmapUpdateMultiple = 0;
  119. desc.m_clipmapToWorldScale = 0.75f;
  120. desc.m_size = 512;
  121. CheckTransformRegionFullBounds(desc);
  122. }
  123. // Clipmap in positive space
  124. {
  125. Terrain::ClipmapBoundsDescriptor desc;
  126. desc.m_worldSpaceCenter = AZ::Vector2(1234.0f, 5432.0f);
  127. desc.m_clipmapUpdateMultiple = 0;
  128. desc.m_clipmapToWorldScale = 1.25f;
  129. desc.m_size = 1024;
  130. CheckTransformRegionFullBounds(desc);
  131. }
  132. // Clipmap on x axis
  133. {
  134. Terrain::ClipmapBoundsDescriptor desc;
  135. desc.m_worldSpaceCenter = AZ::Vector2(1234.0f, -100.0f);
  136. desc.m_clipmapUpdateMultiple = 0;
  137. desc.m_clipmapToWorldScale = 1.5f;
  138. desc.m_size = 256;
  139. CheckTransformRegionFullBounds(desc);
  140. }
  141. // Clipmap on y axis
  142. {
  143. Terrain::ClipmapBoundsDescriptor desc;
  144. desc.m_worldSpaceCenter = AZ::Vector2(-100.0f, 5432.0f);
  145. desc.m_clipmapUpdateMultiple = 0;
  146. desc.m_clipmapToWorldScale = 1.0f;
  147. desc.m_size = 2048;
  148. CheckTransformRegionFullBounds(desc);
  149. }
  150. }
  151. TEST_F(ClipmapBoundsTests, TransformSmallBounds)
  152. {
  153. // Create clipmap around 0.0, so it's perfectly divided into 4 quadrants
  154. Terrain::ClipmapBoundsDescriptor desc;
  155. desc.m_worldSpaceCenter = AZ::Vector2(0.0f, 0.0f);
  156. desc.m_clipmapUpdateMultiple = 0;
  157. desc.m_clipmapToWorldScale = 1.0f;
  158. desc.m_size = 1024;
  159. Terrain::ClipmapBounds bounds(desc);
  160. {
  161. // Single quadrant positive
  162. AZ::Aabb smallArea = AZ::Aabb::CreateFromMinMaxValues(
  163. 10.0f, 10.0f, 0.0f, 50.0f, 50.0f, 0.0f
  164. );
  165. auto output = bounds.TransformRegion(smallArea);
  166. ASSERT_EQ(output.size(), 1);
  167. EXPECT_EQ(output.at(0).m_localAabb, Terrain::Aabb2i({10, 10}, {50, 50}));
  168. EXPECT_TRUE(output.at(0).m_worldAabb.IsClose(AZ::Aabb::CreateFromMinMaxValues(10.0f, 10.0f, 0.0f, 50.0f, 50.0f, 0.0f)));
  169. }
  170. {
  171. // Single quadrant negative
  172. AZ::Aabb smallArea = AZ::Aabb::CreateFromMinMaxValues(
  173. -50.0f, -50.0f, 0.0f, -10.0f, -10.0f, 0.0f
  174. );
  175. auto output = bounds.TransformRegion(smallArea);
  176. ASSERT_EQ(output.size(), 1);
  177. EXPECT_EQ(output.at(0).m_localAabb, Terrain::Aabb2i({974, 974}, {1014, 1014}));
  178. EXPECT_TRUE(output.at(0).m_worldAabb.IsClose(AZ::Aabb::CreateFromMinMaxValues(-50.0f, -50.0f, 0.0f, -10.0f, -10.0f, 0.0f)));
  179. }
  180. {
  181. // 2 quadrant positive
  182. AZ::Aabb smallArea = AZ::Aabb::CreateFromMinMaxValues(
  183. 10.0f, -10.0f, 0.0f, 50.0f, 50.0f, 0.0f
  184. );
  185. auto output = bounds.TransformRegion(smallArea);
  186. ASSERT_EQ(output.size(), 2);
  187. EXPECT_EQ(output.at(0).m_localAabb, Terrain::Aabb2i({10, 1014}, {50, 1024}));
  188. EXPECT_TRUE(output.at(0).m_worldAabb.IsClose(AZ::Aabb::CreateFromMinMaxValues(10.0f, -10.0f, 0.0f, 50.0f, 0.0f, 0.0f)));
  189. EXPECT_EQ(output.at(1).m_localAabb, Terrain::Aabb2i({10, 0}, {50, 50}));
  190. EXPECT_TRUE(output.at(1).m_worldAabb.IsClose(AZ::Aabb::CreateFromMinMaxValues(10.0f, 0.0f, 0.0f, 50.0f, 50.0f, 0.0f)));
  191. }
  192. }
  193. TEST_F(ClipmapBoundsTests, MarginReducesUpdates)
  194. {
  195. // With a margin defined, the bounds should only trigger updates when the camera moves outside the margins
  196. // Create clipmap around 0.0, so it's perfectly divided into 4 quadrants
  197. Terrain::ClipmapBoundsDescriptor desc;
  198. desc.m_worldSpaceCenter = AZ::Vector2(0.0f, 0.0f);
  199. desc.m_clipmapUpdateMultiple = 16;
  200. desc.m_clipmapToWorldScale = 1.0f;
  201. desc.m_size = 1024;
  202. Terrain::ClipmapBounds bounds(desc);
  203. // center moved forward to 10, still within margin
  204. auto output1 = bounds.UpdateCenter(AZ::Vector2(10.0f, 10.0f));
  205. EXPECT_EQ(output1.size(), 0);
  206. // center moved forwrd to 20, beyond margin, triggers update
  207. auto output2 = bounds.UpdateCenter(AZ::Vector2(20.0f, 20.0f));
  208. EXPECT_GT(output2.size(), 0);
  209. // center moved back to 10, still within margin
  210. auto output3 = bounds.UpdateCenter(AZ::Vector2(10.0f, 10.0f));
  211. EXPECT_EQ(output3.size(), 0);
  212. // center moved back to 0, still within margin (on edge)
  213. auto output4 = bounds.UpdateCenter(AZ::Vector2(0.0f, 0.0f));
  214. EXPECT_EQ(output4.size(), 0);
  215. // center moved back to -10, beyond margin, triggers update
  216. auto output5 = bounds.UpdateCenter(AZ::Vector2(-10.0f, -10.0f));
  217. EXPECT_GT(output5.size(), 0);
  218. }
  219. TEST_F(ClipmapBoundsTests, CenterMovementUpdates)
  220. {
  221. // Create clipmap around 0.0, so it's perfectly divided into 4 quadrants
  222. Terrain::ClipmapBoundsDescriptor desc;
  223. desc.m_worldSpaceCenter = AZ::Vector2(0.0f, 0.0f);
  224. desc.m_clipmapUpdateMultiple = 16;
  225. desc.m_clipmapToWorldScale = 1.0f;
  226. desc.m_size = 1024;
  227. Terrain::ClipmapBounds bounds(desc);
  228. {
  229. AZ::Aabb untouchedRegion = AZ::Aabb::CreateNull();
  230. auto output = bounds.UpdateCenter(AZ::Vector2(20.0f, 20.0f), &untouchedRegion);
  231. ASSERT_EQ(output.size(), 4);
  232. // Instead of checking bounds directly, do several checks to make sure the bounds are appropriate. Since
  233. // the center moved just outside the margin along the diagonal, we should expect two edges to be updated
  234. // that are the width of the margin.
  235. // 1. The number of pixels updated in the bounds should be two sides of margin width
  236. float pixelsCovered = 0;
  237. for (auto& region : output)
  238. {
  239. // Note: GetSurfaceArea() returns the area of all 6 sides of the aabb. With a Z extent of 0, that
  240. // means that only the top and bottom will be counted, so we need to multiply by 0.5.
  241. pixelsCovered += region.m_worldAabb.GetSurfaceArea() * 0.5f;
  242. }
  243. // Two edges of margin * size, minus the overlap in the corner.
  244. const uint32_t updateMultiple = desc.m_clipmapUpdateMultiple;
  245. float expectedCoverage = updateMultiple * desc.m_size * 2.0f - updateMultiple * updateMultiple;
  246. EXPECT_NEAR(pixelsCovered, expectedCoverage, 0.0001f);
  247. // 2. The untouched region area should match what's expected
  248. float untouchedRegionArea = untouchedRegion.GetSurfaceArea() * 0.5f;
  249. float expectedUntouchedRegionSide = aznumeric_cast<float>(desc.m_size - desc.m_clipmapUpdateMultiple);
  250. float expectedUntouchedRegionArea = expectedUntouchedRegionSide * expectedUntouchedRegionSide;
  251. EXPECT_NEAR(untouchedRegionArea, expectedUntouchedRegionArea, 0.0001f);
  252. // 3. All of the update regions should be inside the world bounds of the clipmap
  253. AZ::Aabb worldBounds = bounds.GetWorldBounds();
  254. for (auto& region : output)
  255. {
  256. EXPECT_EQ(region.m_worldAabb.GetClamped(worldBounds), region.m_worldAabb);
  257. }
  258. // 4. The untouched region should also be inside the world bounds of the clipmap;
  259. EXPECT_EQ(untouchedRegion.GetClamped(worldBounds), untouchedRegion);
  260. // 5. None of the update regions should overlap each other or the untouched region
  261. // push the untouched region on the vector to make comparisons easier
  262. output.push_back(Terrain::ClipmapBoundsRegion({untouchedRegion, Terrain::Aabb2i({}) }));
  263. for (uint32_t i = 0; i < output.size(); ++i)
  264. {
  265. const AZ::Aabb boundsToCheck = output.at(i).m_worldAabb;
  266. for (uint32_t j = i + 1; j < output.size(); ++j)
  267. {
  268. // AZ::Aabb::Overlaps() counts touching edges as overlapping, so we need a strict version
  269. auto strictOverlaps = [](const AZ::Aabb& aabb1, const AZ::Aabb& aabb2) -> bool
  270. {
  271. return aabb1.GetMin().IsLessThan(aabb2.GetMax()) &&
  272. aabb1.GetMax().IsGreaterThan(aabb2.GetMin());
  273. };
  274. EXPECT_FALSE(strictOverlaps(boundsToCheck, output.at(j).m_worldAabb));
  275. }
  276. }
  277. }
  278. }
  279. // This test is to ensure clipmap update compute shader receives 6 regions at most.
  280. TEST_F(ClipmapBoundsTests, MaxUpdateRegionTest)
  281. {
  282. // The initial clipmap is divided into 4 parts.
  283. // By traversing the 11x11 grid, all possible overlapping cases can be covered.
  284. for (int32_t i = -5; i <= 5; ++i)
  285. {
  286. for (int32_t j = -5; j <= 5; ++j)
  287. {
  288. Terrain::ClipmapBoundsDescriptor desc;
  289. desc.m_worldSpaceCenter = AZ::Vector2(0.0f, 0.0f);
  290. desc.m_clipmapUpdateMultiple = 0;
  291. desc.m_clipmapToWorldScale = 1.0f;
  292. desc.m_size = 1024;
  293. Terrain::ClipmapBounds bounds(desc);
  294. auto list = bounds.UpdateCenter(AZ::Vector2(256.0f * i, 256.0f * j));
  295. uint32_t size = aznumeric_cast<uint32_t>(list.size());
  296. EXPECT_LE(size, Terrain::ClipmapBounds::MaxUpdateRegions);
  297. }
  298. }
  299. }
  300. }