TerrainSystemSettingsTests.cpp 15 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311
  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/Component/ComponentApplication.h>
  9. #include <AzCore/Jobs/JobManagerComponent.h>
  10. #include <AzCore/std/parallel/semaphore.h>
  11. #include <AzTest/AzTest.h>
  12. #include <AZTestShared/Math/MathTestHelpers.h>
  13. #include <TerrainSystem/TerrainSystem.h>
  14. #include <Components/TerrainLayerSpawnerComponent.h>
  15. #include <GradientSignal/Ebuses/MockGradientRequestBus.h>
  16. #include <Tests/Mocks/Terrain/MockTerrainDataRequestBus.h>
  17. #include <Terrain/MockTerrainAreaSurfaceRequestBus.h>
  18. #include <Terrain/MockTerrain.h>
  19. #include <MockAxisAlignedBoxShapeComponent.h>
  20. #include <TerrainTestFixtures.h>
  21. namespace UnitTest
  22. {
  23. using ::testing::NiceMock;
  24. using ::testing::Return;
  25. class TerrainSystemSettingsTests
  26. : public TerrainBaseFixture
  27. , public ::testing::Test
  28. {
  29. protected:
  30. // Defines a structure for defining both an XY position and the expected height for that position.
  31. struct HeightTestPoint
  32. {
  33. AZ::Vector2 m_testLocation = AZ::Vector2::CreateZero();
  34. float m_expectedHeight = 0.0f;
  35. };
  36. AZStd::unique_ptr<NiceMock<UnitTest::MockBoxShapeComponentRequests>> m_boxShapeRequests;
  37. AZStd::unique_ptr<NiceMock<UnitTest::MockShapeComponentRequests>> m_shapeRequests;
  38. AZStd::unique_ptr<NiceMock<UnitTest::MockTerrainAreaHeightRequests>> m_terrainAreaHeightRequests;
  39. AZStd::unique_ptr<NiceMock<UnitTest::MockTerrainAreaSurfaceRequestBus>> m_terrainAreaSurfaceRequests;
  40. void SetUp() override
  41. {
  42. SetupCoreSystems();
  43. }
  44. void TearDown() override
  45. {
  46. m_boxShapeRequests.reset();
  47. m_shapeRequests.reset();
  48. m_terrainAreaHeightRequests.reset();
  49. m_terrainAreaSurfaceRequests.reset();
  50. TearDownCoreSystems();
  51. }
  52. AZStd::unique_ptr<AZ::Entity> CreateAndActivateMockTerrainLayerSpawnerThatReturnsXSquaredAsHeight(const AZ::Aabb& spawnerBox)
  53. {
  54. // Create the base entity with a mock box shape, Terrain Layer Spawner, and height provider.
  55. // Turn off the "use ground plane" setting so that we mark terrain as false anywhere that the spawner doesn't exist.
  56. Terrain::TerrainLayerSpawnerConfig config;
  57. config.m_useGroundPlane = false;
  58. auto entity = CreateEntity();
  59. entity->CreateComponent<UnitTest::MockAxisAlignedBoxShapeComponent>();
  60. entity->CreateComponent<Terrain::TerrainLayerSpawnerComponent>(config);
  61. m_boxShapeRequests = AZStd::make_unique<NiceMock<UnitTest::MockBoxShapeComponentRequests>>(entity->GetId());
  62. m_shapeRequests = AZStd::make_unique<NiceMock<UnitTest::MockShapeComponentRequests>>(entity->GetId());
  63. // Set up the box shape to return whatever spawnerBox was passed in.
  64. ON_CALL(*m_shapeRequests, GetEncompassingAabb).WillByDefault(Return(spawnerBox));
  65. auto mockHeights = [](const AZ::Vector3& inPosition, AZ::Vector3& outPosition, bool& terrainExists)
  66. {
  67. // Return a height (Z) that's equal to X^2.
  68. outPosition = AZ::Vector3(inPosition.GetX(), inPosition.GetY(), inPosition.GetX() * inPosition.GetX());
  69. terrainExists = true;
  70. };
  71. // Set up a mock height provider that returns the X position as the height.
  72. m_terrainAreaHeightRequests = AZStd::make_unique<NiceMock<UnitTest::MockTerrainAreaHeightRequests>>(entity->GetId());
  73. ON_CALL(*m_terrainAreaHeightRequests, GetHeight).WillByDefault(mockHeights);
  74. ON_CALL(*m_terrainAreaHeightRequests, GetHeights)
  75. .WillByDefault(
  76. [mockHeights](AZStd::span<AZ::Vector3> inOutPositionList, AZStd::span<bool> terrainExistsList)
  77. {
  78. for (int i = 0; i < inOutPositionList.size(); i++)
  79. {
  80. mockHeights(inOutPositionList[i], inOutPositionList[i], terrainExistsList[i]);
  81. }
  82. });
  83. ActivateEntity(entity.get());
  84. return entity;
  85. }
  86. void SetupSurfaceWeightMocks(AZ::Entity* entity)
  87. {
  88. auto mockGetSurfaceWeights = [](
  89. const AZ::Vector3& position,
  90. AzFramework::SurfaceData::SurfaceTagWeightList& surfaceWeights)
  91. {
  92. const SurfaceData::SurfaceTag tag1 = SurfaceData::SurfaceTag("tag1");
  93. AzFramework::SurfaceData::SurfaceTagWeight tagWeight1;
  94. tagWeight1.m_surfaceType = tag1;
  95. tagWeight1.m_weight = position.GetX() / 100.0f;
  96. surfaceWeights.clear();
  97. surfaceWeights.push_back(tagWeight1);
  98. };
  99. m_terrainAreaSurfaceRequests = AZStd::make_unique<NiceMock<UnitTest::MockTerrainAreaSurfaceRequestBus>>(entity->GetId());
  100. ON_CALL(*m_terrainAreaSurfaceRequests, GetSurfaceWeights).WillByDefault(mockGetSurfaceWeights);
  101. ON_CALL(*m_terrainAreaSurfaceRequests, GetSurfaceWeightsFromList).WillByDefault(
  102. [mockGetSurfaceWeights](
  103. AZStd::span<const AZ::Vector3> inPositionList,
  104. AZStd::span<AzFramework::SurfaceData::SurfaceTagWeightList> outSurfaceWeightsList)
  105. {
  106. for (size_t i = 0; i < inPositionList.size(); i++)
  107. {
  108. mockGetSurfaceWeights(inPositionList[i], outSurfaceWeightsList[i]);
  109. }
  110. }
  111. );
  112. }
  113. };
  114. TEST_F(TerrainSystemSettingsTests, TerrainWorldMinMaxClampsHeightData)
  115. {
  116. // Verify that any height data returned from a terrain layer spawner is clamped to the world min/max settings.
  117. // Create a mock terrain layer spawner that uses a box of (0,0,0) - (20,20,20) and generates a height equal to the X value squared.
  118. // The world min/max will be set to 5 and 15, so we'll verify the heights are always between 5 and 15.
  119. const AZ::Aabb spawnerBox = AZ::Aabb::CreateFromMinMaxValues(0.0f, 0.0f, 0.0f, 20.0f, 20.0f, 20.0f);
  120. auto entity = CreateAndActivateMockTerrainLayerSpawnerThatReturnsXSquaredAsHeight(spawnerBox);
  121. // Create and activate the terrain system with world height min/max of 5 and 15.
  122. const float queryResolution = 1.0f;
  123. AzFramework::Terrain::FloatRange heightBounds = { 5.0f, 15.0f };
  124. auto terrainSystem = CreateAndActivateTerrainSystem(queryResolution, heightBounds);
  125. // Test a set of points from (0,0) - (20,20). If the world min/max clamp is working, we should always get 5 <= height <= 15.
  126. for (float x = 0.0f; x <= 20.0f; x += queryResolution)
  127. {
  128. AZ::Vector3 position(x, x, 0.0f);
  129. bool heightQueryTerrainExists = false;
  130. float height =
  131. terrainSystem->GetHeight(position, AzFramework::Terrain::TerrainDataRequests::Sampler::DEFAULT, &heightQueryTerrainExists);
  132. // Verify all the heights are between 5 and 15.
  133. EXPECT_GE(height, heightBounds.m_min);
  134. EXPECT_LE(height, heightBounds.m_max);
  135. }
  136. }
  137. TEST_F(TerrainSystemSettingsTests, TerrainHeightQueryResolutionAffectsHeightQueries)
  138. {
  139. // Verify that the terrain height query resolution setting affects height queries. We'll verify this by
  140. // setting the height query resolution to 10 and querying a set of positions from 0 - 20 that return
  141. // the X^2 value as the height.
  142. // If the height query resolution is working, when we use the CLAMP sampler, queries for X=0-9 should return 0, and
  143. // queries for X=10-19 should return 100.
  144. // When we use the EXACT sampler, the query resolution should be ignored and we should get back X^2.
  145. // When we use the BILINEAR sampler, queries for X=0-9 should return values from 0^2-10^2, and X=10-19 should return
  146. // values from 10^2-20^2.
  147. // Create a mock terrain layer spawner that uses a box of (0,0,0) - (30,30,1000) and generates
  148. // a height equal to the X value squared. (We set the max height high enough to allow for the X^2 values without clamping)
  149. const AZ::Aabb spawnerBox = AZ::Aabb::CreateFromMinMaxValues(0.0f, 0.0f, 0.0f, 30.0f, 30.0f, 1000.0f);
  150. auto entity = CreateAndActivateMockTerrainLayerSpawnerThatReturnsXSquaredAsHeight(spawnerBox);
  151. // Create and activate the terrain system with a world bounds that matches the spawner box, and a query resolution of 10.
  152. const float queryResolution = 10.0f;
  153. AzFramework::Terrain::FloatRange heightBounds = { spawnerBox.GetMin().GetZ(), spawnerBox.GetMax().GetZ() };
  154. auto terrainSystem = CreateAndActivateTerrainSystem(queryResolution, heightBounds);
  155. for (auto sampler : { AzFramework::Terrain::TerrainDataRequests::Sampler::BILINEAR,
  156. AzFramework::Terrain::TerrainDataRequests::Sampler::CLAMP,
  157. AzFramework::Terrain::TerrainDataRequests::Sampler::EXACT })
  158. {
  159. // Test a set of points from (0,0) - (20,20). We stop at 20 so that we don't test interpolation with points that don't
  160. // exist on the max boundary edge of 30.
  161. for (float x = 0.0f; x < 20.0f; x += 1.0f)
  162. {
  163. AZ::Vector3 position(x, x, 0.0f);
  164. bool terrainExists = false;
  165. float height =
  166. terrainSystem->GetHeight(position, sampler, &terrainExists);
  167. switch (sampler)
  168. {
  169. case AzFramework::Terrain::TerrainDataRequests::Sampler::BILINEAR:
  170. if (x < 10.0f)
  171. {
  172. // Values from 0-10 should linearly interpolate from 0^2 to 10^2
  173. EXPECT_NEAR(height, AZStd::lerp(0.0f, 100.0f, x / queryResolution), 0.001f);
  174. }
  175. else
  176. {
  177. // Values from 10-19 should linearly interpolate from 10^2 to 20^2
  178. EXPECT_NEAR(height, AZStd::lerp(100.0f, 400.0f, (x - queryResolution) / queryResolution), 0.001f);
  179. }
  180. EXPECT_TRUE(terrainExists);
  181. break;
  182. case AzFramework::Terrain::TerrainDataRequests::Sampler::CLAMP:
  183. // X values from 0-4 should round to X=0 and return 0, X values from 5-14 should round to X=10 and return 10^2,
  184. // and X values from 15-19 should round up to X=20 and return 20^2.
  185. if (x < 5.0f)
  186. {
  187. EXPECT_EQ(height, 0.0f);
  188. EXPECT_TRUE(terrainExists);
  189. }
  190. else if (x < 15.0f)
  191. {
  192. EXPECT_EQ(height, 100.0f);
  193. EXPECT_TRUE(terrainExists);
  194. }
  195. else
  196. {
  197. EXPECT_EQ(height, 400.0f);
  198. }
  199. break;
  200. case AzFramework::Terrain::TerrainDataRequests::Sampler::EXACT:
  201. // All query points should return X^2
  202. EXPECT_EQ(height, x*x);
  203. EXPECT_TRUE(terrainExists);
  204. break;
  205. }
  206. }
  207. }
  208. }
  209. TEST_F(TerrainSystemSettingsTests, TerrainSurfaceQueryResolutionAffectsSurfaceQueries)
  210. {
  211. // Verify that the terrain surface query resolution setting affects surface queries. We'll verify this by
  212. // setting the surface query resolution to 10 and querying a set of positions from 0 - 20 that return
  213. // the X value / 100 as the surface weight.
  214. // If the surface query resolution is working, when we use the CLAMP sampler, queries for X=0-9 should return (0/100), and
  215. // queries for X=10-19 should return (10/100).
  216. // When we use the EXACT sampler, the query resolution should be ignored and we should get back (X/100).
  217. // When we use the BILINEAR sampler, we should get back the same results as the CLAMP sampler, because currently the two
  218. // are interpreted the same way for surface queries.
  219. // Create a mock terrain layer spawner that uses a box of (0,0,0) - (30,30,30).
  220. const AZ::Aabb spawnerBox = AZ::Aabb::CreateFromMinMaxValues(0.0f, 0.0f, 0.0f, 30.0f, 30.0f, 30.0f);
  221. auto entity = CreateAndActivateMockTerrainLayerSpawnerThatReturnsXSquaredAsHeight(spawnerBox);
  222. // Set up the surface weight mocks that will return X/100 as the surface weight.
  223. SetupSurfaceWeightMocks(entity.get());
  224. // Create and activate the terrain system with a world bounds that matches the spawner box, and a query resolution of 10.
  225. const float heightQueryResolution = 1.0f;
  226. const float surfaceQueryResolution = 10.0f;
  227. AzFramework::Terrain::FloatRange heightBounds = { spawnerBox.GetMin().GetZ(), spawnerBox.GetMax().GetZ() };
  228. auto terrainSystem = CreateAndActivateTerrainSystem(heightQueryResolution, surfaceQueryResolution, heightBounds);
  229. for (auto sampler : { AzFramework::Terrain::TerrainDataRequests::Sampler::BILINEAR,
  230. AzFramework::Terrain::TerrainDataRequests::Sampler::CLAMP,
  231. AzFramework::Terrain::TerrainDataRequests::Sampler::EXACT })
  232. {
  233. // Test a set of points from (0,0) - (20,20). We stop at 20 instead of 30 so that we aren't testing what happens when
  234. // a query point doesn't exist.
  235. for (float x = 0.0f; x < 20.0f; x += 1.0f)
  236. {
  237. AZ::Vector3 position(x, x, 0.0f);
  238. bool terrainExists = false;
  239. AzFramework::SurfaceData::SurfaceTagWeight weight =
  240. terrainSystem->GetMaxSurfaceWeight(position, sampler, &terrainExists);
  241. switch (sampler)
  242. {
  243. case AzFramework::Terrain::TerrainDataRequests::Sampler::BILINEAR:
  244. [[fallthrough]];
  245. case AzFramework::Terrain::TerrainDataRequests::Sampler::CLAMP:
  246. // For both BILINEAR and CLAMP:
  247. // X values from 0-4 should round to X=0 and return (0/100), X values from 5-14 should round to X=10
  248. // and return (10/100), and X values from 15-19 should round up to X=20 and return (20/100).
  249. // and X values from 15-19 should round up to X=20 and return 20^2.
  250. if (x < 5.0f)
  251. {
  252. EXPECT_NEAR(weight.m_weight, 0.0f, 0.001f);
  253. }
  254. else if (x < 15.0f)
  255. {
  256. EXPECT_NEAR(weight.m_weight, 0.1f, 0.001f);
  257. }
  258. else
  259. {
  260. EXPECT_NEAR(weight.m_weight, 0.2f, 0.001f);
  261. }
  262. break;
  263. break;
  264. case AzFramework::Terrain::TerrainDataRequests::Sampler::EXACT:
  265. // For EXACT, queries should just return x/100 and ignore the query resolution.
  266. EXPECT_NEAR(weight.m_weight, x / 100.0f, 0.001f);
  267. break;
  268. }
  269. }
  270. }
  271. }
  272. } // namespace UnitTest