TerrainPhysicsColliderTests.cpp 38 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733734735736737738739740741742743744745746747748749750751752753754755756757758759760761762763764765766767768769770771772773774775776777778779780781782783784785786787788789790791792793794
  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/Casting/lossy_cast.h>
  9. #include <AzCore/Component/ComponentApplication.h>
  10. #include <AzCore/Component/TransformBus.h>
  11. #include <AzFramework/Terrain/TerrainDataRequestBus.h>
  12. #include <AzFramework/Physics/Mocks/MockHeightfieldProviderBus.h>
  13. #include <Components/TerrainPhysicsColliderComponent.h>
  14. #include <LmbrCentral/Shape/ShapeComponentBus.h>
  15. #include <LmbrCentral/Shape/BoxShapeComponentBus.h>
  16. #include <LmbrCentral/Shape/MockShapes.h>
  17. #include <AzTest/AzTest.h>
  18. #include <MockAxisAlignedBoxShapeComponent.h>
  19. #include <Tests/Mocks/Terrain/MockTerrainDataRequestBus.h>
  20. #include <TerrainTestFixtures.h>
  21. using ::testing::NiceMock;
  22. using ::testing::AtLeast;
  23. using ::testing::_;
  24. using ::testing::Return;
  25. class TerrainPhysicsColliderComponentTest
  26. : public UnitTest::TerrainTestFixture
  27. {
  28. protected:
  29. AZStd::unique_ptr<AZ::Entity> m_entity;
  30. Terrain::TerrainPhysicsColliderComponent* m_colliderComponent;
  31. UnitTest::MockAxisAlignedBoxShapeComponent* m_boxComponent;
  32. void SetUp() override
  33. {
  34. UnitTest::TerrainTestFixture::SetUp();
  35. m_entity = CreateEntity();
  36. m_boxComponent = m_entity->CreateComponent<UnitTest::MockAxisAlignedBoxShapeComponent>();
  37. }
  38. void TearDown() override
  39. {
  40. m_entity.reset();
  41. }
  42. void AddTerrainPhysicsColliderToEntity(const Terrain::TerrainPhysicsColliderConfig& configuration)
  43. {
  44. m_colliderComponent = m_entity->CreateComponent<Terrain::TerrainPhysicsColliderComponent>(configuration);
  45. }
  46. void ProcessRegionLoop(AzFramework::Terrain::TerrainQueryRegion queryRegion,
  47. AzFramework::Terrain::SurfacePointRegionFillCallback perPositionCallback,
  48. AzFramework::SurfaceData::SurfaceTagWeightList* surfaceTags,
  49. const AZStd::function<float(float, float)>& heightGenerator)
  50. {
  51. if (!perPositionCallback)
  52. {
  53. return;
  54. }
  55. AzFramework::SurfaceData::SurfacePoint surfacePoint;
  56. for (size_t y = 0; y < queryRegion.m_numPointsY; y++)
  57. {
  58. float fy = aznumeric_cast<float>(queryRegion.m_startPoint.GetY() + (y * queryRegion.m_stepSize.GetY()));
  59. for (size_t x = 0; x < queryRegion.m_numPointsX; x++)
  60. {
  61. bool terrainExists = false;
  62. float fx = aznumeric_cast<float>(queryRegion.m_startPoint.GetX() + (x * queryRegion.m_stepSize.GetX()));
  63. surfacePoint.m_position.Set(fx, fy, heightGenerator(fx, fy));
  64. if (surfaceTags)
  65. {
  66. surfacePoint.m_surfaceTags.clear();
  67. if (fy < 128.0)
  68. {
  69. surfacePoint.m_surfaceTags.push_back(surfaceTags->at(0));
  70. }
  71. else
  72. {
  73. surfacePoint.m_surfaceTags.push_back(surfaceTags->at(1));
  74. }
  75. }
  76. perPositionCallback(x, y, surfacePoint, terrainExists);
  77. }
  78. }
  79. }
  80. };
  81. TEST_F(TerrainPhysicsColliderComponentTest, ActivateEntityActivateSuccess)
  82. {
  83. // Check that the entity activates with a collider and the required shape attached.
  84. AddTerrainPhysicsColliderToEntity(Terrain::TerrainPhysicsColliderConfig());
  85. ActivateEntity(m_entity.get());
  86. EXPECT_EQ(m_entity->GetState(), AZ::Entity::State::Active);
  87. }
  88. TEST_F(TerrainPhysicsColliderComponentTest, TerrainPhysicsColliderTransformChangedNotifiesHeightfieldBus)
  89. {
  90. // Check that the HeightfieldBus is notified when the transform of the entity changes.
  91. AddTerrainPhysicsColliderToEntity(Terrain::TerrainPhysicsColliderConfig());
  92. ActivateEntity(m_entity.get());
  93. NiceMock<UnitTest::MockHeightfieldProviderNotificationBusListener> heightfieldListener(m_entity->GetId());
  94. EXPECT_CALL(heightfieldListener, OnHeightfieldDataChanged(_,_)).Times(1);
  95. // The component gets transform change notifications via the shape bus.
  96. LmbrCentral::ShapeComponentNotificationsBus::Event(
  97. m_entity->GetId(), &LmbrCentral::ShapeComponentNotificationsBus::Events::OnShapeChanged,
  98. LmbrCentral::ShapeComponentNotifications::ShapeChangeReasons::TransformChanged);
  99. }
  100. TEST_F(TerrainPhysicsColliderComponentTest, TerrainPhysicsColliderShapeChangedNotifiesHeightfieldBus)
  101. {
  102. // Check that the Heightfield bus is notified when the shape component changes.
  103. AddTerrainPhysicsColliderToEntity(Terrain::TerrainPhysicsColliderConfig());
  104. ActivateEntity(m_entity.get());
  105. NiceMock<UnitTest::MockHeightfieldProviderNotificationBusListener> heightfieldListener(m_entity->GetId());
  106. EXPECT_CALL(heightfieldListener, OnHeightfieldDataChanged(_,_)).Times(1);
  107. LmbrCentral::ShapeComponentNotificationsBus::Event(
  108. m_entity->GetId(), &LmbrCentral::ShapeComponentNotificationsBus::Events::OnShapeChanged,
  109. LmbrCentral::ShapeComponentNotifications::ShapeChangeReasons::ShapeChanged);
  110. }
  111. TEST_F(TerrainPhysicsColliderComponentTest, TerrainPhysicsColliderReturnsAlignedRowBoundsCorrectly)
  112. {
  113. // Check that the heightfield grid size is correct when the shape bounds match the grid resolution.
  114. AddTerrainPhysicsColliderToEntity(Terrain::TerrainPhysicsColliderConfig());
  115. const float boundsMin = 0.0f;
  116. const float boundsMax = 1024.0f;
  117. NiceMock<UnitTest::MockShapeComponentRequests> boxShape(m_entity->GetId());
  118. const AZ::Aabb bounds = AZ::Aabb::CreateFromMinMax(AZ::Vector3(boundsMin), AZ::Vector3(boundsMax));
  119. ON_CALL(boxShape, GetEncompassingAabb).WillByDefault(Return(bounds));
  120. float mockHeightResolution = 1.0f;
  121. NiceMock<UnitTest::MockTerrainDataRequests> terrainListener;
  122. ON_CALL(terrainListener, GetTerrainHeightQueryResolution).WillByDefault(Return(mockHeightResolution));
  123. ActivateEntity(m_entity.get());
  124. size_t cols, rows;
  125. Physics::HeightfieldProviderRequestsBus::Event(
  126. m_entity->GetId(), &Physics::HeightfieldProviderRequestsBus::Events::GetHeightfieldGridSize, cols, rows);
  127. // "max - min" gives us the number of grid squares, "max - min + 1" gives us the number of grid vertices including the final endcap.
  128. const int32_t expectedGridSize = aznumeric_cast<int32_t>(boundsMax - boundsMin) + 1;
  129. // With the bounds set at 0-1024 and a resolution of 1.0, the heightfield grid should be 1025x1025, because it
  130. // should have a final set of vertices to end the grid.
  131. // ex: bounds set from 0-2 would generate *--*--* , which is 3 points, but 2 grid boxes.
  132. EXPECT_EQ(cols, expectedGridSize);
  133. EXPECT_EQ(rows, expectedGridSize);
  134. }
  135. TEST_F(TerrainPhysicsColliderComponentTest, TerrainPhysicsColliderConstrictsMinBoundsCorrectly)
  136. {
  137. // Check that the heightfield grid is correctly constricted if the minimum value of the bounds doesn't land directly
  138. // on a terrain grid boundary line.
  139. AddTerrainPhysicsColliderToEntity(Terrain::TerrainPhysicsColliderConfig());
  140. const float boundsMin = 0.1f;
  141. const float boundsMax = 1024.0f;
  142. NiceMock<UnitTest::MockShapeComponentRequests> boxShape(m_entity->GetId());
  143. const AZ::Aabb bounds = AZ::Aabb::CreateFromMinMax(AZ::Vector3(boundsMin), AZ::Vector3(boundsMax));
  144. ON_CALL(boxShape, GetEncompassingAabb).WillByDefault(Return(bounds));
  145. float mockHeightResolution = 1.0f;
  146. NiceMock<UnitTest::MockTerrainDataRequests> terrainListener;
  147. ON_CALL(terrainListener, GetTerrainHeightQueryResolution).WillByDefault(Return(mockHeightResolution));
  148. ActivateEntity(m_entity.get());
  149. size_t cols, rows;
  150. Physics::HeightfieldProviderRequestsBus::Event(
  151. m_entity->GetId(), &Physics::HeightfieldProviderRequestsBus::Events::GetHeightfieldGridSize, cols, rows);
  152. // "max - min" gives us the number of grid squares, "max - min + 1" gives us the number of grid vertices including the final endcap.
  153. // Note that this is also rounding down via the int truncation
  154. const int32_t expectedGridSize = aznumeric_cast<int32_t>(boundsMax - boundsMin) + 1;
  155. // If the heightfield is not constricted to stay within the shape bounds, the values returned would be 1025.
  156. EXPECT_EQ(cols, expectedGridSize);
  157. EXPECT_EQ(rows, expectedGridSize);
  158. }
  159. TEST_F(TerrainPhysicsColliderComponentTest, TerrainPhysicsColliderConstrictsMaxBoundsCorrectly)
  160. {
  161. // Check that the heightfield grid is correctly constricted if the maximum value of the bounds doesn't land directly
  162. // on a terrain grid boundary line.
  163. AddTerrainPhysicsColliderToEntity(Terrain::TerrainPhysicsColliderConfig());
  164. const float boundsMin = 0.0f;
  165. const float boundsMax = 1023.5f;
  166. NiceMock<UnitTest::MockShapeComponentRequests> boxShape(m_entity->GetId());
  167. const AZ::Aabb bounds = AZ::Aabb::CreateFromMinMax(AZ::Vector3(boundsMin), AZ::Vector3(boundsMax));
  168. ON_CALL(boxShape, GetEncompassingAabb).WillByDefault(Return(bounds));
  169. float mockHeightResolution = 1.0f;
  170. NiceMock<UnitTest::MockTerrainDataRequests> terrainListener;
  171. ON_CALL(terrainListener, GetTerrainHeightQueryResolution).WillByDefault(Return(mockHeightResolution));
  172. ActivateEntity(m_entity.get());
  173. size_t cols, rows;
  174. Physics::HeightfieldProviderRequestsBus::Event(
  175. m_entity->GetId(), &Physics::HeightfieldProviderRequestsBus::Events::GetHeightfieldGridSize, cols, rows);
  176. // "max - min" gives us the number of grid squares, "max - min + 1" gives us the number of grid vertices including the final endcap.
  177. // Note that this is also rounding down via the int truncation
  178. const int32_t expectedGridSize = aznumeric_cast<int32_t>(boundsMax - boundsMin) + 1;
  179. // If the heightfield is not constricted to stay within the shape bounds, the values returned would be 1025.
  180. EXPECT_EQ(cols, expectedGridSize);
  181. EXPECT_EQ(rows, expectedGridSize);
  182. }
  183. TEST_F(TerrainPhysicsColliderComponentTest, TerrainPhysicsColliderGetHeightsReturnsHeights)
  184. {
  185. // Check that the TerrainPhysicsCollider returns a heightfield of the expected size.
  186. AddTerrainPhysicsColliderToEntity(Terrain::TerrainPhysicsColliderConfig());
  187. const float boundsMin = 0.0f;
  188. const float boundsMax = 1024.0f;
  189. NiceMock<UnitTest::MockShapeComponentRequests> boxShape(m_entity->GetId());
  190. const AZ::Aabb bounds = AZ::Aabb::CreateFromMinMax(AZ::Vector3(boundsMin), AZ::Vector3(boundsMax));
  191. ON_CALL(boxShape, GetEncompassingAabb).WillByDefault(Return(bounds));
  192. float mockHeightResolution = 1.0f;
  193. NiceMock<UnitTest::MockTerrainDataRequests> terrainListener;
  194. ON_CALL(terrainListener, GetTerrainHeightQueryResolution).WillByDefault(Return(mockHeightResolution));
  195. ON_CALL(terrainListener, QueryRegion).WillByDefault(
  196. [this](
  197. const AzFramework::Terrain::TerrainQueryRegion& queryRegion,
  198. [[maybe_unused]] AzFramework::Terrain::TerrainDataRequests::TerrainDataMask requestedData,
  199. AzFramework::Terrain::SurfacePointRegionFillCallback perPositionCallback,
  200. [[maybe_unused]] AzFramework::Terrain::TerrainDataRequests::Sampler sampleFilter)
  201. {
  202. ProcessRegionLoop(queryRegion, perPositionCallback, nullptr,
  203. []([[maybe_unused]]float x, [[maybe_unused]]float y){ return 0.0f; });
  204. }
  205. );
  206. ON_CALL(terrainListener, QueryRegionAsync)
  207. .WillByDefault(
  208. [this](
  209. const AzFramework::Terrain::TerrainQueryRegion& queryRegion,
  210. [[maybe_unused]] AzFramework::Terrain::TerrainDataRequests::TerrainDataMask requestedData,
  211. AzFramework::Terrain::SurfacePointRegionFillCallback perPositionCallback,
  212. [[maybe_unused]] AzFramework::Terrain::TerrainDataRequests::Sampler sampleFilter,
  213. AZStd::shared_ptr<AzFramework::Terrain::QueryAsyncParams> params)
  214. -> AZStd::shared_ptr<AzFramework::Terrain::TerrainJobContext>
  215. {
  216. ProcessRegionLoop(
  217. queryRegion, perPositionCallback, nullptr,
  218. []([[maybe_unused]] float x, [[maybe_unused]] float y){ return 0.0f; });
  219. params->m_completionCallback(nullptr);
  220. return nullptr;
  221. });
  222. ActivateEntity(m_entity.get());
  223. size_t cols, rows;
  224. Physics::HeightfieldProviderRequestsBus::Event(
  225. m_entity->GetId(), &Physics::HeightfieldProviderRequestsBus::Events::GetHeightfieldGridSize, cols, rows);
  226. AZStd::vector<float> heights;
  227. Physics::HeightfieldProviderRequestsBus::EventResult(
  228. heights, m_entity->GetId(), &Physics::HeightfieldProviderRequestsBus::Events::GetHeights);
  229. // "max - min" gives us the number of grid squares, "max - min + 1" gives us the number of grid vertices including the final endcap.
  230. const int32_t expectedGridSize = aznumeric_cast<int32_t>(boundsMax - boundsMin) + 1;
  231. EXPECT_EQ(cols, expectedGridSize);
  232. EXPECT_EQ(rows, expectedGridSize);
  233. EXPECT_EQ(heights.size(), cols * rows);
  234. }
  235. TEST_F(TerrainPhysicsColliderComponentTest, TerrainPhysicsColliderReturnsRelativeHeightsCorrectly)
  236. {
  237. // Check that the values stored in the heightfield returned by the TerrainPhysicsCollider are correct.
  238. AddTerrainPhysicsColliderToEntity(Terrain::TerrainPhysicsColliderConfig());
  239. const AZ::Vector3 boundsMin = AZ::Vector3(0.0f);
  240. const AZ::Vector3 boundsMax = AZ::Vector3(256.0f, 256.0f, 32768.0f);
  241. const float mockHeight = 32768.0f;
  242. float mockHeightResolution = 1.0f;
  243. NiceMock<UnitTest::MockTerrainDataRequests> terrainListener;
  244. ON_CALL(terrainListener, GetTerrainHeightQueryResolution).WillByDefault(Return(mockHeightResolution));
  245. ON_CALL(terrainListener, QueryRegion).WillByDefault(
  246. [this, mockHeight](
  247. const AzFramework::Terrain::TerrainQueryRegion& queryRegion,
  248. [[maybe_unused]] AzFramework::Terrain::TerrainDataRequests::TerrainDataMask requestedData,
  249. AzFramework::Terrain::SurfacePointRegionFillCallback perPositionCallback,
  250. [[maybe_unused]] AzFramework::Terrain::TerrainDataRequests::Sampler sampleFilter)
  251. {
  252. ProcessRegionLoop(queryRegion, perPositionCallback, nullptr,
  253. [mockHeight]([[maybe_unused]]float x, [[maybe_unused]]float y){ return mockHeight; });
  254. }
  255. );
  256. ON_CALL(terrainListener, QueryRegionAsync)
  257. .WillByDefault(
  258. [this, mockHeight](
  259. const AzFramework::Terrain::TerrainQueryRegion& queryRegion,
  260. [[maybe_unused]] AzFramework::Terrain::TerrainDataRequests::TerrainDataMask requestedData,
  261. AzFramework::Terrain::SurfacePointRegionFillCallback perPositionCallback,
  262. [[maybe_unused]] AzFramework::Terrain::TerrainDataRequests::Sampler sampleFilter,
  263. AZStd::shared_ptr<AzFramework::Terrain::QueryAsyncParams> params)
  264. -> AZStd::shared_ptr<AzFramework::Terrain::TerrainJobContext>
  265. {
  266. ProcessRegionLoop(
  267. queryRegion, perPositionCallback, nullptr,
  268. [mockHeight]([[maybe_unused]] float x, [[maybe_unused]] float y){ return mockHeight; });
  269. params->m_completionCallback(nullptr);
  270. return nullptr;
  271. });
  272. // Just return the bounds as setup. This is equivalent to the box being at the origin.
  273. NiceMock<UnitTest::MockShapeComponentRequests> boxShape(m_entity->GetId());
  274. const AZ::Aabb bounds = AZ::Aabb::CreateFromMinMax(AZ::Vector3(boundsMin), AZ::Vector3(boundsMax));
  275. ON_CALL(boxShape, GetEncompassingAabb).WillByDefault(Return(bounds));
  276. ActivateEntity(m_entity.get());
  277. AZStd::vector<float> heights;
  278. Physics::HeightfieldProviderRequestsBus::EventResult(heights, m_entity->GetId(), &Physics::HeightfieldProviderRequestsBus::Events::GetHeights);
  279. ASSERT_FALSE(heights.empty());
  280. const float expectedHeightValue = 16384.0f;
  281. EXPECT_NEAR(heights[0], expectedHeightValue, 0.01f);
  282. }
  283. TEST_F(TerrainPhysicsColliderComponentTest, TerrainPhysicsColliderReturnsMaterials)
  284. {
  285. // Check that the TerrainPhysicsCollider returns all the assigned materials.
  286. // Create two SurfaceTag/Material mappings and add them to the collider.
  287. Terrain::TerrainPhysicsColliderConfig config;
  288. const AZ::Data::Asset<Physics::MaterialAsset> mat1(AZ::Data::AssetId(AZ::Uuid::CreateRandom()), {});
  289. const AZ::Data::Asset<Physics::MaterialAsset> mat2(AZ::Data::AssetId(AZ::Uuid::CreateRandom()), {});
  290. const SurfaceData::SurfaceTag tag1 = SurfaceData::SurfaceTag("tag1");
  291. const SurfaceData::SurfaceTag tag2 = SurfaceData::SurfaceTag("tag2");
  292. Terrain::TerrainPhysicsSurfaceMaterialMapping mapping1;
  293. mapping1.m_materialAsset = mat1;
  294. mapping1.m_surfaceTag = tag1;
  295. config.m_surfaceMaterialMappings.emplace_back(mapping1);
  296. Terrain::TerrainPhysicsSurfaceMaterialMapping mapping2;
  297. mapping2.m_materialAsset = mat2;
  298. mapping2.m_surfaceTag = tag2;
  299. config.m_surfaceMaterialMappings.emplace_back(mapping2);
  300. AddTerrainPhysicsColliderToEntity(config);
  301. ActivateEntity(m_entity.get());
  302. AZStd::vector<AZ::Data::Asset<Physics::MaterialAsset>> materialList;
  303. Physics::HeightfieldProviderRequestsBus::EventResult(
  304. materialList, m_entity->GetId(), &Physics::HeightfieldProviderRequestsBus::Events::GetMaterialList);
  305. // The materialList should be 3 items long: the two materials we've added, plus a default material.
  306. EXPECT_EQ(materialList.size(), 3);
  307. const AZ::Data::AssetId nullId; // Select the default material by assigning a null ID to the slot
  308. EXPECT_EQ(materialList[0].GetId(), nullId);
  309. EXPECT_EQ(materialList[1], mat1);
  310. EXPECT_EQ(materialList[2], mat2);
  311. }
  312. TEST_F(TerrainPhysicsColliderComponentTest, TerrainPhysicsColliderReturnsMaterialsWhenNotMapped)
  313. {
  314. // Check that the TerrainPhysicsCollider returns a default material when no surfaces are mapped.
  315. AddTerrainPhysicsColliderToEntity(Terrain::TerrainPhysicsColliderConfig());
  316. ActivateEntity(m_entity.get());
  317. AZStd::vector<AZ::Data::Asset<Physics::MaterialAsset>> materialList;
  318. Physics::HeightfieldProviderRequestsBus::EventResult(
  319. materialList, m_entity->GetId(), &Physics::HeightfieldProviderRequestsBus::Events::GetMaterialList);
  320. // The materialList should be 1 items long: which should be the default material.
  321. EXPECT_EQ(materialList.size(), 1);
  322. const AZ::Data::AssetId nullId; // Select the default material by assigning a null ID to the slot
  323. EXPECT_EQ(materialList[0].GetId(), nullId);
  324. }
  325. TEST_F(TerrainPhysicsColliderComponentTest, TerrainPhysicsColliderGetHeightsAndMaterialsReturnsCorrectly)
  326. {
  327. // Check that the TerrainPhysicsCollider returns a heightfield of the expected size.
  328. // Create two SurfaceTag/Material mappings and add them to the collider.
  329. Terrain::TerrainPhysicsColliderConfig config;
  330. const AZ::Data::Asset<Physics::MaterialAsset> mat1(AZ::Data::AssetId(AZ::Uuid::CreateRandom()), {});
  331. const AZ::Data::Asset<Physics::MaterialAsset> mat2(AZ::Data::AssetId(AZ::Uuid::CreateRandom()), {});
  332. const SurfaceData::SurfaceTag tag1 = SurfaceData::SurfaceTag("tag1");
  333. const SurfaceData::SurfaceTag tag2 = SurfaceData::SurfaceTag("tag2");
  334. Terrain::TerrainPhysicsSurfaceMaterialMapping mapping1;
  335. mapping1.m_materialAsset = mat1;
  336. mapping1.m_surfaceTag = tag1;
  337. config.m_surfaceMaterialMappings.emplace_back(mapping1);
  338. Terrain::TerrainPhysicsSurfaceMaterialMapping mapping2;
  339. mapping2.m_materialAsset = mat2;
  340. mapping2.m_surfaceTag = tag2;
  341. config.m_surfaceMaterialMappings.emplace_back(mapping2);
  342. AddTerrainPhysicsColliderToEntity(config);
  343. const AZ::Vector3 boundsMin = AZ::Vector3(0.0f);
  344. const AZ::Vector3 boundsMax = AZ::Vector3(256.0f, 256.0f, 32768.0f);
  345. NiceMock<UnitTest::MockShapeComponentRequests> boxShape(m_entity->GetId());
  346. const AZ::Aabb bounds = AZ::Aabb::CreateFromMinMax(boundsMin, boundsMax);
  347. ON_CALL(boxShape, GetEncompassingAabb).WillByDefault(Return(bounds));
  348. const float mockHeight = 32768.0f;
  349. float mockHeightResolution = 1.0f;
  350. AzFramework::SurfaceData::SurfaceTagWeight tagWeight1(tag1, 1.0f);
  351. AzFramework::SurfaceData::SurfaceTagWeight tagWeight2(tag2, 1.0f);
  352. AzFramework::SurfaceData::SurfaceTagWeightList surfaceTags = { tagWeight1, tagWeight2 };
  353. NiceMock<UnitTest::MockTerrainDataRequests> terrainListener;
  354. ON_CALL(terrainListener, GetTerrainHeightQueryResolution).WillByDefault(Return(mockHeightResolution));
  355. ON_CALL(terrainListener, QueryRegion).WillByDefault(
  356. [this, mockHeight, &surfaceTags](
  357. const AzFramework::Terrain::TerrainQueryRegion& queryRegion,
  358. [[maybe_unused]] AzFramework::Terrain::TerrainDataRequests::TerrainDataMask requestedData,
  359. AzFramework::Terrain::SurfacePointRegionFillCallback perPositionCallback,
  360. [[maybe_unused]] AzFramework::Terrain::TerrainDataRequests::Sampler sampleFilter)
  361. {
  362. ProcessRegionLoop(queryRegion, perPositionCallback, &surfaceTags,
  363. [mockHeight]([[maybe_unused]]float x, [[maybe_unused]]float y){ return mockHeight; });
  364. }
  365. );
  366. ON_CALL(terrainListener, QueryRegionAsync)
  367. .WillByDefault(
  368. [this, mockHeight, &surfaceTags](
  369. const AzFramework::Terrain::TerrainQueryRegion& queryRegion,
  370. [[maybe_unused]] AzFramework::Terrain::TerrainDataRequests::TerrainDataMask requestedData,
  371. AzFramework::Terrain::SurfacePointRegionFillCallback perPositionCallback,
  372. [[maybe_unused]] AzFramework::Terrain::TerrainDataRequests::Sampler sampleFilter,
  373. AZStd::shared_ptr<AzFramework::Terrain::QueryAsyncParams> params)
  374. -> AZStd::shared_ptr<AzFramework::Terrain::TerrainJobContext>
  375. {
  376. ProcessRegionLoop(
  377. queryRegion, perPositionCallback, &surfaceTags,
  378. [mockHeight]([[maybe_unused]] float x, [[maybe_unused]] float y){ return mockHeight; });
  379. params->m_completionCallback(nullptr);
  380. return nullptr;
  381. });
  382. ActivateEntity(m_entity.get());
  383. AZStd::vector<Physics::HeightMaterialPoint> heightsAndMaterials;
  384. Physics::HeightfieldProviderRequestsBus::EventResult(
  385. heightsAndMaterials, m_entity->GetId(), &Physics::HeightfieldProviderRequestsBus::Events::GetHeightsAndMaterials);
  386. size_t cols, rows;
  387. Physics::HeightfieldProviderRequestsBus::Event(
  388. m_entity->GetId(), &Physics::HeightfieldProviderRequestsBus::Events::GetHeightfieldGridSize, cols, rows);
  389. // "max - min" gives us the number of grid squares, "max - min + 1" gives us the number of grid vertices including the final endcap.
  390. const int32_t expectedGridSize = aznumeric_cast<int32_t>(boundsMax.GetX() - boundsMin.GetX()) + 1;
  391. // Check that the correct number of entries are present.
  392. // We expect 257 x 257 because there should be an extra point in each direction to "cap off" each grid square.
  393. EXPECT_EQ(cols, expectedGridSize);
  394. EXPECT_EQ(rows, expectedGridSize);
  395. EXPECT_EQ(heightsAndMaterials.size(), cols * rows);
  396. const float expectedHeightValue = 16384.0f;
  397. // Check an entry from the first half of the returned list.
  398. EXPECT_EQ(heightsAndMaterials[0].m_materialIndex, 1);
  399. EXPECT_NEAR(heightsAndMaterials[0].m_height, expectedHeightValue, 0.01f);
  400. // Check an entry from the second half of the list
  401. EXPECT_EQ(heightsAndMaterials[cols * 128].m_materialIndex, 2);
  402. EXPECT_NEAR(heightsAndMaterials[cols * 128].m_height, expectedHeightValue, 0.01f);
  403. }
  404. TEST_F(TerrainPhysicsColliderComponentTest, TerrainPhysicsColliderDefaultMaterialAssignedWhenTagHasNoMapping)
  405. {
  406. // Create two SurfaceTag/Material mappings and add them to the collider.
  407. Terrain::TerrainPhysicsColliderConfig config;
  408. const AZ::Data::Asset<Physics::MaterialAsset> defaultSurfaceMaterial(AZ::Data::AssetId(AZ::Uuid::CreateRandom()), {});
  409. const AZ::Data::Asset<Physics::MaterialAsset> mat1(AZ::Data::AssetId(AZ::Uuid::CreateRandom()), {});
  410. const SurfaceData::SurfaceTag tag1 = SurfaceData::SurfaceTag("tag1");
  411. const SurfaceData::SurfaceTag tag2 = SurfaceData::SurfaceTag("tag2");
  412. Terrain::TerrainPhysicsSurfaceMaterialMapping mapping1;
  413. mapping1.m_materialAsset = mat1;
  414. mapping1.m_surfaceTag = tag1;
  415. config.m_surfaceMaterialMappings.emplace_back(mapping1);
  416. config.m_defaultMaterialAsset = defaultSurfaceMaterial;
  417. // Intentionally don't set the mapping for "tag2". It's expected the default material will substitute.
  418. AddTerrainPhysicsColliderToEntity(config);
  419. const AZ::Vector3 boundsMin = AZ::Vector3(0.0f);
  420. const AZ::Vector3 boundsMax = AZ::Vector3(256.0f, 256.0f, 32768.0f);
  421. NiceMock<UnitTest::MockShapeComponentRequests> boxShape(m_entity->GetId());
  422. const AZ::Aabb bounds = AZ::Aabb::CreateFromMinMax(boundsMin, boundsMax);
  423. ON_CALL(boxShape, GetEncompassingAabb).WillByDefault(Return(bounds));
  424. const float mockHeight = 32768.0f;
  425. float mockHeightResolution = 1.0f;
  426. AzFramework::SurfaceData::SurfaceTagWeight tagWeight1(tag1, 1.0f);
  427. AzFramework::SurfaceData::SurfaceTagWeight tagWeight2(tag2, 1.0f);
  428. AzFramework::SurfaceData::SurfaceTagWeightList surfaceTags = { tagWeight1, tagWeight2 };
  429. NiceMock<UnitTest::MockTerrainDataRequests> terrainListener;
  430. ON_CALL(terrainListener, GetTerrainHeightQueryResolution).WillByDefault(Return(mockHeightResolution));
  431. ON_CALL(terrainListener, QueryRegion).WillByDefault(
  432. [this, mockHeight, &surfaceTags](
  433. const AzFramework::Terrain::TerrainQueryRegion& queryRegion,
  434. [[maybe_unused]] AzFramework::Terrain::TerrainDataRequests::TerrainDataMask requestedData,
  435. AzFramework::Terrain::SurfacePointRegionFillCallback perPositionCallback,
  436. [[maybe_unused]] AzFramework::Terrain::TerrainDataRequests::Sampler sampleFilter)
  437. {
  438. ProcessRegionLoop(queryRegion, perPositionCallback, &surfaceTags,
  439. [mockHeight]([[maybe_unused]]float x, [[maybe_unused]]float y){ return mockHeight; });
  440. }
  441. );
  442. ON_CALL(terrainListener, QueryRegionAsync)
  443. .WillByDefault(
  444. [this, mockHeight, &surfaceTags](
  445. const AzFramework::Terrain::TerrainQueryRegion& queryRegion,
  446. [[maybe_unused]] AzFramework::Terrain::TerrainDataRequests::TerrainDataMask requestedData,
  447. AzFramework::Terrain::SurfacePointRegionFillCallback perPositionCallback,
  448. [[maybe_unused]] AzFramework::Terrain::TerrainDataRequests::Sampler sampleFilter,
  449. AZStd::shared_ptr<AzFramework::Terrain::QueryAsyncParams> params)
  450. -> AZStd::shared_ptr<AzFramework::Terrain::TerrainJobContext>
  451. {
  452. ProcessRegionLoop(
  453. queryRegion, perPositionCallback, &surfaceTags,
  454. [mockHeight]([[maybe_unused]] float x, [[maybe_unused]] float y){ return mockHeight; });
  455. params->m_completionCallback(nullptr);
  456. return nullptr;
  457. });
  458. ActivateEntity(m_entity.get());
  459. // Validate material list is generated with the default material
  460. {
  461. AZStd::vector<AZ::Data::Asset<Physics::MaterialAsset>> materialList;
  462. Physics::HeightfieldProviderRequestsBus::EventResult(
  463. materialList, m_entity->GetId(), &Physics::HeightfieldProviderRequestsBus::Events::GetMaterialList);
  464. // The materialList should be 2 items long: the default material and mat1.
  465. EXPECT_EQ(materialList.size(), 2);
  466. EXPECT_EQ(materialList[0], defaultSurfaceMaterial);
  467. EXPECT_EQ(materialList[1], mat1);
  468. }
  469. // Validate material indices
  470. {
  471. AZStd::vector<Physics::HeightMaterialPoint> heightsAndMaterials;
  472. Physics::HeightfieldProviderRequestsBus::EventResult(
  473. heightsAndMaterials, m_entity->GetId(), &Physics::HeightfieldProviderRequestsBus::Events::GetHeightsAndMaterials);
  474. size_t cols, rows;
  475. Physics::HeightfieldProviderRequestsBus::Event(
  476. m_entity->GetId(), &Physics::HeightfieldProviderRequestsBus::Events::GetHeightfieldGridSize, cols, rows);
  477. // "max - min" gives us the number of grid squares, "max - min + 1" gives us the number of grid vertices including the final endcap.
  478. const int32_t expectedGridSize = aznumeric_cast<int32_t>(boundsMax.GetX() - boundsMin.GetX()) + 1;
  479. // Check that the correct number of entries are present.
  480. // We expect 257 x 257 because there should be an extra point in each direction to "cap off" each grid square.
  481. EXPECT_EQ(cols, expectedGridSize);
  482. EXPECT_EQ(rows, expectedGridSize);
  483. EXPECT_EQ(heightsAndMaterials.size(), cols * rows);
  484. // Check an entry from the first half of the returned list.
  485. EXPECT_EQ(heightsAndMaterials[0].m_materialIndex, 1);
  486. // Check an entry from the second half of the list.
  487. // This should point to the default material (0) since we don't have a mapping for "tag2"
  488. EXPECT_EQ(heightsAndMaterials[cols * 128].m_materialIndex, 0);
  489. }
  490. }
  491. TEST_F(TerrainPhysicsColliderComponentTest, TerrainPhysicsColliderDefaultMaterialAssignedWhenNoMappingsExist)
  492. {
  493. // Create only the default material with no mapping for the tags. It's expected the default material will be assigned to both tags.
  494. Terrain::TerrainPhysicsColliderConfig config;
  495. const AZ::Data::Asset<Physics::MaterialAsset> defaultSurfaceMaterial(AZ::Data::AssetId(AZ::Uuid::CreateRandom()), {});
  496. config.m_defaultMaterialAsset = defaultSurfaceMaterial;
  497. AddTerrainPhysicsColliderToEntity(config);
  498. const AZ::Vector3 boundsMin = AZ::Vector3(0.0f);
  499. const AZ::Vector3 boundsMax = AZ::Vector3(256.0f, 256.0f, 32768.0f);
  500. NiceMock<UnitTest::MockShapeComponentRequests> boxShape(m_entity->GetId());
  501. const AZ::Aabb bounds = AZ::Aabb::CreateFromMinMax(boundsMin, boundsMax);
  502. ON_CALL(boxShape, GetEncompassingAabb).WillByDefault(Return(bounds));
  503. const float mockHeight = 32768.0f;
  504. float mockHeightResolution = 1.0f;
  505. const SurfaceData::SurfaceTag tag1 = SurfaceData::SurfaceTag("tag1");
  506. AzFramework::SurfaceData::SurfaceTagWeight tagWeight1(tag1, 1.0f);
  507. const SurfaceData::SurfaceTag tag2 = SurfaceData::SurfaceTag("tag2");
  508. AzFramework::SurfaceData::SurfaceTagWeight tagWeight2(tag2, 1.0f);
  509. AzFramework::SurfaceData::SurfaceTagWeightList surfaceTags = { tagWeight1, tagWeight2 };
  510. NiceMock<UnitTest::MockTerrainDataRequests> terrainListener;
  511. ON_CALL(terrainListener, GetTerrainHeightQueryResolution).WillByDefault(Return(mockHeightResolution));
  512. ON_CALL(terrainListener, QueryRegion).WillByDefault(
  513. [this, mockHeight, &surfaceTags](
  514. const AzFramework::Terrain::TerrainQueryRegion& queryRegion,
  515. [[maybe_unused]] AzFramework::Terrain::TerrainDataRequests::TerrainDataMask requestedData,
  516. AzFramework::Terrain::SurfacePointRegionFillCallback perPositionCallback,
  517. [[maybe_unused]] AzFramework::Terrain::TerrainDataRequests::Sampler sampleFilter)
  518. {
  519. ProcessRegionLoop(queryRegion, perPositionCallback, &surfaceTags,
  520. [mockHeight]([[maybe_unused]]float x, [[maybe_unused]]float y){ return mockHeight; });
  521. }
  522. );
  523. ON_CALL(terrainListener, QueryRegionAsync)
  524. .WillByDefault(
  525. [this, mockHeight, &surfaceTags](
  526. const AzFramework::Terrain::TerrainQueryRegion& queryRegion,
  527. [[maybe_unused]] AzFramework::Terrain::TerrainDataRequests::TerrainDataMask requestedData,
  528. AzFramework::Terrain::SurfacePointRegionFillCallback perPositionCallback,
  529. [[maybe_unused]] AzFramework::Terrain::TerrainDataRequests::Sampler sampleFilter,
  530. AZStd::shared_ptr<AzFramework::Terrain::QueryAsyncParams> params)
  531. -> AZStd::shared_ptr<AzFramework::Terrain::TerrainJobContext>
  532. {
  533. ProcessRegionLoop(
  534. queryRegion, perPositionCallback, &surfaceTags,
  535. [mockHeight]([[maybe_unused]] float x, [[maybe_unused]] float y){ return mockHeight; });
  536. params->m_completionCallback(nullptr);
  537. return nullptr;
  538. });
  539. ActivateEntity(m_entity.get());
  540. // Validate material list is generated with the default material
  541. {
  542. AZStd::vector<AZ::Data::Asset<Physics::MaterialAsset>> materialList;
  543. Physics::HeightfieldProviderRequestsBus::EventResult(
  544. materialList, m_entity->GetId(), &Physics::HeightfieldProviderRequestsBus::Events::GetMaterialList);
  545. EXPECT_EQ(materialList.size(), 1);
  546. EXPECT_EQ(materialList[0], defaultSurfaceMaterial);
  547. }
  548. // Validate material indices
  549. {
  550. AZStd::vector<Physics::HeightMaterialPoint> heightsAndMaterials;
  551. Physics::HeightfieldProviderRequestsBus::EventResult(
  552. heightsAndMaterials, m_entity->GetId(), &Physics::HeightfieldProviderRequestsBus::Events::GetHeightsAndMaterials);
  553. size_t cols, rows;
  554. Physics::HeightfieldProviderRequestsBus::Event(
  555. m_entity->GetId(), &Physics::HeightfieldProviderRequestsBus::Events::GetHeightfieldGridSize, cols, rows);
  556. // "max - min" gives us the number of grid squares, "max - min + 1" gives us the number of grid vertices including the final endcap.
  557. const int32_t expectedGridSize = aznumeric_cast<int32_t>(boundsMax.GetX() - boundsMin.GetX()) + 1;
  558. // Check that the correct number of entries are present.
  559. // We expect 257 x 257 because there should be an extra point in each direction to "cap off" each grid square.
  560. EXPECT_EQ(cols, expectedGridSize);
  561. EXPECT_EQ(rows, expectedGridSize);
  562. EXPECT_EQ(heightsAndMaterials.size(), cols * rows);
  563. // Check an entry from the first half of the returned list. Should be the default material index 0.
  564. EXPECT_EQ(heightsAndMaterials[0].m_materialIndex, 0);
  565. // Check an entry from the second half of the list. Should be the default material index 0.
  566. EXPECT_EQ(heightsAndMaterials[cols * 128].m_materialIndex, 0);
  567. }
  568. }
  569. TEST_F(TerrainPhysicsColliderComponentTest, TerrainPhysicsColliderRequestSubpartForDirtyRegion)
  570. {
  571. // The test validates the requested sub-part of terrain collider matches the source data
  572. AddTerrainPhysicsColliderToEntity(Terrain::TerrainPhysicsColliderConfig());
  573. const int32_t terrainSize = 256;
  574. const int32_t expectedGridSize = terrainSize + 1;
  575. const AZ::Vector3 boundsMin = AZ::Vector3(0.0f);
  576. const AZ::Vector3 boundsMax = AZ::Vector3(terrainSize, terrainSize, 512.0f);
  577. NiceMock<UnitTest::MockShapeComponentRequests> boxShape(m_entity->GetId());
  578. const AZ::Aabb bounds = AZ::Aabb::CreateFromMinMax(boundsMin, boundsMax);
  579. ON_CALL(boxShape, GetEncompassingAabb).WillByDefault(Return(bounds));
  580. AzFramework::SurfaceData::SurfaceTagWeight tagWeight1(AZ::Crc32("tag1"), 1.0f);
  581. AzFramework::SurfaceData::SurfaceTagWeight tagWeight2(AZ::Crc32("tag2"), 1.0f);
  582. AzFramework::SurfaceData::SurfaceTagWeightList surfaceTags = { tagWeight1, tagWeight2 };
  583. float mockHeightResolution = 1.0f;
  584. NiceMock<UnitTest::MockTerrainDataRequests> terrainListener;
  585. ON_CALL(terrainListener, GetTerrainHeightQueryResolution).WillByDefault(Return(mockHeightResolution));
  586. ON_CALL(terrainListener, QueryRegion).WillByDefault(
  587. [this, &surfaceTags](
  588. const AzFramework::Terrain::TerrainQueryRegion& queryRegion,
  589. [[maybe_unused]] AzFramework::Terrain::TerrainDataRequests::TerrainDataMask requestedData,
  590. AzFramework::Terrain::SurfacePointRegionFillCallback perPositionCallback,
  591. [[maybe_unused]] AzFramework::Terrain::TerrainDataRequests::Sampler sampleFilter)
  592. {
  593. // Assign a variety of heights across the terrain
  594. ProcessRegionLoop(queryRegion, perPositionCallback, &surfaceTags,
  595. [](float x, float y){ return x + y; });
  596. }
  597. );
  598. ON_CALL(terrainListener, QueryRegionAsync)
  599. .WillByDefault(
  600. [this, &surfaceTags](
  601. const AzFramework::Terrain::TerrainQueryRegion& queryRegion,
  602. [[maybe_unused]] AzFramework::Terrain::TerrainDataRequests::TerrainDataMask requestedData,
  603. AzFramework::Terrain::SurfacePointRegionFillCallback perPositionCallback,
  604. [[maybe_unused]] AzFramework::Terrain::TerrainDataRequests::Sampler sampleFilter,
  605. AZStd::shared_ptr<AzFramework::Terrain::QueryAsyncParams> params)
  606. -> AZStd::shared_ptr<AzFramework::Terrain::TerrainJobContext>
  607. {
  608. ProcessRegionLoop(
  609. queryRegion, perPositionCallback, &surfaceTags,
  610. [](float x, float y){ return x + y; });
  611. params->m_completionCallback(nullptr);
  612. return nullptr;
  613. });
  614. ActivateEntity(m_entity.get());
  615. // Get the entire array of points
  616. AZStd::vector<Physics::HeightMaterialPoint> heightsMaterials = m_colliderComponent->GetHeightsAndMaterials();
  617. EXPECT_EQ(heightsMaterials.size(), expectedGridSize * expectedGridSize);
  618. // Request a sub-part of the terrain and validate the points match the original data
  619. int32_t callCounter = 0;
  620. Physics::UpdateHeightfieldSampleFunction validateDataCallback =
  621. [&callCounter, &heightsMaterials](size_t column, size_t row, const Physics::HeightMaterialPoint& dataPoint)
  622. {
  623. size_t lookUpIndex = row * expectedGridSize + column;
  624. EXPECT_LT(lookUpIndex, heightsMaterials.size());
  625. EXPECT_EQ(heightsMaterials[lookUpIndex].m_height, dataPoint.m_height);
  626. ++callCounter;
  627. };
  628. AZ::Vector3 regionMin(AZ::Vector3(10.0f));
  629. AZ::Vector3 regionMax(AZ::Vector3(200.0f));
  630. int32_t dx = int32_t(regionMax.GetX() - regionMin.GetX()) + 1;
  631. int32_t dy = int32_t(regionMax.GetY() - regionMin.GetY()) + 1;
  632. size_t startRow, startColumn, numRows, numColumns;
  633. m_colliderComponent->GetHeightfieldIndicesFromRegion(
  634. AZ::Aabb::CreateFromMinMax(regionMin, regionMax), startColumn, startRow, numColumns, numRows);
  635. m_colliderComponent->UpdateHeightsAndMaterials(validateDataCallback, startColumn, startRow, numColumns, numRows);
  636. // Validate update heightfield callback was called the exact amount of times required for the region
  637. EXPECT_EQ(dx * dy, callCounter);
  638. }